aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/sonar-web/package.json10
-rw-r--r--server/sonar-web/src/main/js/apps/issues/controller.js2
-rw-r--r--server/sonar-web/src/main/js/apps/issues/models/issues.js4
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js159
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.js59
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/SourceViewerIssueLocations.js319
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/SourceViewerLine.js81
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/helpers/highlight.js3
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/helpers/indexing.js67
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/helpers/issueLocations.js18
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/styles.css92
-rw-r--r--server/sonar-web/src/main/js/components/issue/Issue.js4
-rw-r--r--server/sonar-web/src/main/js/components/issue/types.js10
-rw-r--r--server/sonar-web/src/main/js/components/issue/views/changelog-view.js1
-rw-r--r--server/sonar-web/src/main/js/helpers/issues.js7
-rw-r--r--server/sonar-web/src/main/js/helpers/scrolling.js83
-rw-r--r--server/sonar-web/src/main/less/components/source.less16
-rw-r--r--server/sonar-web/src/main/less/components/ui.less5
-rw-r--r--server/sonar-web/src/main/less/pages/issues.less2
-rw-r--r--server/sonar-web/yarn.lock59
20 files changed, 849 insertions, 152 deletions
diff --git a/server/sonar-web/package.json b/server/sonar-web/package.json
index 1a993a9e742..2b942f126a4 100644
--- a/server/sonar-web/package.json
+++ b/server/sonar-web/package.json
@@ -19,15 +19,17 @@
"lodash": "4.6.1",
"moment": "2.10.6",
"numeral": "1.5.3",
- "react": "15.3.2",
- "react-addons-shallow-compare": "15.3.2",
- "react-dom": "15.3.2",
+ "react": "15.4.2",
+ "react-addons-shallow-compare": "15.4.2",
+ "react-dom": "15.4.2",
+ "react-draggable": "2.2.3",
"react-helmet": "3.1.0",
"react-modal": "^1.6.4",
"react-redux": "4.4.1",
"react-router": "2.8.1",
"react-router-redux": "4.0.2",
"react-select": "^1.0.0-rc.2",
+ "react-virtualized": "^9.1.0",
"redux": "3.3.1",
"redux-logger": "2.2.1",
"redux-thunk": "1.0.2",
@@ -82,7 +84,7 @@
"less-loader": "2.2.3",
"path-exists": "2.1.0",
"postcss-loader": "0.8.0",
- "react-addons-test-utils": "15.3.2",
+ "react-addons-test-utils": "15.4.2",
"react-dev-utils": "0.2.1",
"react-transform-hmr": "1.0.4",
"recursive-readdir": "2.1.0",
diff --git a/server/sonar-web/src/main/js/apps/issues/controller.js b/server/sonar-web/src/main/js/apps/issues/controller.js
index dd03639f5b0..156a7f3632a 100644
--- a/server/sonar-web/src/main/js/apps/issues/controller.js
+++ b/server/sonar-web/src/main/js/apps/issues/controller.js
@@ -63,8 +63,10 @@ export default Controller.extend({
const issues = that.options.app.list.parseIssues(r);
this.receiveIssues(issues);
if (firstPage) {
+ const issues = that.options.app.list.parseIssues(r);
that.options.app.list.reset(issues);
} else {
+ const issues = that.options.app.list.parseIssues(r, that.options.app.list.length);
that.options.app.list.add(issues);
}
that.options.app.list.setIndex();
diff --git a/server/sonar-web/src/main/js/apps/issues/models/issues.js b/server/sonar-web/src/main/js/apps/issues/models/issues.js
index 6106c81404f..d7340187c0a 100644
--- a/server/sonar-web/src/main/js/apps/issues/models/issues.js
+++ b/server/sonar-web/src/main/js/apps/issues/models/issues.js
@@ -76,10 +76,10 @@ export default Backbone.Collection.extend({
return issue;
},
- parseIssues (r) {
+ parseIssues (r, startIndex = 0) {
const that = this;
return r.issues.map((issue, index) => {
- Object.assign(issue, { index });
+ Object.assign(issue, { index: startIndex + index });
issue = that._injectRelational(issue, r.components, 'component', 'key');
issue = that._injectRelational(issue, r.components, 'project', 'key');
issue = that._injectRelational(issue, r.components, 'subProject', 'key');
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 2ad750e7120..4a011e163c3 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js
+++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js
@@ -23,12 +23,12 @@ import classNames from 'classnames';
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 { TooltipsContainer } from '../mixins/tooltips-mixin';
import Source from '../source-viewer/source';
import loadIssues from './helpers/loadIssues';
import getCoverageStatus from './helpers/getCoverageStatus';
@@ -38,12 +38,21 @@ import {
locationsByIssueAndLine,
locationMessagesByIssueAndLine,
duplicationsByLine,
- symbolsByLine
+ symbolsByLine,
+ findLocationByIndex
+} from './helpers/indexing';
+import type {
+ LinearIssueLocation,
+ IndexedIssueLocation,
+ IndexedIssueLocationsByIssueAndLine,
+ IndexedIssueLocationMessagesByIssueAndLine
} from './helpers/indexing';
import { getComponentForSourceViewer, getSources, getDuplications, getTests } from '../../api/components';
import { translate } from '../../helpers/l10n';
+import { scrollToElement } from '../../helpers/scrolling';
import type { SourceLine } from './types';
import type { Issue } from '../issue/types';
+import './styles.css';
// TODO react-virtualized
@@ -81,28 +90,25 @@ type State = {
highlightedSymbol: string | null,
issues?: Array<Issue>,
issuesByLine: { [number]: Array<string> },
- issueLocationsByLine: { [number]: Array<{ from: number, to: number }> },
- issueSecondaryLocationsByIssueByLine: {
- [string]: {
- [number]: Array<{ from: number, to: number }>
- }
- },
- issueSecondaryLocationMessagesByIssueByLine: {
- [issueKey: string]: {
- [line: number]: Array<{ msg: string, index?: number }>
- }
- },
+ issueLocationsByLine: { [number]: Array<LinearIssueLocation> },
+ issueSecondaryLocationsByIssueByLine: IndexedIssueLocationsByIssueAndLine,
+ issueSecondaryLocationMessagesByIssueByLine: IndexedIssueLocationMessagesByIssueAndLine,
loading: boolean,
loadingSourcesAfter: boolean,
loadingSourcesBefore: boolean,
+ locationsPanelHeight: number,
notAccessible: boolean,
notExist: boolean,
+ selectedIssueLocation: IndexedIssueLocation | null,
sources?: Array<SourceLine>,
symbolsByLine: { [number]: Array<string> }
};
const LINES = 500;
+const LOCATIONS_PANEL_DEFAULT_HEIGHT = 200;
+const LOCATIONS_PANEL_HEIGHT_LOCAL_STORAGE_KEY = 'sonarqube.locations.height';
+
const loadComponent = (key: string): Promise<*> => {
return getComponentForSourceViewer(key);
};
@@ -141,9 +147,11 @@ export default class SourceViewerBase extends React.Component {
loading: true,
loadingSourcesAfter: false,
loadingSourcesBefore: false,
+ locationsPanelHeight: this.getInitialLocationsPanelHeight(),
notAccessible: false,
notExist: false,
selectedIssue: props.defaultSelectedIssue || null,
+ selectedIssueLocation: null,
symbolsByLine: {}
};
}
@@ -153,19 +161,33 @@ export default class SourceViewerBase extends React.Component {
this.fetchComponent();
}
- componentDidUpdate (prevProps: Props) {
+ 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)) {
this.fetchSources();
}
+
+ if (prevState.selectedIssueLocation !== this.state.selectedIssueLocation &&
+ this.state.selectedIssueLocation != null) {
+ this.scrollToLine(this.state.selectedIssueLocation.line);
+ }
}
componentWillUnmount () {
this.mounted = false;
}
+ scrollToLine (line: number) {
+ const lineElement = this.node.querySelector(
+ `.source-line-code[data-line-number="${line}"] .source-line-issue-locations`
+ );
+ if (lineElement) {
+ scrollToElement(lineElement, 125, this.state.locationsPanelHeight + 75);
+ }
+ }
+
computeCoverageStatus (lines: Array<SourceLine>): Array<SourceLine> {
return lines.map(line => ({ ...line, coverageStatus: getCoverageStatus(line) }));
}
@@ -342,6 +364,23 @@ export default class SourceViewerBase extends React.Component {
});
};
+ getInitialLocationsPanelHeight () {
+ try {
+ const rawValue = window.localStorage.getItem(LOCATIONS_PANEL_HEIGHT_LOCAL_STORAGE_KEY);
+ if (!rawValue) {
+ return LOCATIONS_PANEL_DEFAULT_HEIGHT;
+ }
+ const intValue = Number(rawValue);
+ return !isNaN(intValue) ? intValue : LOCATIONS_PANEL_DEFAULT_HEIGHT;
+ } catch (e) {
+ return LOCATIONS_PANEL_DEFAULT_HEIGHT;
+ }
+ }
+
+ storeLocationsPanelHeight (height: number) {
+ window.localStorage.setItem(LOCATIONS_PANEL_HEIGHT_LOCAL_STORAGE_KEY, height);
+ }
+
openNewWindow = () => {
const { component } = this.state;
if (component != null) {
@@ -424,41 +463,57 @@ export default class SourceViewerBase extends React.Component {
popup.render();
};
+ handleSelectIssueLocation = (flowIndex: number, locationIndex: number) => {
+ this.setState(prevState => {
+ const selectedIssueLocation = findLocationByIndex(
+ prevState.issueSecondaryLocationsByIssueByLine,
+ flowIndex,
+ locationIndex
+ );
+ return { selectedIssueLocation };
+ });
+ };
+
+ handleLocationsPanelResize = (height: number) => {
+ this.setState({ locationsPanelHeight: height });
+ this.storeLocationsPanelHeight(height);
+ };
+
renderCode (sources: Array<SourceLine>) {
const hasSourcesBefore = sources.length > 0 && sources[0].line > 1;
return (
- <TooltipsContainer>
- <SourceViewerCode
- displayAllIssues={this.props.displayAllIssues}
- duplications={this.state.duplications}
- duplicationsByLine={this.state.duplicationsByLine}
- duplicatedFiles={this.state.duplicatedFiles}
- hasSourcesBefore={hasSourcesBefore}
- hasSourcesAfter={this.state.hasSourcesAfter}
- filterLine={this.props.filterLine}
- highlightedLine={this.state.highlightedLine}
- highlightedSymbol={this.state.highlightedSymbol}
- issues={this.state.issues}
- issuesByLine={this.state.issuesByLine}
- issueLocationsByLine={this.state.issueLocationsByLine}
- issueSecondaryLocationsByIssueByLine={this.state.issueSecondaryLocationsByIssueByLine}
- issueSecondaryLocationMessagesByIssueByLine={this.state.issueSecondaryLocationMessagesByIssueByLine}
- loadDuplications={this.loadDuplications}
- loadSourcesAfter={this.loadSourcesAfter}
- loadSourcesBefore={this.loadSourcesBefore}
- loadingSourcesAfter={this.state.loadingSourcesAfter}
- loadingSourcesBefore={this.state.loadingSourcesBefore}
- onCoverageClick={this.handleCoverageClick}
- onDuplicationClick={this.handleDuplicationClick}
- onIssueSelect={this.props.onIssueSelect}
- onIssueUnselect={this.props.onIssueUnselect}
- onLineClick={this.handleLineClick}
- onSCMClick={this.handleSCMClick}
- onSymbolClick={this.handleSymbolClick}
- selectedIssue={this.props.selectedIssue}
- sources={sources}
- symbolsByLine={this.state.symbolsByLine}/>
- </TooltipsContainer>
+ <SourceViewerCode
+ displayAllIssues={this.props.displayAllIssues}
+ duplications={this.state.duplications}
+ duplicationsByLine={this.state.duplicationsByLine}
+ duplicatedFiles={this.state.duplicatedFiles}
+ hasSourcesBefore={hasSourcesBefore}
+ hasSourcesAfter={this.state.hasSourcesAfter}
+ filterLine={this.props.filterLine}
+ highlightedLine={this.state.highlightedLine}
+ highlightedSymbol={this.state.highlightedSymbol}
+ issues={this.state.issues}
+ issuesByLine={this.state.issuesByLine}
+ issueLocationsByLine={this.state.issueLocationsByLine}
+ issueSecondaryLocationsByIssueByLine={this.state.issueSecondaryLocationsByIssueByLine}
+ issueSecondaryLocationMessagesByIssueByLine={this.state.issueSecondaryLocationMessagesByIssueByLine}
+ loadDuplications={this.loadDuplications}
+ loadSourcesAfter={this.loadSourcesAfter}
+ loadSourcesBefore={this.loadSourcesBefore}
+ loadingSourcesAfter={this.state.loadingSourcesAfter}
+ loadingSourcesBefore={this.state.loadingSourcesBefore}
+ onCoverageClick={this.handleCoverageClick}
+ onDuplicationClick={this.handleDuplicationClick}
+ onIssueSelect={this.props.onIssueSelect}
+ onIssueUnselect={this.props.onIssueUnselect}
+ onLineClick={this.handleLineClick}
+ onSCMClick={this.handleSCMClick}
+ onSelectLocation={this.handleSelectIssueLocation}
+ onSymbolClick={this.handleSymbolClick}
+ selectedIssue={this.props.selectedIssue}
+ selectedIssueLocation={this.state.selectedIssueLocation}
+ sources={sources}
+ symbolsByLine={this.state.symbolsByLine}/>
);
}
@@ -481,6 +536,10 @@ export default class SourceViewerBase extends React.Component {
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;
+
return (
<div className={className} ref={node => this.node = node}>
<SourceViewerHeader
@@ -493,6 +552,14 @@ export default class SourceViewerBase extends React.Component {
</div>
)}
{this.state.sources != null && this.renderCode(this.state.sources)}
+ {selectedIssueObj != null && selectedIssueObj.flows.length > 0 && (
+ <SourceViewerIssueLocations
+ height={this.state.locationsPanelHeight}
+ issue={selectedIssueObj}
+ onResize={this.handleLocationsPanelResize}
+ onSelectLocation={this.handleSelectIssueLocation}
+ 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 32092dd47c5..fb98c06ab15 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.js
+++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.js
@@ -20,9 +20,16 @@
// @flow
import React from 'react';
import SourceViewerLine from './SourceViewerLine';
+import { TooltipsContainer } from '../mixins/tooltips-mixin';
import { translate } from '../../helpers/l10n';
import type { Duplication, SourceLine } from './types';
import type { Issue } from '../issue/types';
+import type {
+ LinearIssueLocation,
+ IndexedIssueLocation,
+ IndexedIssueLocationsByIssueAndLine,
+ IndexedIssueLocationMessagesByIssueAndLine
+} from './helpers/indexing';
const EMPTY_ARRAY = [];
@@ -32,7 +39,7 @@ const ZERO_LINE = {
line: 0
};
-export default class SourceViewerCode extends React.Component {
+export default class SourceViewerCode extends React.PureComponent {
props: {
displayAllIssues: boolean,
duplications?: Array<Duplication>,
@@ -45,17 +52,9 @@ export default class SourceViewerCode extends React.Component {
highlightedSymbol: string | null,
issues: Array<Issue>,
issuesByLine: { [number]: Array<string> },
- issueLocationsByLine: { [number]: Array<{ from: number, to: number }> },
- issueSecondaryLocationsByIssueByLine: {
- [string]: {
- [number]: Array<{ from: number, to: number }>
- }
- },
- issueSecondaryLocationMessagesByIssueByLine: {
- [issueKey: string]: {
- [line: number]: Array<{ msg: string, index?: number }>
- }
- },
+ issueLocationsByLine: { [number]: Array<LinearIssueLocation> },
+ issueSecondaryLocationsByIssueByLine: IndexedIssueLocationsByIssueAndLine,
+ issueSecondaryLocationMessagesByIssueByLine: IndexedIssueLocationMessagesByIssueAndLine,
loadDuplications: (SourceLine, HTMLElement) => void,
loadSourcesAfter: () => void,
loadSourcesBefore: () => void,
@@ -67,8 +66,10 @@ export default class SourceViewerCode extends React.Component {
onIssueUnselect: () => void,
onLineClick: (number, HTMLElement) => void,
onSCMClick: (SourceLine, HTMLElement) => void,
+ onSelectLocation: (flowIndex: number, locationIndex: number) => void,
onSymbolClick: (string) => void,
selectedIssue: string | null,
+ selectedIssueLocation: IndexedIssueLocation | null,
sources: Array<SourceLine>,
symbolsByLine: { [number]: Array<string> }
};
@@ -133,6 +134,14 @@ export default class SourceViewerCode extends React.Component {
const optimizedSelectedIssue = selectedIssue != null && issuesForLine.includes(selectedIssue) ?
selectedIssue : null;
+ const { selectedIssueLocation } = this.props;
+ const optimizedSelectedIssueLocation =
+ selectedIssueLocation != null &&
+ secondaryIssueLocations.some(location =>
+ location.flowIndex === selectedIssueLocation.flowIndex &&
+ location.locationIndex === selectedIssueLocation.locationIndex
+ ) ? selectedIssueLocation : null;
+
return (
<SourceViewerLine
displayAllIssues={this.props.displayAllIssues}
@@ -157,10 +166,12 @@ export default class SourceViewerCode extends React.Component {
onIssueSelect={this.props.onIssueSelect}
onIssueUnselect={this.props.onIssueUnselect}
onSCMClick={this.props.onSCMClick}
+ onSelectLocation={this.props.onSelectLocation}
onSymbolClick={this.props.onSymbolClick}
secondaryIssueLocations={secondaryIssueLocations}
secondaryIssueLocationMessages={secondaryIssueLocationMessages}
- selectedIssue={optimizedSelectedIssue}/>
+ selectedIssue={optimizedSelectedIssue}
+ selectedIssueLocation={optimizedSelectedIssueLocation}/>
);
};
@@ -191,16 +202,18 @@ export default class SourceViewerCode extends React.Component {
</div>
)}
- <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)
- ))}
- </tbody>
- </table>
+ <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)
+ ))}
+ </tbody>
+ </table>
+ </TooltipsContainer>
{this.props.hasSourcesAfter && (
<div className="source-viewer-more-code">
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerIssueLocations.js b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerIssueLocations.js
new file mode 100644
index 00000000000..d4881347372
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerIssueLocations.js
@@ -0,0 +1,319 @@
+/*
+ * 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 AutoSizer from 'react-virtualized/dist/commonjs/AutoSizer';
+import { DraggableCore } from 'react-draggable';
+import classNames from 'classnames';
+import throttle from 'lodash/throttle';
+import { scrollToElement } from '../../helpers/scrolling';
+import { translate } from '../../helpers/l10n';
+import type { Issue, FlowLocation } from '../issue/types';
+import type { IndexedIssueLocation } from './helpers/indexing';
+
+type Props = {
+ height: number,
+ issue: Issue,
+ onResize: (height: number) => void,
+ onSelectLocation: (flowIndex: number, locationIndex: number) => void,
+ selectedLocation: IndexedIssueLocation | null
+};
+
+type State = {
+ fixed: boolean,
+ locationBlink: boolean
+};
+
+export default class SourceViewerIssueLocations extends React.Component {
+ fixedNode: HTMLElement;
+ locations: { [string]: HTMLElement } = {};
+ node: HTMLElement;
+ props: Props;
+ rootNode: HTMLElement;
+ state: State;
+
+ constructor (props: Props) {
+ super(props);
+ this.state = { fixed: true, locationBlink: false };
+ this.handleScroll = throttle(this.handleScroll, 50);
+ }
+
+ componentDidMount () {
+ this.bindShortcuts();
+ this.listenScroll();
+ }
+
+ componentWillReceiveProps (nextProps: Props) {
+ /* eslint-disable no-console */
+ console.log('foo');
+
+ if (nextProps.selectedLocation !== this.props.selectedLocation) {
+ this.setState({ locationBlink: false });
+ }
+ }
+
+ componentDidUpdate (prevProps: Props) {
+ if (
+ prevProps.selectedLocation !== this.props.selectedLocation &&
+ this.props.selectedLocation != null
+ ) {
+ this.scrollToLocation();
+ }
+ }
+
+ componentWillUnmount () {
+ this.unbindShortcuts();
+ this.unlistenScroll();
+ }
+
+ bindShortcuts () {
+ document.addEventListener('keydown', this.handleKeyPress);
+ }
+
+ unbindShortcuts () {
+ document.removeEventListener('keydown', this.handleKeyPress);
+ }
+
+ listenScroll () {
+ window.addEventListener('scroll', this.handleScroll);
+ }
+
+ unlistenScroll () {
+ window.removeEventListener('scroll', this.handleScroll);
+ }
+
+ blinkLocation = () => {
+ this.setState({ locationBlink: true });
+ setTimeout(() => this.setState({ locationBlink: false }), 1000);
+ };
+
+ handleScroll = () => {
+ const rootNodeTop = this.rootNode.getBoundingClientRect().top;
+ const fixedNodeRect = this.fixedNode.getBoundingClientRect();
+ const fixedNodeTop = fixedNodeRect.top;
+ const fixedNodeBottom = fixedNodeRect.bottom;
+ this.setState((state: State) => {
+ if (state.fixed) {
+ if (rootNodeTop <= fixedNodeTop) {
+ return { fixed: false };
+ }
+ } else if (fixedNodeBottom >= window.innerHeight) {
+ return { fixed: true };
+ }
+ });
+ };
+
+ handleDrag = (e: Event, data: { deltaY: number }) => {
+ let height = this.props.height - data.deltaY;
+ if (height < 100) {
+ height = 100;
+ }
+ if (height > window.innerHeight / 2) {
+ height = window.innerHeight / 2;
+ }
+ this.props.onResize(height);
+ };
+
+ scrollToLocation () {
+ const { selectedLocation } = this.props;
+ if (selectedLocation != null) {
+ const key = `${selectedLocation.flowIndex}-${selectedLocation.locationIndex}`;
+ const locationElement = this.locations[key];
+ if (locationElement) {
+ scrollToElement(locationElement, 15, 15, this.node);
+ }
+ }
+ }
+
+ handleSelectPrev () {
+ const { issue, selectedLocation } = this.props;
+ if (!selectedLocation) {
+ if (issue.flows.length > 0) {
+ // move to the first location of the first flow
+ this.props.onSelectLocation(0, 0);
+ }
+ } else {
+ const currentFlow = issue.flows[selectedLocation.flowIndex];
+ if (
+ currentFlow.locations != null &&
+ currentFlow.locations.length > selectedLocation.locationIndex + 1
+ ) {
+ // move to the next location for the same flow
+ this.props.onSelectLocation(selectedLocation.flowIndex, selectedLocation.locationIndex + 1);
+ } else if (selectedLocation.flowIndex > 0) {
+ // move to the first location of the previous flow
+ this.props.onSelectLocation(selectedLocation.flowIndex - 1, 0);
+ } else {
+ this.blinkLocation();
+ }
+ }
+ }
+
+ handleSelectNext () {
+ const { issue, selectedLocation } = this.props;
+ if (!selectedLocation) {
+ if (issue.flows.length > 0) {
+ // move to the last location of the first flow
+ const firstFlow = issue.flows[0];
+ if (firstFlow.locations != null) {
+ this.props.onSelectLocation(0, firstFlow.locations.length - 1);
+ }
+ }
+ } else if (selectedLocation.locationIndex > 0) {
+ // move to the previous location for the same flow
+ this.props.onSelectLocation(selectedLocation.flowIndex, selectedLocation.locationIndex - 1);
+ } else if (issue.flows.length > selectedLocation.flowIndex + 1) {
+ // move to the last location of the next flow
+ const nextFlow = issue.flows[selectedLocation.flowIndex + 1];
+ if (nextFlow.locations) {
+ this.props.onSelectLocation(selectedLocation.flowIndex + 1, nextFlow.locations.length - 1);
+ }
+ } else {
+ this.blinkLocation();
+ }
+ }
+
+ handleKeyPress = (e: Object) => {
+ const tagName = e.target.tagName.toUpperCase();
+ const shouldHandle = tagName !== 'INPUT' && tagName !== 'TEXTAREA' && tagName !== 'BUTTON';
+
+ if (shouldHandle) {
+ const selectNext = e.keyCode === 40 && e.altKey;
+ const selectPrev = e.keyCode === 38 && e.altKey;
+
+ if (selectNext) {
+ e.preventDefault();
+ this.handleSelectNext();
+ }
+
+ if (selectPrev) {
+ e.preventDefault();
+ this.handleSelectPrev();
+ }
+ }
+ };
+
+ reverseLocations (locations: Array<*>) {
+ return [...locations].reverse();
+ }
+
+ isLocationSelected (flowIndex: number, locationIndex: number) {
+ const { selectedLocation } = this.props;
+ if (selectedLocation == null) {
+ return false;
+ } else {
+ return selectedLocation.flowIndex === flowIndex &&
+ selectedLocation.locationIndex === locationIndex;
+ }
+ }
+
+ handleLocationClick (flowIndex: number, locationIndex: number, e: SyntheticInputEvent) {
+ e.preventDefault();
+ this.props.onSelectLocation(flowIndex, locationIndex);
+ }
+
+ renderLocation = (
+ location: FlowLocation,
+ flowIndex: number,
+ locationIndex: number,
+ locations: Array<*>
+ ) => {
+ const displayIndex = locations.length > 1;
+ const line = location.textRange ? location.textRange.startLine : null;
+ const key = `${flowIndex}-${locationIndex}`;
+ // note that locations order is reversed
+ const selected = this.isLocationSelected(flowIndex, locations.length - locationIndex - 1);
+
+ return (
+ <li key={key} ref={node => this.locations[key] = node} className="spacer-bottom">
+ {line != null && <code className="source-issue-locations-line">L{line}</code>}
+ <a
+ className={classNames('issue-location-message', 'flash', 'flash-heavy', {
+ selected,
+ in: selected && this.state.locationBlink
+ })}
+ href="#"
+ onClick={this.handleLocationClick.bind(
+ this,
+ flowIndex,
+ locations.length - locationIndex - 1
+ )}>
+ {displayIndex && <strong>{locationIndex + 1}: </strong>}
+ {location.msg}
+ </a>
+ </li>
+ );
+ };
+
+ render () {
+ const { flows } = this.props.issue;
+ const { height } = this.props;
+
+ const className = classNames('source-issue-locations-panel', { fixed: this.state.fixed });
+
+ return (
+ <AutoSizer disableHeight={true}>
+ {({ width }) => (
+ <div
+ ref={node => this.rootNode = node}
+ className="source-issue-locations"
+ style={{ width, height }}>
+ <div
+ ref={node => this.fixedNode = node}
+ className={className}
+ style={{ width, height }}>
+ <header className="source-issue-locations-header"/>
+ <div className="source-issue-locations-shortcuts">
+ <span className="shortcut-button">Alt</span>
+ {' + '}
+ <span className="shortcut-button">&uarr;</span>
+ {' '}
+ <span className="shortcut-button">&darr;</span>
+ {' '}
+ {translate('source_viewer.to_navigate_issue_locations')}
+ </div>
+ <ul
+ ref={node => this.node = node}
+ className="source-issue-locations-list"
+ style={{ height: height - 15 }}>
+ {flows.map(
+ (flow, flowIndex) =>
+ flow.locations != null &&
+ this
+ .reverseLocations(flow.locations)
+ .map((location, locationIndex) =>
+ this.renderLocation(
+ location,
+ flowIndex,
+ locationIndex,
+ flow.locations || []
+ ))
+ )}
+ </ul>
+ <DraggableCore axis="y" onDrag={this.handleDrag} offsetParent={document.body}>
+ <div className="workspace-viewer-resize"/>
+ </DraggableCore>
+ </div>
+ </div>
+ )}
+ </AutoSizer>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerLine.js b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerLine.js
index 72cb0d5c053..5a53275c7af 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerLine.js
+++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerLine.js
@@ -26,6 +26,7 @@ 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,
@@ -39,7 +40,7 @@ type Props = {
filtered: boolean | null,
highlighted: boolean,
highlightedSymbol: string | null,
- issueLocations: Array<{ from: number, to: number }>,
+ issueLocations: Array<LinearIssueLocation>,
issues: Array<string>,
line: SourceLine,
loadDuplications: (SourceLine, HTMLElement) => void,
@@ -49,12 +50,13 @@ type Props = {
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
- secondaryIssueLocations: Array<{ from: number, to: number }>,
- // $FlowFixMe
- secondaryIssueLocationMessages: Array<{ msg: string, index?: number }>
+ secondaryIssueLocationMessages: Array<IndexedIssueLocationMessage>,
+ selectedIssueLocation: IndexedIssueLocation | null
};
type State = {
@@ -77,16 +79,7 @@ export default class SourceViewerLine extends React.PureComponent {
this.detachEvents();
}
- componentDidUpdate (prevProps: Props) {
- /* eslint-disable no-console */
- console.log('re-render line', this.props.line.line, 'because they are not equal:');
- Object.keys(this.props).forEach(prop => {
- if (this.props[prop] !== prevProps[prop]) {
- console.log(prop);
- }
- });
- console.log('');
-
+ componentDidUpdate () {
this.attachEvents();
}
@@ -276,21 +269,49 @@ export default class SourceViewerLine extends React.PureComponent {
);
}
- renderSecondaryIssueLocationMessages (locationMessages: Array<{ msg: string, index?: number }>) {
+ 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">
- {locationMessages.map((locationMessage, index) => (
- <div key={index} className="source-viewer-issue-location" title={locationMessage.msg}>
- {locationMessage.index && (
- <strong>{locationMessage.index}: </strong>
- )}
- {limitString(locationMessage.msg)}
- </div>
- ))}
+ {locations.map(this.renderSecondaryIssueLocationMessage)}
</div>
);
}
@@ -312,7 +333,19 @@ export default class SourceViewerLine extends React.PureComponent {
}
if (secondaryIssueLocations) {
- tokens = highlightIssueLocations(tokens, secondaryIssueLocations, 'source-line-code-secondary-issue');
+ 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);
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 0adc3f0d31f..c0ca46bb9d1 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
@@ -19,6 +19,7 @@
*/
// @flow
import escapeHtml from 'escape-html';
+import type { LinearIssueLocation } from './indexing';
type Token = { className: string, text: string };
type Tokens = Array<Token>;
@@ -78,7 +79,7 @@ const part = (str: string, from: number, to: number, acc: number): string => {
*/
export const highlightIssueLocations = (
tokens: Tokens,
- issueLocations: Array<{ from: number, to: number }>,
+ issueLocations: Array<LinearIssueLocation>,
rootClassName: string = ISSUE_LOCATION_CLASS
): Tokens => {
issueLocations.forEach(location => {
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 a9016ef0c7c..dcfe2f273fa 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
@@ -23,6 +23,39 @@ import { getLinearLocations, getIssueLocations } from './issueLocations';
import type { Issue } from '../../issue/types';
import type { SourceLine } from '../types';
+export type LinearIssueLocation = {
+ from: number,
+ line: number,
+ to: number
+};
+
+export type IndexedIssueLocation = {
+ flowIndex: number,
+ from: number,
+ line: number,
+ locationIndex: number,
+ to: number,
+};
+
+export type IndexedIssueLocationMessage = {
+ flowIndex: number,
+ locationIndex: number,
+ msg: string
+};
+
+export type IndexedIssueLocationsByIssueAndLine = {
+ [issueKey: string]: {
+ // $FlowFixMe
+ [lineNumber: number]: Array<IndexedIssueLocation>
+ }
+};
+
+export type IndexedIssueLocationMessagesByIssueAndLine = {
+ [issueKey: string]: {
+ [lineNumber: number]: Array<IndexedIssueLocationMessage>
+ }
+};
+
export const issuesByLine = (issues: Array<Issue>) => {
const index = {};
issues.forEach(issue => {
@@ -35,7 +68,7 @@ export const issuesByLine = (issues: Array<Issue>) => {
return index;
};
-export const locationsByLine = (issues: Array<Issue>) => {
+export const locationsByLine = (issues: Array<Issue>): { [number]: Array<LinearIssueLocation> } => {
const index = {};
issues.forEach(issue => {
getLinearLocations(issue.textRange).forEach(location => {
@@ -48,7 +81,7 @@ export const locationsByLine = (issues: Array<Issue>) => {
return index;
};
-export const locationsByIssueAndLine = (issues: Array<Issue>) => {
+export const locationsByIssueAndLine = (issues: Array<Issue>): IndexedIssueLocationsByIssueAndLine => {
const index = {};
issues.forEach(issue => {
const byLine = {};
@@ -57,7 +90,11 @@ export const locationsByIssueAndLine = (issues: Array<Issue>) => {
if (!(linearLocation.line in byLine)) {
byLine[linearLocation.line] = [];
}
- byLine[linearLocation.line].push({ from: linearLocation.from, to: linearLocation.to });
+ byLine[linearLocation.line].push({
+ ...linearLocation,
+ flowIndex: location.flowIndex,
+ locationIndex: location.locationIndex
+ });
});
});
index[issue.key] = byLine;
@@ -65,7 +102,7 @@ export const locationsByIssueAndLine = (issues: Array<Issue>) => {
return index;
};
-export const locationMessagesByIssueAndLine = (issues: Array<Issue>) => {
+export const locationMessagesByIssueAndLine = (issues: Array<Issue>): IndexedIssueLocationMessagesByIssueAndLine => {
const index = {};
issues.forEach(issue => {
const byLine = {};
@@ -74,7 +111,7 @@ export const locationMessagesByIssueAndLine = (issues: Array<Issue>) => {
if (!(line in byLine)) {
byLine[line] = [];
}
- byLine[line].push({ msg: location.msg, index: location.index });
+ byLine[line].push(location);
});
index[issue.key] = byLine;
});
@@ -117,3 +154,23 @@ export const symbolsByLine = (sources: Array<SourceLine>) => {
});
return index;
};
+
+export const findLocationByIndex = (
+ locations: IndexedIssueLocationsByIssueAndLine,
+ flowIndex: number,
+ locationIndex: number) => {
+ const issueKeys = Object.keys(locations);
+ for (const issueKey of issueKeys) {
+ const lineNumbers = Object.keys(locations[issueKey]);
+ for (let lineIndex = 0; lineIndex < lineNumbers.length; lineIndex++) {
+ for (let i = 0; i < locations[issueKey][lineNumbers[lineIndex]].length; i++) {
+ const location = locations[issueKey][lineNumbers[lineIndex]][i];
+ if (location.flowIndex === flowIndex && location.locationIndex === locationIndex) {
+ return location;
+ }
+ }
+ }
+ }
+
+ return null;
+};
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 d2c8991fc3c..70af97af1a5 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
@@ -36,18 +36,22 @@ export const getLinearLocations = (textRange?: TextRange): Array<{ line: number,
return locations;
};
-export const getIssueLocations = (issue: Issue): Array<{ msg: string, textRange: TextRange, index?: number }> => {
- const primaryLocation = {
- msg: issue.message,
- textRange: issue.textRange
- };
- const allLocations = [primaryLocation];
- issue.flows.forEach(({ locations }) => {
+export const getIssueLocations = (issue: Issue): Array<{
+ msg: string,
+ flowIndex: number,
+ locationIndex: number,
+ textRange?: TextRange,
+ index?: number
+}> => {
+ const allLocations = [];
+ issue.flows.forEach(({ locations }, flowIndex) => {
if (locations) {
const locationsCount = locations.length;
locations.forEach((location, index) => {
const flowLocation = {
...location,
+ flowIndex,
+ locationIndex: index,
// set index only for real flows, do not set for just secondary locations
index: locationsCount > 1 ? locationsCount - index : undefined
};
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/styles.css b/server/sonar-web/src/main/js/components/SourceViewer/styles.css
new file mode 100644
index 00000000000..6371f9e8cb3
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/SourceViewer/styles.css
@@ -0,0 +1,92 @@
+.source-issue-locations {
+ position: relative;
+}
+
+.source-issue-locations-panel {
+ background-color: #fff;
+ box-shadow: 0 -6px 12px rgba(0, 0, 0, .175);
+}
+
+.source-issue-locations-panel.fixed {
+ position: fixed;
+ bottom: 0;
+ margin-left: -1px;
+ border-left: 1px solid #e6e6e6;
+ border-right: 1px solid #e6e6e6;
+}
+
+.source-issue-locations-header {
+ height: 15px;
+ padding: 0 15px;
+ box-sizing: border-box;
+ background-color: #404040;
+ color: #fff;
+}
+
+.source-issue-locations-shortcuts {
+ position: absolute;
+ top: 18px;
+ right: 18px;
+ padding: 6px;
+ background-color: #fff;
+ color: #777;
+ font-size: 11px;
+}
+
+.source-issue-locations-list {
+ height: 185px;
+ padding: 15px;
+ box-sizing: border-box;
+ overflow: auto;
+}
+
+.source-issue-locations-line {
+ display: inline-block;
+ min-width: 25px;
+ margin-right: 15px;
+ color: #777;
+ font-size: 12px;
+ text-align: right;
+}
+
+.issue-location,
+.issue-location-message {
+ display: inline-block;
+ vertical-align: top;
+ line-height: 16px;
+ height: 17px;
+ border: 1px solid #ffeaea;
+ box-sizing: border-box;
+ background-color: #ffeaea;
+}
+
+.issue-location {
+ /* nothing so far */
+}
+
+.issue-location.highlighted {
+ border-color: #e1e1f2;
+ background-color: #e1e1f2;
+}
+
+.issue-location.selected {
+ border-color: #f4b1b0;
+ background-color: #f4b1b0;
+}
+
+.issue-location-message {
+ padding: 0 10px;
+ color: #444 !important;
+ font-size: 12px;
+ white-space: nowrap;
+ transition: all 0.3s ease;
+}
+
+.issue-location-message:hover {
+ border-color: #f4b1b0;
+ background-color: #f4b1b0;
+}
+
+.issue-location-message.selected {
+ border-color: #dd4040;
+} \ No newline at end of file
diff --git a/server/sonar-web/src/main/js/components/issue/Issue.js b/server/sonar-web/src/main/js/components/issue/Issue.js
index c437b8f41af..ce7781caabb 100644
--- a/server/sonar-web/src/main/js/components/issue/Issue.js
+++ b/server/sonar-web/src/main/js/components/issue/Issue.js
@@ -42,6 +42,10 @@ class Issue extends React.PureComponent {
node: HTMLElement;
props: Props;
+ static defaultProps = {
+ selected: false
+ };
+
componentDidMount () {
this.renderIssueView();
if (this.props.selected) {
diff --git a/server/sonar-web/src/main/js/components/issue/types.js b/server/sonar-web/src/main/js/components/issue/types.js
index dd0bbc1d2e6..9d3982f8f28 100644
--- a/server/sonar-web/src/main/js/components/issue/types.js
+++ b/server/sonar-web/src/main/js/components/issue/types.js
@@ -25,13 +25,15 @@ export type TextRange = {
endOffset: number
};
+export type FlowLocation = {
+ msg: string,
+ textRange?: TextRange
+};
+
export type Issue = {
key: string,
flows: Array<{
- locations?: Array<{
- msg: string,
- textRange?: TextRange
- }>
+ locations?: Array<FlowLocation>
}>,
line?: number,
message: string,
diff --git a/server/sonar-web/src/main/js/components/issue/views/changelog-view.js b/server/sonar-web/src/main/js/components/issue/views/changelog-view.js
index 95c7c6da664..f57d968f14d 100644
--- a/server/sonar-web/src/main/js/components/issue/views/changelog-view.js
+++ b/server/sonar-web/src/main/js/components/issue/views/changelog-view.js
@@ -34,4 +34,3 @@ export default PopupView.extend({
};
}
});
-
diff --git a/server/sonar-web/src/main/js/helpers/issues.js b/server/sonar-web/src/main/js/helpers/issues.js
index 3a1e509f790..6410fe3e25c 100644
--- a/server/sonar-web/src/main/js/helpers/issues.js
+++ b/server/sonar-web/src/main/js/helpers/issues.js
@@ -41,6 +41,13 @@ type RawIssue = {
author: string,
comments?: Array<Comment>,
component: string,
+ flows: Array<{
+ locations: Array<{
+ msg: string,
+ textRange: TextRange
+ }>
+ }>,
+ key: string,
line?: number,
project: string,
rule: string,
diff --git a/server/sonar-web/src/main/js/helpers/scrolling.js b/server/sonar-web/src/main/js/helpers/scrolling.js
new file mode 100644
index 00000000000..e456eb3b340
--- /dev/null
+++ b/server/sonar-web/src/main/js/helpers/scrolling.js
@@ -0,0 +1,83 @@
+/*
+ * 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 debounce from 'lodash/debounce';
+
+const SCROLLING_DURATION = 100;
+const SCROLLING_INTERVAL = 10;
+const SCROLLING_STEPS = SCROLLING_DURATION / SCROLLING_INTERVAL;
+
+const getScrollPosition = (element: HTMLElement): number => {
+ return element === window ? window.scrollY : element.scrollTop;
+};
+
+const scrollElement = (element: HTMLElement, position: number) => {
+ if (element === window) {
+ window.scrollTo(0, position);
+ } else {
+ element.scrollTop = position;
+ }
+};
+
+let smoothScrollTop = (y: number, parent) => {
+ const scrollTop = getScrollPosition(parent);
+ const scrollingDown = y > scrollTop;
+ const step = Math.ceil(Math.abs(y - scrollTop) / SCROLLING_STEPS);
+ let stepsDone = 0;
+
+ const interval = setInterval(() => {
+ const scrollTop = getScrollPosition(parent);
+ if (scrollTop === y || SCROLLING_STEPS === stepsDone) {
+ clearInterval(interval);
+ } else {
+ let goal;
+ if (scrollingDown) {
+ goal = Math.min(y, scrollTop + step);
+ } else {
+ goal = Math.max(y, scrollTop - step);
+ }
+ stepsDone++;
+ scrollElement(parent, goal);
+ }
+ }, SCROLLING_INTERVAL);
+};
+
+smoothScrollTop = debounce(smoothScrollTop, SCROLLING_DURATION, { leading: true });
+
+export const scrollToElement = (
+ element: HTMLElement,
+ topOffset: number = 0,
+ bottomOffset: number = 0,
+ parent: HTMLElement = window
+) => {
+ const { top, bottom } = element.getBoundingClientRect();
+ const scrollTop = getScrollPosition(parent);
+ const height: number = parent === window ? window.innerHeight : parent.getBoundingClientRect().height;
+
+ const parentTop = parent === window ? 0 : parent.getBoundingClientRect().top;
+
+ if (top - parentTop < topOffset) {
+ smoothScrollTop(scrollTop - topOffset + top - parentTop, parent);
+ }
+
+ if (bottom - parentTop > height - bottomOffset) {
+ smoothScrollTop(scrollTop + bottom - parentTop - height + bottomOffset, parent);
+ }
+};
diff --git a/server/sonar-web/src/main/less/components/source.less b/server/sonar-web/src/main/less/components/source.less
index 9a89d87959a..07485b239d2 100644
--- a/server/sonar-web/src/main/less/components/source.less
+++ b/server/sonar-web/src/main/less/components/source.less
@@ -127,15 +127,6 @@
background-position: bottom;
}
-.source-line-code-secondary-issue {
- display: inline-block;
- background-color: @issueBackgroundColor;
-
- &.highlighted {
- background-color: mix(#B3D4FF, @issueBackgroundColor, 40%);
- }
-}
-
.source-meta {
vertical-align: top;
width: 1px;
@@ -461,14 +452,8 @@
}
.source-viewer-issue-location {
- float: right;
max-width: 200px;
- height: @source-line-height - 1px;
- line-height: @source-line-height - 1px;
margin-right: 10px;
- padding: 0 10px;
- background-color: #ffeaea;
- font-size: 12px;
.text-ellipsis;
}
@@ -479,7 +464,6 @@
.source-line-issue-locations {
float: right;
margin-right: -10px;
- padding-bottom: 1px;
&:empty {
display: none;
diff --git a/server/sonar-web/src/main/less/components/ui.less b/server/sonar-web/src/main/less/components/ui.less
index a0a852de9ad..357e37ebb9f 100644
--- a/server/sonar-web/src/main/less/components/ui.less
+++ b/server/sonar-web/src/main/less/components/ui.less
@@ -95,7 +95,7 @@
display: inline-block;
min-width: 24px;
height: 24px;
- line-height: 24px;
+ line-height: 21px;
padding: 0 4px;
box-sizing: border-box;
border: 1px solid #ccc;
@@ -103,8 +103,7 @@
background-image: linear-gradient(to bottom, #f5f5f5, #eee);
box-shadow: inset 0 1px 0 #fff, 0 1px 0 #ccc;
color: @secondFontColor;
- font-size: @baseFontSize;
- font-weight: 600;
+ font-size: 11px;
text-align: center;
}
diff --git a/server/sonar-web/src/main/less/pages/issues.less b/server/sonar-web/src/main/less/pages/issues.less
index 98bf375c2ea..3c25afc7d90 100644
--- a/server/sonar-web/src/main/less/pages/issues.less
+++ b/server/sonar-web/src/main/less/pages/issues.less
@@ -59,7 +59,7 @@
.issues-workspace-component-viewer {
display: none;
- padding: 1px 10px;
+ padding: 1px 10px 10px;
min-height: 100vh;
.code-issue-modern {
diff --git a/server/sonar-web/yarn.lock b/server/sonar-web/yarn.lock
index 16f6f693dc4..45ba783706e 100644
--- a/server/sonar-web/yarn.lock
+++ b/server/sonar-web/yarn.lock
@@ -1114,7 +1114,7 @@ classnames@2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.0.tgz#8f61df81f356c45d18a31d83fde4dfb194ea8722"
-classnames@^2.2.4:
+classnames@^2.2.3, classnames@^2.2.4, classnames@^2.2.5:
version "2.2.5"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.5.tgz#fb3801d453467649ef3603c7d61a02bd129bde6d"
@@ -1604,6 +1604,10 @@ dom-converter@~0.1:
dependencies:
utila "~0.3"
+"dom-helpers@^2.4.0 || ^3.0.0":
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.2.1.tgz#3203e07fed217bd1f424b019735582fc37b2825a"
+
dom-serializer@0, dom-serializer@~0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82"
@@ -2117,7 +2121,7 @@ fbjs@0.1.0-alpha.10:
promise "^7.0.3"
whatwg-fetch "^0.9.0"
-fbjs@^0.8.4:
+fbjs@^0.8.1, fbjs@^0.8.4:
version "0.8.8"
resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.8.tgz#02f1b6e0ea0d46c24e0b51a2d24df069563a5ad6"
dependencies:
@@ -3571,7 +3575,7 @@ longest@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097"
-loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0:
+loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.0.tgz#6b26248c42f6d4fa4b0d8542f78edfcde35642a8"
dependencies:
@@ -4526,13 +4530,19 @@ rc@~1.1.6:
minimist "^1.2.0"
strip-json-comments "~1.0.4"
-react-addons-shallow-compare@15.3.2:
- version "15.3.2"
- resolved "https://registry.yarnpkg.com/react-addons-shallow-compare/-/react-addons-shallow-compare-15.3.2.tgz#c9edba49b9eab44d0c59024d289beb1ab97318b5"
+react-addons-shallow-compare@15.4.2:
+ version "15.4.2"
+ resolved "https://registry.yarnpkg.com/react-addons-shallow-compare/-/react-addons-shallow-compare-15.4.2.tgz#027ffd9720e3a1e0b328dcd8fc62e214a0d174a5"
+ dependencies:
+ fbjs "^0.8.4"
+ object-assign "^4.1.0"
-react-addons-test-utils@15.3.2:
- version "15.3.2"
- resolved "https://registry.yarnpkg.com/react-addons-test-utils/-/react-addons-test-utils-15.3.2.tgz#c09a44f583425a4a9c1b38444d7a6c3e6f0f41f6"
+react-addons-test-utils@15.4.2:
+ version "15.4.2"
+ resolved "https://registry.yarnpkg.com/react-addons-test-utils/-/react-addons-test-utils-15.4.2.tgz#93bcaa718fcae7360d42e8fb1c09756cc36302a2"
+ dependencies:
+ fbjs "^0.8.4"
+ object-assign "^4.1.0"
react-deep-force-update@^1.0.0:
version "1.0.1"
@@ -4550,9 +4560,19 @@ react-dev-utils@0.2.1:
sockjs-client "1.0.3"
strip-ansi "3.0.1"
-react-dom@15.3.2:
- version "15.3.2"
- resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.3.2.tgz#c46b0aa5380d7b838e7a59c4a7beff2ed315531f"
+react-dom@15.4.2:
+ version "15.4.2"
+ resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.4.2.tgz#015363f05b0a1fd52ae9efdd3a0060d90695208f"
+ dependencies:
+ fbjs "^0.8.1"
+ loose-envify "^1.1.0"
+ object-assign "^4.1.0"
+
+react-draggable@2.2.3:
+ version "2.2.3"
+ resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-2.2.3.tgz#17628cb8aaefed639d38e0021b978a685d80b08b"
+ dependencies:
+ classnames "^2.2.5"
react-helmet@3.1.0:
version "3.1.0"
@@ -4626,9 +4646,18 @@ react-transform-hmr@1.0.4:
global "^4.3.0"
react-proxy "^1.1.7"
-react@15.3.2:
- version "15.3.2"
- resolved "https://registry.yarnpkg.com/react/-/react-15.3.2.tgz#a7bccd2fee8af126b0317e222c28d1d54528d09e"
+react-virtualized@^9.1.0:
+ version "9.1.0"
+ resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.1.0.tgz#ac022281860f832ffecaf7863c099813681be521"
+ dependencies:
+ babel-runtime "^6.11.6"
+ classnames "^2.2.3"
+ dom-helpers "^2.4.0 || ^3.0.0"
+ loose-envify "^1.3.0"
+
+react@15.4.2:
+ version "15.4.2"
+ resolved "https://registry.yarnpkg.com/react/-/react-15.4.2.tgz#41f7991b26185392ba9bae96c8889e7e018397ef"
dependencies:
fbjs "^0.8.4"
loose-envify "^1.1.0"