diff options
author | Stas Vilchik <vilchiks@gmail.com> | 2017-03-03 11:47:55 +0100 |
---|---|---|
committer | Stas Vilchik <vilchiks@gmail.com> | 2017-03-03 11:47:55 +0100 |
commit | b6baff8775a45fd057ed9f15ba9ac29633261fea (patch) | |
tree | 1f3f8e2ae50ab60ff56e8afb207c836fad62566a | |
parent | 313ffad34e614acb9379fe1e0a0879a49b05be52 (diff) | |
download | sonarqube-b6baff8775a45fd057ed9f15ba9ac29633261fea.tar.gz sonarqube-b6baff8775a45fd057ed9f15ba9ac29633261fea.zip |
Revert "refactor source viewer (#1705)"
This reverts commit ce9f0892fc3d15638c4eaa4054ed06f3d7e5fc19.
71 files changed, 509 insertions, 2811 deletions
diff --git a/server/sonar-server/src/main/java/org/sonar/server/component/ws/AppAction.java b/server/sonar-server/src/main/java/org/sonar/server/component/ws/AppAction.java index fa07c2e8920..73fc5366f6e 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/component/ws/AppAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/component/ws/AppAction.java @@ -44,17 +44,14 @@ import org.sonar.db.metric.MetricDto; import org.sonar.db.property.PropertyDto; import org.sonar.db.property.PropertyQuery; import org.sonar.server.component.ComponentFinder; -import org.sonar.server.component.ComponentFinder.ParamNames; import org.sonar.server.user.UserSession; import static com.google.common.collect.Lists.newArrayList; import static org.sonar.core.util.Uuids.UUID_EXAMPLE_01; -import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001; public class AppAction implements RequestHandler { private static final String PARAM_COMPONENT_ID = "componentId"; - private static final String PARAM_COMPONENT = "component"; private static final String PARAM_PERIOD = "period"; static final List<String> METRIC_KEYS = newArrayList(CoreMetrics.LINES_KEY, CoreMetrics.VIOLATIONS_KEY, CoreMetrics.COVERAGE_KEY, CoreMetrics.DUPLICATED_LINES_DENSITY_KEY, CoreMetrics.TESTS_KEY, @@ -82,40 +79,31 @@ public class AppAction implements RequestHandler { action .createParam(PARAM_COMPONENT_ID) + .setRequired(true) .setDescription("Component ID") - .setDeprecatedSince("6.4") .setDeprecatedKey("uuid", "6.4") .setExampleValue(UUID_EXAMPLE_01); - action.createParam(PARAM_COMPONENT) - .setDescription("Component key") - .setExampleValue(KEY_PROJECT_EXAMPLE_001) - .setSince("6.4"); - action .createParam(PARAM_PERIOD) .setDescription("User leak Period in order to get differential measures") - .setDeprecatedSince("6.4") .setPossibleValues(1); } @Override public void handle(Request request, Response response) { - try (DbSession session = dbClient.openSession(false)) { - ComponentDto component = componentFinder.getByUuidOrKey(session, - request.param(PARAM_COMPONENT_ID), - request.param(PARAM_COMPONENT), - ParamNames.COMPONENT_ID_AND_COMPONENT); + try (DbSession session = dbClient.openSession(false); + JsonWriter json = response.newJsonWriter()) { + json.beginObject(); + String componentUuid = request.mandatoryParam(PARAM_COMPONENT_ID); + ComponentDto component = componentFinder.getByUuid(session, componentUuid); userSession.checkComponentPermission(UserRole.USER, component); - JsonWriter json = response.newJsonWriter(); - json.beginObject(); Map<String, MeasureDto> measuresByMetricKey = measuresByMetricKey(component, session); appendComponent(json, component, userSession, session); appendPermissions(json, component, userSession); appendMeasures(json, measuresByMetricKey); json.endObject(); - json.close(); } } @@ -150,7 +138,7 @@ public class AppAction implements RequestHandler { private static void appendPermissions(JsonWriter json, ComponentDto component, UserSession userSession) { boolean hasBrowsePermission = userSession.hasComponentPermission(UserRole.USER, component); - json.prop("canMarkAsFavorite", userSession.isLoggedIn() && hasBrowsePermission); + json.prop("canMarkAsFavourite", userSession.isLoggedIn() && hasBrowsePermission); } private static void appendMeasures(JsonWriter json, Map<String, MeasureDto> measuresByMetricKey) { diff --git a/server/sonar-server/src/main/resources/org/sonar/server/component/ws/app-example.json b/server/sonar-server/src/main/resources/org/sonar/server/component/ws/app-example.json index d06748ba5e9..8863fa6535d 100644 --- a/server/sonar-server/src/main/resources/org/sonar/server/component/ws/app-example.json +++ b/server/sonar-server/src/main/resources/org/sonar/server/component/ws/app-example.json @@ -7,7 +7,7 @@ "project": "com.sonarsource:java-markdown", "projectName": "Java Markdown", "fav": false, - "canMarkAsFavorite": true, + "canMarkAsFavourite": true, "canCreateManualIssue": true, "measures": { "lines": "786", diff --git a/server/sonar-server/src/test/java/org/sonar/server/component/ws/ComponentsWsTest.java b/server/sonar-server/src/test/java/org/sonar/server/component/ws/ComponentsWsTest.java index b1d32d8d8cf..ce42f92b41e 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/component/ws/ComponentsWsTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/component/ws/ComponentsWsTest.java @@ -83,6 +83,6 @@ public class ComponentsWsTest { assertThat(action.isInternal()).isTrue(); assertThat(action.isPost()).isFalse(); assertThat(action.handler()).isNotNull(); - assertThat(action.params()).hasSize(3); + assertThat(action.params()).hasSize(2); } } diff --git a/server/sonar-server/src/test/resources/org/sonar/server/component/ws/AppActionTest/app.json b/server/sonar-server/src/test/resources/org/sonar/server/component/ws/AppActionTest/app.json index e3554e20775..f7f2947c189 100644 --- a/server/sonar-server/src/test/resources/org/sonar/server/component/ws/AppActionTest/app.json +++ b/server/sonar-server/src/test/resources/org/sonar/server/component/ws/AppActionTest/app.json @@ -10,6 +10,6 @@ "project": "org.sonarsource.sonarqube:sonarqube", "projectName": "SonarQube", "fav": false, - "canMarkAsFavorite": true, + "canMarkAsFavourite": true, "measures": {} } diff --git a/server/sonar-server/src/test/resources/org/sonar/server/component/ws/AppActionTest/app_with_measures.json b/server/sonar-server/src/test/resources/org/sonar/server/component/ws/AppActionTest/app_with_measures.json index 64548fda18f..f50715b4b2c 100644 --- a/server/sonar-server/src/test/resources/org/sonar/server/component/ws/AppActionTest/app_with_measures.json +++ b/server/sonar-server/src/test/resources/org/sonar/server/component/ws/AppActionTest/app_with_measures.json @@ -10,7 +10,7 @@ "project": "org.sonarsource.sonarqube:sonarqube", "projectName": "SonarQube", "fav": false, - "canMarkAsFavorite": true, + "canMarkAsFavourite": true, "measures": { "lines": "200.0", "coverage": "95.4", diff --git a/server/sonar-server/src/test/resources/org/sonar/server/component/ws/AppActionTest/app_with_ut_measure.json b/server/sonar-server/src/test/resources/org/sonar/server/component/ws/AppActionTest/app_with_ut_measure.json index f906f7dccbf..59b6db6325a 100644 --- a/server/sonar-server/src/test/resources/org/sonar/server/component/ws/AppActionTest/app_with_ut_measure.json +++ b/server/sonar-server/src/test/resources/org/sonar/server/component/ws/AppActionTest/app_with_ut_measure.json @@ -10,7 +10,7 @@ "project": "org.sonarsource.sonarqube:sonarqube", "projectName": "SonarQube", "fav": false, - "canMarkAsFavorite": true, + "canMarkAsFavourite": true, "measures": { "coverage": "95.4" } diff --git a/server/sonar-web/.eslintrc b/server/sonar-web/.eslintrc index be74c6cf9b4..6632e18d8e4 100644 --- a/server/sonar-web/.eslintrc +++ b/server/sonar-web/.eslintrc @@ -13,8 +13,7 @@ "globals": { "key": true, "d3": true, - "baseUrl": true, - "SyntheticInputEvent": true + "baseUrl": true }, "parser": "babel-eslint", diff --git a/server/sonar-web/src/main/js/api/components.js b/server/sonar-web/src/main/js/api/components.js index c09192522e3..07ca4204d0b 100644 --- a/server/sonar-web/src/main/js/api/components.js +++ b/server/sonar-web/src/main/js/api/components.js @@ -140,26 +140,3 @@ export function bulkChangeKey (project: string, from: string, to: string, dryRun export const getSuggestions = (query: string): Promise<Object> => ( getJSON('/api/components/suggestions', { s: query }) ); - -export const getComponentForSourceViewer = (component: string): Promise<*> => ( - getJSON('/api/components/app', { component }) -); - -export const getSources = (component: string, from?: number, to?: number): Promise<Array<*>> => { - const data: Object = { key: component }; - if (from) { - Object.assign(data, { from }); - } - if (to) { - Object.assign(data, { to }); - } - return getJSON('/api/sources/lines', data).then(r => r.sources); -}; - -export const getDuplications = (component: string): Promise<*> => ( - getJSON('/api/duplications/show', { key: component }) -); - -export const getTests = (component: string, line: number | string): Promise<*> => ( - getJSON('/api/tests/list', { sourceFileKey: component, sourceFileLineNumber: line }).then(r => r.tests) -); diff --git a/server/sonar-web/src/main/js/api/issues.js b/server/sonar-web/src/main/js/api/issues.js index 75cbdec6e2d..adcb3a1626b 100644 --- a/server/sonar-web/src/main/js/api/issues.js +++ b/server/sonar-web/src/main/js/api/issues.js @@ -20,21 +20,7 @@ // @flow import { getJSON, post } from '../helpers/request'; -type IssuesResponse = { - components?: Array<*>, - debtTotal?: number, - facets: Array<*>, - issues: Array<*>, - paging: { - pageIndex: number, - pageSize: number, - total: number - }, - rules?: Array<*>, - users?: Array<*> -}; - -export const searchIssues = (query: {}): Promise<IssuesResponse> => ( +export const searchIssues = (query: {}) => ( getJSON('/api/issues/search', query) ); @@ -66,10 +52,10 @@ export function getTags (query: {}): Promise<*> { export function extractAssignees ( facet: Array<{ val: string }>, - response: IssuesResponse + response: { users: Array<{ login: string }> } ) { return facet.map(item => { - const user = response.users ? response.users.find(user => user.login = item.val) : null; + const user = response.users.find(user => user.login = item.val); return { ...item, user }; }); } @@ -81,7 +67,7 @@ export function getAssignees (query: {}): Promise<*> { export function getIssuesCount (query: {}): Promise<*> { const data = { ...query, ps: 1, facetMode: 'effort' }; return searchIssues(data).then(r => { - return { issues: r.paging.total, debt: r.debtTotal }; + return { issues: r.total, debt: r.debtTotal }; }); } 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..d4a107033d1 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/source-viewer/SourceViewer'; import Search from './Search'; import ListFooter from '../../../components/controls/ListFooter'; import { retrieveComponentChildren, retrieveComponent, loadMoreChildren, parseError } from '../utils'; @@ -203,7 +203,7 @@ class App extends React.Component { {shouldShowSourceViewer && ( <div className="spacer-top"> - <SourceViewer component={sourceViewer.key}/> + <SourceViewer component={sourceViewer}/> </div> )} </div> diff --git a/server/sonar-web/src/main/js/apps/code/components/ComponentPin.js b/server/sonar-web/src/main/js/apps/code/components/ComponentPin.js index 20670ac9bcb..33adce19b8a 100644 --- a/server/sonar-web/src/main/js/apps/code/components/ComponentPin.js +++ b/server/sonar-web/src/main/js/apps/code/components/ComponentPin.js @@ -25,7 +25,7 @@ import { translate } from '../../../helpers/l10n'; const ComponentPin = ({ component }) => { const handleClick = e => { e.preventDefault(); - Workspace.openComponent({ key: component.key }); + Workspace.openComponent({ uuid: component.id }); }; return ( diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/bubbleChart/BubbleChart.js b/server/sonar-web/src/main/js/apps/component-measures/components/bubbleChart/BubbleChart.js index 4cd674b7c1f..d8ad4c31afc 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/bubbleChart/BubbleChart.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/bubbleChart/BubbleChart.js @@ -118,7 +118,7 @@ export default class BubbleChart extends React.Component { handleBubbleClick (component) { if (['FIL', 'UTS'].includes(component.qualifier)) { - Workspace.openComponent({ key: component.key }); + Workspace.openComponent({ uuid: component.id }); } else { window.location = getComponentUrl(component.refKey || component.key); } 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..149582321dc 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 @@ -19,11 +19,10 @@ */ import React from 'react'; import classNames from 'classnames'; -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/source-viewer/SourceViewer'; import ListFooter from '../../../../components/controls/ListFooter'; export default class ListView extends React.Component { @@ -105,16 +104,6 @@ export default class ListView extends React.Component { } const selectedIndex = components.indexOf(selected); const sourceViewerPeriod = metric.key.indexOf('new_') === 0 && !!leakPeriod ? leakPeriod : null; - const sourceViewerPeriodDate = sourceViewerPeriod != null ? moment(sourceViewerPeriod.date).toDate() : null; - - const filterLine = sourceViewerPeriodDate != null ? line => { - if (line.scmDate) { - const scmDate = moment(line.scmDate).toDate(); - return scmDate >= sourceViewerPeriodDate; - } else { - return false; - } - } : undefined; return ( <div ref="container" className="measure-details-plain-list"> @@ -151,8 +140,8 @@ export default class ListView extends React.Component { {!!selected && ( <div className="measure-details-viewer"> <SourceViewer - component={selected.key} - filterLine={filterLine}/> + component={selected} + period={sourceViewerPeriod}/> </div> )} </div> 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..554a2904227 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 @@ -18,11 +18,10 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import React from 'react'; -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/source-viewer/SourceViewer'; import ListFooter from '../../../../components/controls/ListFooter'; export default class TreeView extends React.Component { @@ -98,16 +97,6 @@ export default class TreeView extends React.Component { const selectedIndex = components.indexOf(selected); const sourceViewerPeriod = metric.key.indexOf('new_') === 0 && !!leakPeriod ? leakPeriod : null; - const sourceViewerPeriodDate = sourceViewerPeriod != null ? moment(sourceViewerPeriod.date).toDate() : null; - - const filterLine = sourceViewerPeriodDate != null ? line => { - if (line.scmDate) { - const scmDate = moment(line.scmDate).toDate(); - return scmDate >= sourceViewerPeriodDate; - } else { - return false; - } - } : undefined; return ( <div ref="container" className="measure-details-plain-list"> @@ -144,8 +133,8 @@ export default class TreeView extends React.Component { {!!selected && ( <div className="measure-details-viewer"> <SourceViewer - component={selected.key} - filterLine={filterLine}/> + component={selected} + period={sourceViewerPeriod}/> </div> )} </div> diff --git a/server/sonar-web/src/main/js/apps/component-measures/details/treemap/MeasureTreemap.js b/server/sonar-web/src/main/js/apps/component-measures/details/treemap/MeasureTreemap.js index b599af3cd2b..2255ef5c4c8 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/details/treemap/MeasureTreemap.js +++ b/server/sonar-web/src/main/js/apps/component-measures/details/treemap/MeasureTreemap.js @@ -134,7 +134,7 @@ export default class MeasureTreemap extends React.Component { const isFile = node.qualifier === 'FIL' || node.qualifier === 'UTS'; if (isFile) { - Workspace.openComponent({ key: node.key }); + Workspace.openComponent({ uuid: node.id }); return; } 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..d889e4df77f 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,43 +19,32 @@ */ // @flow import React from 'react'; -import SourceViewer from '../../../components/SourceViewer/StandaloneSourceViewer'; +import SourceViewer from '../../../components/source-viewer/SourceViewer'; +import { getComponentNavigation } from '../../../api/nav'; export default class App extends React.Component { - props: { - location: { - query: { - id: string, - line?: string - } - } - } - - scrollToLine = () => { - const { line } = this.props.location.query; - if (line) { - const row = document.querySelector(`.source-line[data-line-number="${line}"]`); - if (row) { - const rect = row.getBoundingClientRect(); - const topOffset = window.innerHeight / 2 - 60; - const goal = rect.top - topOffset; - window.scrollTo(0, goal); - } - } + static propTypes = { + location: React.PropTypes.object.isRequired }; + state = {}; + + componentDidMount () { + getComponentNavigation(this.props.location.query.id).then(component => ( + this.setState({ component }) + )); + } + render () { - const { id, line } = this.props.location.query; + if (!this.state.component) { + return null; + } - const finalLine = line != null ? Number(line) : null; + const { line } = this.props.location.query; return ( <div className="page"> - <SourceViewer - aroundLine={finalLine} - component={id} - highlightedLine={finalLine} - onLoaded={this.scrollToLine}/> + <SourceViewer component={{ id: this.state.component.id }} line={line}/> </div> ); } diff --git a/server/sonar-web/src/main/js/components/issue/ConnectedIssue.js b/server/sonar-web/src/main/js/apps/issues/component-viewer/issue-view.js index 28be4c7ba4b..95e18194208 100644 --- a/server/sonar-web/src/main/js/components/issue/ConnectedIssue.js +++ b/server/sonar-web/src/main/js/apps/issues/component-viewer/issue-view.js @@ -17,13 +17,19 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -// @flow -import { connect } from 'react-redux'; -import Issue from './Issue'; -import { getIssueByKey } from '../../store/rootReducer'; +import IssueView from '../workspace-list-item-view'; -const mapStateToProps = (state, ownProps) => ({ - issue: getIssueByKey(state, ownProps.issueKey) +export default IssueView.extend({ + onRender () { + IssueView.prototype.onRender.apply(this, arguments); + this.$el.removeClass('issue-navigate-right issue-with-checkbox'); + }, + + serializeData () { + return { + ...IssueView.prototype.serializeData.apply(this, arguments), + showComponent: false + }; + } }); -export default connect(mapStateToProps)(Issue); diff --git a/server/sonar-web/src/main/js/apps/issues/component-viewer/main.js b/server/sonar-web/src/main/js/apps/issues/component-viewer/main.js index 49d7152c45f..0f08c21112a 100644 --- a/server/sonar-web/src/main/js/apps/issues/component-viewer/main.js +++ b/server/sonar-web/src/main/js/apps/issues/component-viewer/main.js @@ -18,114 +18,186 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import $ from 'jquery'; -import React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import Marionette from 'backbone.marionette'; -import SourceViewer from '../../../components/SourceViewer/SourceViewer'; -import WithStore from '../../../components/shared/WithStore'; - -export default Marionette.ItemView.extend({ - template () { - return '<div></div>'; +import SourceViewer from '../../../components/source-viewer/main'; +import IssueView from './issue-view'; + +export default SourceViewer.extend({ + events () { + return { + ...SourceViewer.prototype.events.apply(this, arguments), + 'click .js-close-component-viewer': 'closeComponentViewer', + 'click .code-issue': 'selectIssue' + }; }, initialize (options) { - this.handleLoadIssues = this.handleLoadIssues.bind(this); - this.scrollToBaseIssue = this.scrollToBaseIssue.bind(this); - this.selectIssue = this.selectIssue.bind(this); - this.listenTo(options.app.state, 'change:selectedIndex', this.select); - }, - - onRender () { - this.showViewer(); - }, - - onDestroy () { - this.unbindShortcuts(); - unmountComponentAtNode(this.el); - }, - - handleLoadIssues (component: string) { - // TODO fromLine: number, toLine: number - const issues = this.options.app.list.toJSON().filter(issue => issue.componentKey === component); - return Promise.resolve(issues); + SourceViewer.prototype.initialize.apply(this, arguments); + return this.listenTo(options.app.state, 'change:selectedIndex', this.select); }, - showViewer (onLoaded) { - if (!this.baseIssue) { - return; - } - - const componentKey = this.baseIssue.get('component'); - - render(( - <WithStore> - <SourceViewer - aroundLine={this.baseIssue.get('line')} - component={componentKey} - displayAllIssues={true} - loadIssues={this.handleLoadIssues} - onLoaded={onLoaded} - onIssueSelect={this.selectIssue} - selectedIssue={this.baseIssue.get('key')}/> - </WithStore> - ), this.el); - }, - - openFileByIssue (issue) { - this.baseIssue = issue; - this.selectedIssue = issue.get('key'); - this.showViewer(this.scrollToBaseIssue); + onLoaded () { + SourceViewer.prototype.onLoaded.apply(this, arguments); this.bindShortcuts(); + if (this.baseIssue != null) { + this.baseIssue.trigger('locations', this.baseIssue); + this.scrollToLine(this.baseIssue.get('line')); + } }, bindShortcuts () { + const that = this; + const doAction = function (action) { + const selectedIssueView = that.getSelectedIssueEl(); + if (!selectedIssueView) { + return; + } + selectedIssueView.find('.js-issue-' + action).click(); + }; key('up', 'componentViewer', () => { - this.options.app.controller.selectPrev(); + that.options.app.controller.selectPrev(); return false; }); key('down', 'componentViewer', () => { - this.options.app.controller.selectNext(); + that.options.app.controller.selectNext(); return false; }); key('left,backspace', 'componentViewer', () => { - this.options.app.controller.closeComponentViewer(); + that.options.app.controller.closeComponentViewer(); return false; }); + key('f', 'componentViewer', () => doAction('transition')); + key('a', 'componentViewer', () => doAction('assign')); + key('m', 'componentViewer', () => doAction('assign-to-me')); + key('p', 'componentViewer', () => doAction('plan')); + key('i', 'componentViewer', () => doAction('set-severity')); + key('c', 'componentViewer', () => doAction('comment')); }, unbindShortcuts () { - key.deleteScope('componentViewer'); + return key.deleteScope('componentViewer'); + }, + + onDestroy () { + SourceViewer.prototype.onDestroy.apply(this, arguments); + this.unbindScrollEvents(); + return this.unbindShortcuts(); }, select () { const selected = this.options.app.state.get('selectedIndex'); const selectedIssue = this.options.app.list.at(selected); + if (selectedIssue.get('component') === this.model.get('key')) { + selectedIssue.trigger('locations', selectedIssue); + return this.scrollToIssue(selectedIssue.get('key')); + } else { + this.unbindShortcuts(); + return this.options.app.controller.showComponentViewer(selectedIssue); + } + }, + + getSelectedIssueEl () { + const selected = this.options.app.state.get('selectedIndex'); + if (selected == null) { + return null; + } + const selectedIssue = this.options.app.list.at(selected); + if (selectedIssue == null) { + return null; + } + const selectedIssueView = this.$('#issue-' + (selectedIssue.get('key'))); + if (selectedIssueView.length > 0) { + return selectedIssueView; + } else { + return null; + } + }, + + selectIssue (e) { + const key = $(e.currentTarget).data('issue-key'); + const issue = this.issues.find(model => model.get('key') === key); + const index = this.options.app.list.indexOf(issue); + return this.options.app.state.set({ selectedIndex: index }); + }, + + scrollToIssue (key) { + const el = this.$('#issue-' + key); + if (el.length > 0) { + const line = el.closest('[data-line-number]').data('line-number'); + return this.scrollToLine(line); + } else { + this.unbindShortcuts(); + const selected = this.options.app.state.get('selectedIndex'); + const selectedIssue = this.options.app.list.at(selected); + return this.options.app.controller.showComponentViewer(selectedIssue); + } + }, + + openFileByIssue (issue) { + this.baseIssue = issue; + const componentKey = issue.get('component'); + const componentUuid = issue.get('componentUuid'); + return this.open(componentUuid, componentKey); + }, + + linesLimit () { + let line = this.LINES_LIMIT / 2; + if ((this.baseIssue != null) && this.baseIssue.has('line')) { + line = Math.max(line, this.baseIssue.get('line')); + } + return { + from: line - this.LINES_LIMIT / 2 + 1, + to: line + this.LINES_LIMIT / 2 + }; + }, - if (selectedIssue.get('component') === this.baseIssue.get('component')) { - this.baseIssue = selectedIssue; - this.showViewer(this.scrollToBaseIssue); - this.scrollToBaseIssue(); + limitIssues (issues) { + const that = this; + let index = this.ISSUES_LIMIT / 2; + if ((this.baseIssue != null) && this.baseIssue.has('index')) { + index = Math.max(index, this.baseIssue.get('index')); + } + return issues.filter(issue => Math.abs(issue.get('index') - index) <= that.ISSUES_LIMIT / 2); + }, + + requestIssues () { + const that = this; + let r; + if (this.options.app.list.last().get('component') === this.model.get('key')) { + r = this.options.app.controller.fetchNextPage(); } else { - this.options.app.controller.showComponentViewer(selectedIssue); + r = $.Deferred().resolve().promise(); } + return r.done(() => { + that.issues.reset(that.options.app.list.filter(issue => issue.get('component') === that.model.key())); + that.issues.reset(that.limitIssues(that.issues)); + return that.addIssuesPerLineMeta(that.issues); + }); + }, + + renderIssues () { + this.issues.forEach(this.renderIssue, this); + return this.$('.source-line-issues').addClass('hidden'); + }, + + renderIssue (issue) { + const issueView = new IssueView({ + el: '#issue-' + issue.get('key'), + model: issue, + app: this.options.app + }); + this.issueViews.push(issueView); + return issueView.render(); }, scrollToLine (line) { const row = this.$(`[data-line-number=${line}]`); const topOffset = $(window).height() / 2 - 60; const goal = row.length > 0 ? row.offset().top - topOffset : 0; - $(window).scrollTop(goal); - }, - - selectIssue (issueKey) { - const issue = this.options.app.list.find(model => model.get('key') === issueKey); - const index = this.options.app.list.indexOf(issue); - this.options.app.state.set({ selectedIndex: index }); + return $(window).scrollTop(goal); }, - scrollToBaseIssue () { - this.scrollToLine(this.baseIssue.get('line')); + closeComponentViewer () { + return this.options.app.controller.closeComponentViewer(); } }); 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 71df7acdb7c..edc86827050 100644 --- a/server/sonar-web/src/main/js/apps/issues/controller.js +++ b/server/sonar-web/src/main/js/apps/issues/controller.js @@ -44,7 +44,14 @@ export default Controller.extend({ this.options.app.state.set({ selectedIndex: 0, page: 1 }, { silent: true }); this.closeComponentViewer(); } - const data = this.getQueryAsObject(); + const data = this._issuesParameters(); + Object.assign(data, this.options.app.state.get('query')); + if (this.options.app.state.get('query').assigned_to_me) { + Object.assign(data, { assignees: '__me__' }); + } + if (this.options.app.state.get('isContext')) { + Object.assign(data, this.options.app.state.get('contextQuery')); + } return $.get(window.baseUrl + '/api/issues/search', data).done(r => { const issues = that.options.app.list.parseIssues(r); if (firstPage) { diff --git a/server/sonar-web/src/main/js/apps/issues/templates/issues-issue-checkbox.hbs b/server/sonar-web/src/main/js/apps/issues/templates/issues-issue-checkbox.hbs new file mode 100644 index 00000000000..dbb50e24779 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/templates/issues-issue-checkbox.hbs @@ -0,0 +1,3 @@ +<div class="js-toggle issue-checkbox-container"> + <i class="issue-checkbox icon-checkbox {{#if selected}}icon-checkbox-checked{{/if}}"></i> +</div> diff --git a/server/sonar-web/src/main/js/apps/issues/templates/issues-issue-filter.hbs b/server/sonar-web/src/main/js/apps/issues/templates/issues-issue-filter.hbs new file mode 100644 index 00000000000..16a212ddd60 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/templates/issues-issue-filter.hbs @@ -0,0 +1,6 @@ +<li class="issue-meta"> + <button class="button-link issue-action issue-action-with-options js-issue-filter" + aria-label="{{t "issue.filter_similar_issues"}}"> + <i class="icon-filter icon-half-transparent"></i> <i class="icon-dropdown"></i> + </button> +</li> diff --git a/server/sonar-web/src/main/js/apps/issues/workspace-list-item-view.js b/server/sonar-web/src/main/js/apps/issues/workspace-list-item-view.js index 43817e46586..9c57d181aa9 100644 --- a/server/sonar-web/src/main/js/apps/issues/workspace-list-item-view.js +++ b/server/sonar-web/src/main/js/apps/issues/workspace-list-item-view.js @@ -18,12 +18,10 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import $ from 'jquery'; -import React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import Marionette from 'backbone.marionette'; -import Issue from '../../components/issue/Issue'; +import IssueView from '../../components/issue/issue-view'; import IssueFilterView from './issue-filter-view'; -import WithStore from '../../components/shared/WithStore'; +import CheckboxTemplate from './templates/issues-issue-checkbox.hbs'; +import FilterTemplate from './templates/issues-issue-filter.hbs'; const SHOULD_NULL = { any: ['issues'], @@ -33,43 +31,35 @@ const SHOULD_NULL = { assigned: ['assignees'] }; -export default Marionette.ItemView.extend({ - className: 'issues-workspace-list-item', +export default IssueView.extend({ + checkboxTemplate: CheckboxTemplate, + filterTemplate: FilterTemplate, - initialize (options) { - this.openComponentViewer = this.openComponentViewer.bind(this); - this.onIssueFilterClick = this.onIssueFilterClick.bind(this); - this.onIssueCheck = this.onIssueCheck.bind(this); - this.listenTo(options.app.state, 'change:selectedIndex', this.showIssue); - this.listenTo(this.model, 'change:selected', this.showIssue); + events () { + return { + ...IssueView.prototype.events.apply(this, arguments), + 'click': 'selectCurrent', + 'dblclick': 'openComponentViewer', + 'click .js-issue-navigate': 'openComponentViewer', + 'click .js-issue-filter': 'onIssueFilterClick', + 'click .js-toggle': 'onIssueToggle' + }; }, - template () { - return '<div></div>'; + initialize (options) { + IssueView.prototype.initialize.apply(this, arguments); + this.listenTo(options.app.state, 'change:selectedIndex', this.select); }, onRender () { - this.showIssue(); - }, - - onDestroy () { - unmountComponentAtNode(this.el); - }, - - showIssue () { - const selected = this.model.get('index') === this.options.app.state.get('selectedIndex'); - - render(( - <WithStore> - <Issue - issue={this.model} - checked={this.model.get('selected')} - onCheck={this.onIssueCheck} - onClick={this.openComponentViewer} - onFilterClick={this.onIssueFilterClick} - selected={selected}/> - </WithStore> - ), this.el); + IssueView.prototype.onRender.apply(this, arguments); + this.select(); + this.addFilterSelect(); + this.addCheckbox(); + this.$el.addClass('issue-navigate-right'); + if (this.options.app.state.get('canBulkChange')) { + this.$el.addClass('issue-with-checkbox'); + } }, onIssueFilterClick (e) { @@ -99,21 +89,26 @@ export default Marionette.ItemView.extend({ this.popup.render(); }, - onIssueCheck (e) { + onIssueToggle (e) { e.preventDefault(); - e.stopPropagation(); this.model.set({ selected: !this.model.get('selected') }); const selected = this.model.collection.where({ selected: true }).length; this.options.app.state.set({ selected }); }, - changeSelection () { + addFilterSelect () { + this.$('.issue-table-meta-cell-first') + .find('.issue-meta-list') + .append(this.filterTemplate(this.model.toJSON())); + }, + + addCheckbox () { + this.$el.append(this.checkboxTemplate(this.model.toJSON())); + }, + + select () { const selected = this.model.get('index') === this.options.app.state.get('selectedIndex'); - if (selected) { - this.select(); - } else { - this.unselect(); - } + this.$el.toggleClass('selected', selected); }, selectCurrent () { @@ -142,5 +137,12 @@ export default Marionette.ItemView.extend({ } else { return this.options.app.controller.showComponentViewer(this.model); } + }, + + serializeData () { + return { + ...IssueView.prototype.serializeData.apply(this, arguments), + showComponent: true + }; } }); diff --git a/server/sonar-web/src/main/js/apps/issues/workspace-list-view.js b/server/sonar-web/src/main/js/apps/issues/workspace-list-view.js index 383d3145f24..669f4c139e6 100644 --- a/server/sonar-web/src/main/js/apps/issues/workspace-list-view.js +++ b/server/sonar-web/src/main/js/apps/issues/workspace-list-view.js @@ -37,6 +37,14 @@ export default WorkspaceListView.extend({ bindShortcuts () { const that = this; + const doAction = function (action) { + const selectedIssue = that.collection.at(that.options.app.state.get('selectedIndex')); + if (selectedIssue == null) { + return; + } + const selectedIssueView = that.children.findByModel(selectedIssue); + selectedIssueView.$('.js-issue-' + action).click(); + }; WorkspaceListView.prototype.bindShortcuts.apply(this, arguments); key('right', 'list', () => { const selectedIssue = that.collection.at(that.options.app.state.get('selectedIndex')); @@ -48,12 +56,26 @@ export default WorkspaceListView.extend({ selectedIssue.set({ selected: !selectedIssue.get('selected') }); return false; }); + key('f', 'list', () => doAction('transition')); + key('a', 'list', () => doAction('assign')); + key('m', 'list', () => doAction('assign-to-me')); + key('p', 'list', () => doAction('plan')); + key('i', 'list', () => doAction('set-severity')); + key('c', 'list', () => doAction('comment')); + key('t', 'list', () => doAction('edit-tags')); }, unbindShortcuts () { WorkspaceListView.prototype.unbindShortcuts.apply(this, arguments); key.unbind('right', 'list'); key.unbind('space', 'list'); + key.unbind('f', 'list'); + key.unbind('a', 'list'); + key.unbind('m', 'list'); + key.unbind('p', 'list'); + key.unbind('i', 'list'); + key.unbind('c', 'list'); + key.unbind('t', 'list'); }, scrollTo () { @@ -100,6 +122,7 @@ export default WorkspaceListView.extend({ displayComponent (container, model) { const data = { ...model.toJSON() }; + /* eslint-disable no-console */ const qualifier = this.options.app.state.get('contextComponentQualifier'); if (qualifier === 'VW' || qualifier === 'SVW') { Object.assign(data, { organization: undefined }); 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..84147530647 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 @@ -54,10 +54,10 @@ class App extends React.Component { const { component } = this.props; if (['FIL', 'UTS'].includes(component.qualifier)) { - const SourceViewer = require('../../../components/SourceViewer/StandaloneSourceViewer').default; + const SourceViewer = require('../../../components/source-viewer/SourceViewer').default; return ( <div className="page"> - <SourceViewer component={component.key}/> + <SourceViewer component={component}/> </div> ); } diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.js b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.js deleted file mode 100644 index 2c6e5b594a6..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.js +++ /dev/null @@ -1,47 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -// @flow -import { connect } from 'react-redux'; -import SourceViewerBase from './SourceViewerBase'; -import { receiveFavorites } from '../../store/favorites/duck'; -import { receiveIssues } from '../../store/issues/duck'; - -const mapStateToProps = null; - -const onReceiveComponent = (component: { key: string, canMarkAsFavorite: boolean, fav: boolean }) => dispatch => { - if (component.canMarkAsFavorite) { - const favorites = []; - const notFavorites = []; - if (component.fav) { - favorites.push({ key: component.key }); - } else { - notFavorites.push({ key: component.key }); - } - dispatch(receiveFavorites(favorites, notFavorites)); - } -}; - -const onReceiveIssues = (issues: Array<*>) => dispatch => { - dispatch(receiveIssues(issues)); -}; - -const mapDispatchToProps = { onReceiveComponent, onReceiveIssues }; - -export default connect(mapStateToProps, mapDispatchToProps)(SourceViewerBase); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js deleted file mode 100644 index 2ad750e7120..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js +++ /dev/null @@ -1,499 +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 uniqBy from 'lodash/uniqBy'; -import SourceViewerHeader from './SourceViewerHeader'; -import SourceViewerCode from './SourceViewerCode'; -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'; -import { - issuesByLine, - locationsByLine, - locationsByIssueAndLine, - locationMessagesByIssueAndLine, - duplicationsByLine, - symbolsByLine -} from './helpers/indexing'; -import { getComponentForSourceViewer, getSources, getDuplications, getTests } from '../../api/components'; -import { translate } from '../../helpers/l10n'; -import type { SourceLine } from './types'; -import type { Issue } from '../issue/types'; - -// TODO react-virtualized - -type Props = { - aroundLine?: number, - component: string, - displayAllIssues: boolean, - filterLine?: (line: SourceLine) => boolean, - highlightedLine?: number, - loadComponent: (string) => Promise<*>, - loadIssues: (string, number, number) => Promise<*>, - loadSources: (string, number, number) => Promise<*>, - onLoaded?: (component: Object, sources: Array<*>, issues: Array<*>) => void, - onIssueSelect: (string) => void, - onIssueUnselect: () => void, - onReceiveComponent: ({ canMarkAsFavorite: boolean, fav: boolean, key: string }) => void, - onReceiveIssues: (issues: Array<*>) => void, - selectedIssue: string | null, -}; - -type State = { - component?: Object, - displayDuplications: boolean, - duplications?: Array<{ - blocks: Array<{ - _ref: string, - from: number, - size: number - }> - }>, - duplicationsByLine: { [number]: Array<number> }, - duplicatedFiles?: Array<{ key: string }>, - hasSourcesAfter: boolean, - highlightedLine: number | null, - 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 }> - } - }, - loading: boolean, - loadingSourcesAfter: boolean, - loadingSourcesBefore: boolean, - notAccessible: boolean, - notExist: boolean, - sources?: Array<SourceLine>, - symbolsByLine: { [number]: Array<string> } -}; - -const LINES = 500; - -const loadComponent = (key: string): Promise<*> => { - return getComponentForSourceViewer(key); -}; - -const loadSources = (key: string, from?: number, to?: number): Promise<Array<*>> => { - return getSources(key, from, to); -}; - -export default class SourceViewerBase extends React.Component { - mounted: boolean; - node: HTMLElement; - props: Props; - state: State; - - static defaultProps = { - displayAllIssues: false, - onIssueSelect: () => { }, - onIssueUnselect: () => { }, - loadComponent, - loadIssues, - loadSources - }; - - constructor (props: Props) { - super(props); - this.state = { - displayDuplications: false, - duplicationsByLine: {}, - hasSourcesAfter: false, - highlightedLine: props.highlightedLine || null, - highlightedSymbol: null, - issuesByLine: {}, - issueLocationsByLine: {}, - issueSecondaryLocationsByIssueByLine: {}, - issueSecondaryLocationMessagesByIssueByLine: {}, - loading: true, - loadingSourcesAfter: false, - loadingSourcesBefore: false, - notAccessible: false, - notExist: false, - selectedIssue: props.defaultSelectedIssue || null, - symbolsByLine: {} - }; - } - - componentDidMount () { - this.mounted = true; - this.fetchComponent(); - } - - componentDidUpdate (prevProps: Props) { - 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(); - } - } - - componentWillUnmount () { - this.mounted = false; - } - - computeCoverageStatus (lines: Array<SourceLine>): Array<SourceLine> { - return lines.map(line => ({ ...line, coverageStatus: getCoverageStatus(line) })); - } - - isLineOutsideOfRange (lineNumber: number) { - const { sources } = this.state; - if (sources != null && sources.length > 0) { - const firstLine = sources[0]; - const lastList = sources[sources.length - 1]; - return lineNumber < firstLine.line || lineNumber > lastList.line; - } else { - return true; - } - } - - fetchComponent () { - this.setState({ loading: true }); - - const loadIssues = (component, sources) => { - this.props.loadIssues(this.props.component, 1, LINES).then(issues => { - 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); - } - }); - } - }); - }; - - const onFailLoadComponent = ({ response }) => { - // TODO handle other statuses - if (this.mounted && response.status === 404) { - this.setState({ loading: false, notExist: true }); - } - }; - - const onFailLoadSources = (response, component) => { - // TODO handle other statuses - if (this.mounted) { - if (response.status === 403) { - this.setState({ component, loading: false, notAccessible: true }); - } - } - }; - - const onResolve = component => { - this.props.onReceiveComponent(component); - this.loadSources().then( - sources => loadIssues(component, sources), - response => onFailLoadSources(response, component) - ); - }; - - this.props.loadComponent(this.props.component).then(onResolve, onFailLoadComponent); - } - - fetchSources () { - 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); - } - }); - } - }); - } - - loadSources () { - return new Promise((resolve, reject) => { - const onFailLoadSources = ({ response }) => { - // TODO handle other statuses - if (this.mounted) { - if (response.status === 403) { - reject(response); - } else if (response.status === 404) { - resolve([]); - } - } - }; - - const from = this.props.aroundLine ? Math.max(1, this.props.aroundLine - LINES / 2 + 1) : 1; - // 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 - ); - }); - } - - loadSourcesBefore = () => { - if (!this.state.sources) { - return; - } - const firstSourceLine = this.state.sources[0]; - this.setState({ loadingSourcesBefore: true }); - const from = Math.max(1, firstSourceLine.line - LINES); - this.props.loadSources(this.props.component, from, firstSourceLine.line - 1).then(sources => { - this.props.loadIssues(this.props.component, from, firstSourceLine.line - 1).then(issues => { - this.props.onReceiveIssues(issues); - if (this.mounted) { - this.setState(prevState => ({ - issues: uniqBy([...issues, ...prevState.issues], issue => issue.key), - loadingSourcesBefore: false, - sources: [...this.computeCoverageStatus(sources), ...prevState.sources], - symbolsByLine: { ...prevState.symbolsByLine, ...symbolsByLine(sources) } - })); - } - }); - }); - }; - - loadSourcesAfter = () => { - if (!this.state.sources) { - return; - } - const lastSourceLine = this.state.sources[this.state.sources.length - 1]; - this.setState({ loadingSourcesAfter: true }); - const fromLine = lastSourceLine.line + 1; - // request one additional line to define `hasSourcesAfter` - const toLine = lastSourceLine.line + LINES + 1; - this.props.loadSources(this.props.component, fromLine, toLine).then(sources => { - this.props.loadIssues(this.props.component, fromLine, toLine).then(issues => { - this.props.onReceiveIssues(issues); - if (this.mounted) { - this.setState(prevState => ({ - issues: uniqBy([...prevState.issues, ...issues], issue => issue.key), - hasSourcesAfter: sources.length > LINES, - loadingSourcesAfter: false, - sources: [...prevState.sources, ...this.computeCoverageStatus(sources.slice(0, LINES))], - symbolsByLine: { ...prevState.symbolsByLine, ...symbolsByLine(sources.slice(0, LINES)) } - })); - } - }); - }); - }; - - 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); - } - }); - } - }); - }; - - openNewWindow = () => { - const { component } = this.state; - if (component != null) { - let query = 'id=' + encodeURIComponent(component.key); - const windowParams = 'resizable=1,scrollbars=1,status=1'; - if (this.state.highlightedLine) { - query = query + '&line=' + this.state.highlightedLine; - } - window.open(window.baseUrl + '/component/index?' + query, component.name, windowParams); - } - }; - - showMeasures = () => { - const model = new Source(this.state.component); - const measuresOvervlay = new MeasuresOverlay({ model, large: true }); - measuresOvervlay.render(); - }; - - handleCoverageClick = (line: SourceLine, element: HTMLElement) => { - getTests(this.props.component, line.line).then(tests => { - const popup = new CoveragePopupView({ line, tests, triggerEl: element }); - popup.render(); - }); - }; - - handleDuplicationClick = (index: number, line: number) => { - const duplication = this.state.duplications && this.state.duplications[index]; - let blocks = (duplication && duplication.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 element = this.node.querySelector(`.source-line-duplications-extra[data-line-number="${line}"]`); - if (element) { - const popup = new DuplicationPopupView({ - blocks, - inRemovedComponent, - component: this.state.component, - files: this.state.duplicatedFiles, - triggerEl: element - }); - popup.render(); - } - }; - - displayLinePopup (line: number, element: HTMLElement) { - const popup = new LineActionsPopupView({ - line, - triggerEl: element, - component: this.state.component - }); - popup.render(); - } - - handleLineClick = (line: number, element: HTMLElement) => { - this.setState(prevState => ({ - highlightedLine: prevState.highlightedLine === line ? null : line - })); - this.displayLinePopup(line, element); - }; - - handleSymbolClick = (symbol: string) => { - this.setState(prevState => ({ - highlightedSymbol: prevState.highlightedSymbol === symbol ? null : symbol - })); - }; - - handleSCMClick = (line: SourceLine, element: HTMLElement) => { - const popup = new SCMPopupView({ triggerEl: element, line }); - popup.render(); - }; - - 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> - ); - } - - render () { - const { component, loading } = this.state; - - if (loading) { - return null; - } - - if (this.state.notExist) { - return ( - <div className="alert alert-warning spacer-top">{translate('component_viewer.no_component')}</div> - ); - } - - if (component == null) { - return null; - } - - const className = classNames('source-viewer', { 'source-duplications-expanded': this.state.displayDuplications }); - - return ( - <div className={className} ref={node => this.node = node}> - <SourceViewerHeader - component={this.state.component} - openNewWindow={this.openNewWindow} - showMeasures={this.showMeasures}/> - {this.state.notAccessible && ( - <div className="alert alert-warning spacer-top"> - {translate('code_viewer.no_source_code_displayed_due_to_security')} - </div> - )} - {this.state.sources != null && this.renderCode(this.state.sources)} - </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 deleted file mode 100644 index 32092dd47c5..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.js +++ /dev/null @@ -1,222 +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 SourceViewerLine from './SourceViewerLine'; -import { translate } from '../../helpers/l10n'; -import type { Duplication, SourceLine } from './types'; -import type { Issue } from '../issue/types'; - -const EMPTY_ARRAY = []; - -const ZERO_LINE = { - code: '', - duplicated: false, - line: 0 -}; - -export default class SourceViewerCode extends React.Component { - props: { - displayAllIssues: boolean, - duplications?: Array<Duplication>, - duplicationsByLine: { [number]: Array<number> }, - duplicatedFiles?: Array<{ key: string }>, - filterLine?: (SourceLine) => boolean, - hasSourcesAfter: boolean, - hasSourcesBefore: boolean, - highlightedLine: number | null, - 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 }> - } - }, - loadDuplications: (SourceLine, HTMLElement) => void, - loadSourcesAfter: () => void, - loadSourcesBefore: () => void, - loadingSourcesAfter: boolean, - loadingSourcesBefore: boolean, - onCoverageClick: (SourceLine, HTMLElement) => void, - onDuplicationClick: (number, number) => void, - onIssueSelect: (string) => void, - onIssueUnselect: () => void, - onLineClick: (number, HTMLElement) => void, - onSCMClick: (SourceLine, HTMLElement) => void, - onSymbolClick: (string) => void, - selectedIssue: string | 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; - } - - getIssuesForLine (line: SourceLine): Array<string> { - return this.props.issuesByLine[line.line] || EMPTY_ARRAY; - } - - getIssueLocationsForLine (line: SourceLine) { - return this.props.issueLocationsByLine[line.line] || EMPTY_ARRAY; - } - - getSecondaryIssueLocationsForLine (line: SourceLine, issueKey: string) { - const index = this.props.issueSecondaryLocationsByIssueByLine; - if (index[issueKey] == null) { - return EMPTY_ARRAY; - } - return index[issueKey][line.line] || EMPTY_ARRAY; - } - - getSecondaryIssueLocationMessagesForLine (line: SourceLine, issueKey: string) { - return this.props.issueSecondaryLocationMessagesByIssueByLine[issueKey][line.line] || EMPTY_ARRAY; - } - - renderLine = ( - line: SourceLine, - index: number, - displayCoverage: boolean, - displayDuplications: boolean, - displayFiltered: boolean, - displayIssues: boolean - ) => { - 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 duplicationsCount = this.props.duplications ? this.props.duplications.length : 0; - - const issuesForLine = this.getIssuesForLine(line); - - // 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 optimizedSelectedIssue = selectedIssue != null && issuesForLine.includes(selectedIssue) ? - selectedIssue : null; - - return ( - <SourceViewerLine - 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} - highlighted={line.line === this.props.highlightedLine} - highlightedSymbol={optimizedHighlightedSymbol} - issueLocations={this.getIssueLocationsForLine(line)} - issues={issuesForLine} - key={line.line} - line={line} - loadDuplications={this.props.loadDuplications} - onClick={this.props.onLineClick} - onCoverageClick={this.props.onCoverageClick} - onDuplicationClick={this.props.onDuplicationClick} - onIssueSelect={this.props.onIssueSelect} - onIssueUnselect={this.props.onIssueUnselect} - onSCMClick={this.props.onSCMClick} - onSymbolClick={this.props.onSymbolClick} - secondaryIssueLocations={secondaryIssueLocations} - secondaryIssueLocationMessages={secondaryIssueLocationMessages} - selectedIssue={optimizedSelectedIssue}/> - ); - }; - - render () { - const { sources } = this.props; - - const hasCoverage = sources.some(s => s.coverageStatus != null); - const hasDuplications = sources.some(s => s.duplicated); - const displayFiltered = this.props.filterLine != null; - const hasIssues = this.props.issues.length > 0; - - const hasFileIssues = hasIssues && this.props.issues.some(issue => !issue.line); - - return ( - <div> - {this.props.hasSourcesBefore && ( - <div className="source-viewer-more-code"> - {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> - </div> - ) : ( - <button className="js-component-viewer-source-before" onClick={this.props.loadSourcesBefore}> - {translate('source_viewer.load_more_code')} - </button> - )} - </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> - - {this.props.hasSourcesAfter && ( - <div className="source-viewer-more-code"> - {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> - </div> - ) : ( - <button className="js-component-viewer-source-after" onClick={this.props.loadSourcesAfter}> - {translate('source_viewer.load_more_code')} - </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 deleted file mode 100644 index 14dedd85572..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.js +++ /dev/null @@ -1,185 +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 { 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 { - props: { - component: { - canMarkAsFavorite: boolean, - key: string, - measures: { - coverage?: string, - duplicationDensity?: string, - issues?: string, - lines?: string, - tests?: string - }, - path: string, - project: string, - projectName: string, - q: string, - subProject?: string, - subProjectName?: string - }, - openNewWindow: () => void, - showMeasures: () => void - }; - - showMeasures = (e: SyntheticInputEvent) => { - e.preventDefault(); - this.props.showMeasures(); - }; - - openNewWindow = (e: SyntheticInputEvent) => { - e.preventDefault(); - this.props.openNewWindow(); - }; - - openInWorkspace = (e: SyntheticInputEvent) => { - e.preventDefault(); - const { key } = this.props.component; - Workspace.openComponent({ key }); - }; - - render () { - 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; - const rawSourcesLink = `${window.baseUrl}/api/sources/raw?key=${encodeURIComponent(this.props.component.key)}`; - - // TODO favorite - return ( - <div className="source-viewer-header"> - <div className="source-viewer-header-component"> - <div className="component-name"> - <div className="component-name-parent"> - <Link to={getProjectUrl(project)} className="link-with-icon"> - <QualifierIcon qualifier="TRK"/> <span>{projectName}</span> - </Link> - </div> - - {subProject != null && ( - <div className="component-name-parent"> - <Link to={getProjectUrl(subProject)} className="link-with-icon"> - <QualifierIcon qualifier="BRC"/> <span>{subProjectName}</span> - </Link> - </div> - )} - - <div className="component-name-path"> - <QualifierIcon qualifier={q}/> - {' '} - <span>{collapsedDirFromPath(path)}</span> - <span className="component-name-file">{fileFromPath(path)}</span> - - {this.props.component.canMarkAsFavorite && ( - <FavoriteContainer className="component-name-favorite" componentKey={key}/> - )} - </div> - </div> - </div> - - <div className="dropdown source-viewer-header-actions"> - <a className="js-actions icon-list dropdown-toggle" - data-toggle="dropdown" - title={translate('component_viewer.more_actions')}/> - <ul className="dropdown-menu dropdown-menu-right"> - <li> - <a className="js-measures" href="#" onClick={this.showMeasures}> - {translate('component_viewer.show_details')} - </a> - </li> - <li> - <a className="js-new-window" href="#" onClick={this.openNewWindow}> - {translate('component_viewer.new_window')} - </a> - </li> - {!workspace && ( - <li> - <a className="js-workspace" href="#" onClick={this.openInWorkspace}> - {translate('component_viewer.open_in_workspace')} - </a> - </li> - )} - <li> - <a className="js-raw-source" href={rawSourcesLink} target="_blank"> - {translate('component_viewer.show_raw_source')} - </a> - </li> - </ul> - </div> - - <div className="source-viewer-header-measures"> - {isUnitTest && ( - <div className="source-viewer-header-measure"> - <span className="source-viewer-header-measure-value">{formatMeasure(measures.tests, 'SHORT_INT')}</span> - <span className="source-viewer-header-measure-label">{translate('metric.tests.name')}</span> - </div> - )} - - {!isUnitTest && ( - <div className="source-viewer-header-measure"> - <span className="source-viewer-header-measure-value">{formatMeasure(measures.lines, 'SHORT_INT')}</span> - <span className="source-viewer-header-measure-label">{translate('metric.lines.name')}</span> - </div> - )} - - <div className="source-viewer-header-measure"> - <span className="source-viewer-header-measure-value"> - <Link to={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> - </div> - - {measures.coverage != null && ( - <div className="source-viewer-header-measure"> - <span className="source-viewer-header-measure-value">{formatMeasure(measures.coverage, 'PERCENT')}</span> - <span className="source-viewer-header-measure-label">{translate('metric.coverage.name')}</span> - </div> - )} - - {measures.duplicationDensity != null && ( - <div className="source-viewer-header-measure"> - <span className="source-viewer-header-measure-value"> - {formatMeasure(measures.duplicationDensity, 'PERCENT')} - </span> - <span className="source-viewer-header-measure-label">{translate('duplications')}</span> - </div> - )} - </div> - </div> - ); - } -} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerIssuesIndicator.js b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerIssuesIndicator.js deleted file mode 100644 index f6993949244..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerIssuesIndicator.js +++ /dev/null @@ -1,44 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -// @flow -import React from 'react'; -import { connect } from 'react-redux'; -import SeverityIcon from '../shared/severity-icon'; -import { getIssueByKey } from '../../store/rootReducer'; -import { sortBySeverity } from '../../helpers/issues'; - -class SourceViewerIssuesIndicator extends React.Component { - props: { - issue: { severity: string } - }; - - render () { - return ( - <SeverityIcon severity={this.props.issue.severity}/> - ); - } -} - -const mapStateToProps = (state, ownProps: { issues: Array<string> }) => { - const issues = ownProps.issues.map(issueKey => getIssueByKey(state, issueKey)); - return { issue: sortBySeverity(issues)[0] }; -}; - -export default connect(mapStateToProps)(SourceViewerIssuesIndicator); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerLine.js b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerLine.js deleted file mode 100644 index 72cb0d5c053..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerLine.js +++ /dev/null @@ -1,377 +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'; - -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<{ from: number, to: number }>, - 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, - onSymbolClick: (string) => void, - selectedIssue: string | null, - // $FlowFixMe - secondaryIssueLocations: Array<{ from: number, to: number }>, - // $FlowFixMe - secondaryIssueLocationMessages: Array<{ msg: string, index?: number }> -}; - -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 (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(''); - - 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> - ); - } - - renderSecondaryIssueLocationMessages (locationMessages: Array<{ msg: string, index?: number }>) { - const limitString = (str: string) => ( - str.length > 30 ? str.substr(0, 30) + '...' : str - ); - - 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> - ))} - </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) { - tokens = highlightIssueLocations(tokens, secondaryIssueLocations, 'source-line-code-secondary-issue'); - } - - const finalCode = generateHTML(tokens); - - const showIssues = (this.state.issuesOpen || this.props.displayAllIssues) && issues.length > 0; - - return ( - <td className={className} data-line-number={line.line}> - <div className="source-line-code-inner"> - <pre ref={node => this.codeNode = node} dangerouslySetInnerHTML={{ __html: finalCode }}/> - {secondaryIssueLocationMessages != null && secondaryIssueLocationMessages.length > 0 && ( - this.renderSecondaryIssueLocationMessages(secondaryIssueLocationMessages) - )} - </div> - {showIssues && ( - <div className="issue-list"> - {issues.map(issue => ( - <ConnectedIssue - key={issue} - issueKey={issue} - onClick={this.handleIssueSelect} - selected={this.props.selectedIssue === issue}/> - ))} - </div> - )} - </td> - ); - } - - render () { - const { line, duplicationsCount, filtered } = this.props; - const className = classNames('source-line', { - 'source-line-highlighted': this.props.highlighted, - 'source-line-shadowed': filtered === false, - 'source-line-filtered': filtered === true - }); - - return ( - <tr className={className} data-line-number={line.line}> - {this.renderLineNumber()} - - {this.renderSCM()} - - {this.props.displayCoverage && this.renderCoverage()} - - {this.props.displayDuplications && this.renderDuplications()} - - {duplicationsCount > 0 && this.renderDuplicationsExtra()} - - {this.props.displayIssues && !this.props.displayAllIssues && this.renderIssuesIndicator()} - - {this.props.displayFiltered && ( - <td className="source-meta source-line-filtered-container" data-line-number={line.line}> - <div className="source-line-bar"/> - </td> - )} - - {this.renderCode()} - </tr> - ); - } -} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/StandaloneSourceViewer.js b/server/sonar-web/src/main/js/components/SourceViewer/StandaloneSourceViewer.js deleted file mode 100644 index d673bd44dd0..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/StandaloneSourceViewer.js +++ /dev/null @@ -1,47 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -// @flow -import { connect } from 'react-redux'; -import StandaloneSourceViewerBase from './StandaloneSourceViewerBase'; -import { receiveFavorites } from '../../store/favorites/duck'; -import { receiveIssues } from '../../store/issues/duck'; - -const mapStateToProps = null; - -const onReceiveComponent = (component: { key: string, canMarkAsFavorite: boolean, fav: boolean }) => dispatch => { - if (component.canMarkAsFavorite) { - const favorites = []; - const notFavorites = []; - if (component.fav) { - favorites.push({ key: component.key }); - } else { - notFavorites.push({ key: component.key }); - } - dispatch(receiveFavorites(favorites, notFavorites)); - } -}; - -const onReceiveIssues = (issues: Array<*>) => dispatch => { - dispatch(receiveIssues(issues)); -}; - -const mapDispatchToProps = { onReceiveComponent, onReceiveIssues }; - -export default connect(mapStateToProps, mapDispatchToProps)(StandaloneSourceViewerBase); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/StandaloneSourceViewerBase.js b/server/sonar-web/src/main/js/components/SourceViewer/StandaloneSourceViewerBase.js deleted file mode 100644 index ea28e00b36f..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/StandaloneSourceViewerBase.js +++ /dev/null @@ -1,50 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -// @flow -import React from 'react'; -import SourceViewerBase from './SourceViewerBase'; - -type State = { - selectedIssue: string | null -}; - -export default class StandaloneSourceViewerBase extends React.Component { - state: State = { - selectedIssue: null - }; - - handleIssueSelect = (issue: string) => { - this.setState({ selectedIssue: issue }); - }; - - handleIssueUnselect = () => { - this.setState({ selectedIssue: null }); - }; - - render () { - return ( - <SourceViewerBase - {...this.props} - onIssueSelect={this.handleIssueSelect} - onIssueUnselect={this.handleIssueUnselect} - selectedIssue={this.state.selectedIssue}/> - ); - } -} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/getCoverageStatus.js b/server/sonar-web/src/main/js/components/SourceViewer/helpers/getCoverageStatus.js deleted file mode 100644 index 2f99ed81675..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/helpers/getCoverageStatus.js +++ /dev/null @@ -1,37 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -// @flow -import type { SourceLine } from '../types'; - -const getCoverageStatus = (s: SourceLine): string | null => { - let status = null; - if (s.lineHits != null && s.lineHits > 0) { - status = 'partially-covered'; - } - if (s.lineHits != null && s.lineHits > 0 && s.conditions === s.coveredConditions) { - status = 'covered'; - } - if (s.lineHits === 0 || s.coveredConditions === 0) { - status = 'uncovered'; - } - return status; -}; - -export default getCoverageStatus; 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 deleted file mode 100644 index 0adc3f0d31f..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/helpers/highlight.js +++ /dev/null @@ -1,115 +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 escapeHtml from 'escape-html'; - -type Token = { className: string, text: string }; -type Tokens = Array<Token>; - -const ISSUE_LOCATION_CLASS = 'source-line-code-issue'; - -export const splitByTokens = (code: string, rootClassName: string = ''): Tokens => { - 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; -}; - -export const highlightSymbol = (tokens: Tokens, symbol: string): Tokens => ( - tokens.map(token => token.className.includes(symbol) ? - { ...token, className: `${token.className} highlighted` } : - token -)); - -/** - * Intersect two ranges - * @param s1 Start position of the first range - * @param e1 End position of the first range - * @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 } => { - return { from: Math.max(s1, s2), to: Math.min(e1, e2) }; -}; - -/** - * Get the substring of a string - * @param str A string - * @param from "From" offset - * @param to "To" offset - * @param acc Global offset to eliminate - */ -const part = (str: string, from: number, to: number, acc: number): string => { - // 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); -}; - -/** - * Highlight issue locations in the list of tokens - */ -export const highlightIssueLocations = ( - tokens: Tokens, - issueLocations: Array<{ from: number, to: number }>, - rootClassName: string = ISSUE_LOCATION_CLASS -): Tokens => { - 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(rootClassName) === -1 ? - `${token.className} ${rootClassName}` : - 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; -}; - -export const generateHTML = (tokens: Tokens): string => { - 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 deleted file mode 100644 index a9016ef0c7c..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/helpers/indexing.js +++ /dev/null @@ -1,119 +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 { splitByTokens } from './highlight'; -import { getLinearLocations, getIssueLocations } from './issueLocations'; -import type { Issue } from '../../issue/types'; -import type { SourceLine } from '../types'; - -export const issuesByLine = (issues: Array<Issue>) => { - const index = {}; - issues.forEach(issue => { - const line = issue.line || 0; - if (!(line in index)) { - index[line] = []; - } - index[line].push(issue.key); - }); - return index; -}; - -export const locationsByLine = (issues: Array<Issue>) => { - const index = {}; - issues.forEach(issue => { - getLinearLocations(issue.textRange).forEach(location => { - if (!(location.line in index)) { - index[location.line] = []; - } - index[location.line].push(location); - }); - }); - return index; -}; - -export const locationsByIssueAndLine = (issues: Array<Issue>) => { - const index = {}; - issues.forEach(issue => { - const byLine = {}; - getIssueLocations(issue).forEach(location => { - getLinearLocations(location.textRange).forEach(linearLocation => { - if (!(linearLocation.line in byLine)) { - byLine[linearLocation.line] = []; - } - byLine[linearLocation.line].push({ from: linearLocation.from, to: linearLocation.to }); - }); - }); - index[issue.key] = byLine; - }); - return index; -}; - -export const locationMessagesByIssueAndLine = (issues: Array<Issue>) => { - const index = {}; - issues.forEach(issue => { - const byLine = {}; - getIssueLocations(issue).forEach(location => { - const line = location.textRange ? location.textRange.startLine : 0; - if (!(line in byLine)) { - byLine[line] = []; - } - byLine[line].push({ msg: location.msg, index: location.index }); - }); - index[issue.key] = byLine; - }); - return index; -}; - -export const duplicationsByLine = (duplications: Array<*> | null) => { - if (duplications == null) { - return {}; - } - - const duplicationsByLine = {}; - - duplications.forEach(({ blocks }, duplicationIndex) => { - blocks.forEach(block => { - if (block._ref === '1') { - for (let line = block.from; line < block.from + block.size; line++) { - if (!(line in duplicationsByLine)) { - duplicationsByLine[line] = []; - } - duplicationsByLine[line].push(duplicationIndex); - } - } - }); - }); - - return duplicationsByLine; -}; - -export const symbolsByLine = (sources: Array<SourceLine>) => { - const index = {}; - sources.forEach(line => { - const tokens = splitByTokens(line.code); - index[line.line] = tokens - .map(token => { - const key = token.className.match(/sym-\d+/); - return key && key[0]; - }) - .filter(key => key); - }); - return index; -}; 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 deleted file mode 100644 index d2c8991fc3c..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/helpers/issueLocations.js +++ /dev/null @@ -1,59 +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 type { TextRange, Issue } from '../../issue/types'; - -export const getLinearLocations = (textRange?: TextRange): Array<{ line: number, from: number, to: number }> => { - if (!textRange) { - return []; - } - const locations = []; - - // go through all lines of the `textRange` - for (let line = textRange.startLine; line <= textRange.endLine; line++) { - // TODO fix 999999 - const from = line === textRange.startLine ? textRange.startOffset : 0; - const to = line === textRange.endLine ? textRange.endOffset : 999999; - locations.push({ line, from, to }); - } - 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 }) => { - if (locations) { - const locationsCount = locations.length; - locations.forEach((location, index) => { - const flowLocation = { - ...location, - // set index only for real flows, do not set for just secondary locations - index: locationsCount > 1 ? locationsCount - index : undefined - }; - allLocations.push(flowLocation); - }); - } - }); - return allLocations; -}; 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 deleted file mode 100644 index ddc2963c0e7..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.js +++ /dev/null @@ -1,76 +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 { searchIssues } from '../../../api/issues'; -import { parseIssueFromResponse } from '../../../helpers/issues'; - -export type Query = { [string]: string }; - -export type Issues = Array<*>; - -// maximum possible value -const PAGE_SIZE = 500; - -const buildQuery = (component: string): Query => ({ - additionalFields: '_all', - resolved: 'false', - componentKeys: component, - 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 loadPageAndNext = ( - query: Query, - toLine: number, - page: number, - pageSize: number = PAGE_SIZE -): Promise<Issues> => { - return loadPage(query, page).then(issues => { - if (issues.length === 0) { - return []; - } - - const lastIssue = issues[issues.length - 1]; - - if ((lastIssue.line != null && lastIssue.line > toLine) || issues.length < pageSize) { - return issues; - } - - return loadPageAndNext(query, toLine, page + 1, pageSize).then(nextIssues => { - return [...issues, ...nextIssues]; - }); - }); -}; - -const loadIssues = (component: string, fromLine: number, toLine: number): Promise<Issues> => { - const query = buildQuery(component); - return new Promise(resolve => { - loadPageAndNext(query, toLine, 1).then(issues => { - resolve(issues); - }); - }); -}; - -export default loadIssues; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/types.js b/server/sonar-web/src/main/js/components/SourceViewer/types.js deleted file mode 100644 index 3dd00eeb553..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/types.js +++ /dev/null @@ -1,40 +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 -export type SourceLine = { - code: string, - conditions?: number, - coverageStatus?: string | null, - coveredConditions?: number, - duplicated: boolean, - line: number, - lineHits?: number, - scmAuthor?: string, - scmDate?: string, - scmRevision?: string -}; - -export type Duplication = { - blocks: Array<{ - _ref: string, - from: number, - size: number - }> -}; diff --git a/server/sonar-web/src/main/js/components/common/popup.js b/server/sonar-web/src/main/js/components/common/popup.js index af7d22f632c..363f0bdf72b 100644 --- a/server/sonar-web/src/main/js/components/common/popup.js +++ b/server/sonar-web/src/main/js/components/common/popup.js @@ -25,23 +25,22 @@ export default Marionette.ItemView.extend({ onRender () { this.$el.detach().appendTo($('body')); - const triggerEl = $(this.options.triggerEl); if (this.options.bottom) { this.$el.addClass('bubble-popup-bottom'); this.$el.css({ - top: triggerEl.offset().top + triggerEl.outerHeight(), - left: triggerEl.offset().left + top: this.options.triggerEl.offset().top + this.options.triggerEl.outerHeight(), + left: this.options.triggerEl.offset().left }); } else if (this.options.bottomRight) { this.$el.addClass('bubble-popup-bottom-right'); this.$el.css({ - top: triggerEl.offset().top + triggerEl.outerHeight(), - right: $(window).width() - triggerEl.offset().left - triggerEl.outerWidth() + top: this.options.triggerEl.offset().top + this.options.triggerEl.outerHeight(), + right: $(window).width() - this.options.triggerEl.offset().left - this.options.triggerEl.outerWidth() }); } else { this.$el.css({ - top: triggerEl.offset().top, - left: triggerEl.offset().left + triggerEl.outerWidth() + top: this.options.triggerEl.offset().top, + left: this.options.triggerEl.offset().left + this.options.triggerEl.outerWidth() }); } this.attachCloseEvents(); @@ -49,7 +48,6 @@ export default Marionette.ItemView.extend({ attachCloseEvents () { const that = this; - const triggerEl = $(this.options.triggerEl); key('escape', () => { that.destroy(); }); @@ -57,8 +55,8 @@ export default Marionette.ItemView.extend({ $('body').off('click.bubble-popup'); that.destroy(); }); - triggerEl.on('click.bubble-popup', e => { - triggerEl.off('click.bubble-popup'); + this.options.triggerEl.on('click.bubble-popup', e => { + that.options.triggerEl.off('click.bubble-popup'); e.stopPropagation(); that.destroy(); }); @@ -66,7 +64,7 @@ export default Marionette.ItemView.extend({ onDestroy () { $('body').off('click.bubble-popup'); - const triggerEl = $(this.options.triggerEl); - triggerEl.off('click.bubble-popup'); + this.options.triggerEl.off('click.bubble-popup'); } }); + diff --git a/server/sonar-web/src/main/js/components/issue/Issue.js b/server/sonar-web/src/main/js/components/issue/Issue.js deleted file mode 100644 index c437b8f41af..00000000000 --- a/server/sonar-web/src/main/js/components/issue/Issue.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. - */ -// @flow -import React from 'react'; -import { connect } from 'react-redux'; -import IssueView from './issue-view'; -import IssueModel from './models/issue'; -import { receiveIssues } from '../../store/issues/duck'; -import type { Issue as IssueType } from './types'; - -type Model = { toJSON: () => {} }; - -type Props = { - checked?: boolean, - issue: IssueType | Model, - onCheck?: () => void, - onClick: () => void, - onFilterClick?: () => void, - onIssueChange: ({}) => void, - selected: boolean -}; - -class Issue extends React.PureComponent { - issueView: Object; - node: HTMLElement; - props: Props; - - componentDidMount () { - this.renderIssueView(); - if (this.props.selected) { - this.bindShortcuts(); - } - } - - componentWillUpdate (nextProps: Props) { - if (!nextProps.selected && this.props.selected) { - this.unbindShortcuts(); - } - this.destroyIssueView(); - } - - componentDidUpdate (prevProps: Props) { - this.renderIssueView(); - if (!prevProps.selected && this.props.selected) { - this.bindShortcuts(); - } - } - - componentWillUnmount () { - if (this.props.selected) { - this.unbindShortcuts(); - } - this.destroyIssueView(); - } - - bindShortcuts () { - document.addEventListener('keypress', this.handleKeyPress); - } - - unbindShortcuts () { - document.removeEventListener('keypress', this.handleKeyPress); - } - - doIssueAction (action: string) { - this.issueView.$('.js-issue-' + action).click(); - } - - handleKeyPress = (e: Object) => { - const tagName = e.target.tagName.toUpperCase(); - const shouldHandle = tagName !== 'INPUT' && tagName !== 'TEXTAREA' && tagName !== 'BUTTON'; - - if (shouldHandle) { - switch (e.key) { - case 'f': return this.doIssueAction('transition'); - case 'a': return this.doIssueAction('assign'); - case 'm': return this.doIssueAction('assign-to-me'); - case 'p': return this.doIssueAction('plan'); - case 'i': return this.doIssueAction('set-severity'); - case 'c': return this.doIssueAction('comment'); - case 't': return this.doIssueAction('edit-tags'); - } - } - }; - - renderIssueView () { - const model = this.props.issue.toJSON ? this.props.issue : new IssueModel(this.props.issue); - this.issueView = new IssueView({ - model, - checked: this.props.checked, - onCheck: this.props.onCheck, - onClick: this.props.onClick, - onFilterClick: this.props.onFilterClick, - onIssueChange: this.props.onIssueChange - }); - this.issueView.render().$el.appendTo(this.node); - if (this.props.selected) { - this.issueView.select(); - } - } - - destroyIssueView () { - this.issueView.destroy(); - } - - render () { - return <div className="issue-container" ref={node => this.node = node}/>; - } -} - -const onIssueChange = issue => dispatch => { - dispatch(receiveIssues([issue])); -}; - -const mapDispatchToProps = { onIssueChange }; - -export default connect(null, mapDispatchToProps)(Issue); 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 a4a691fa955..71ced0ff47a 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 @@ -34,21 +34,16 @@ import Template from './templates/issue.hbs'; import getCurrentUserFromStore from '../../app/utils/getCurrentUserFromStore'; export default Marionette.ItemView.extend({ + className: 'issue', template: Template, modelEvents: { - 'change': 'notifyAndRender', + 'change': 'render', 'transition': 'onTransition' }, - className () { - const hasCheckbox = this.options.onCheck != null; - return hasCheckbox ? 'issue issue-with-checkbox' : 'issue'; - }, - events () { return { - 'click': 'handleClick', 'click .js-issue-comment': 'onComment', 'click .js-issue-comment-edit': 'editComment', 'click .js-issue-comment-delete': 'deleteComment', @@ -61,24 +56,10 @@ export default Marionette.ItemView.extend({ 'click .js-issue-show-changelog': 'showChangeLog', 'click .js-issue-rule': 'showRule', 'click .js-issue-edit-tags': 'editTags', - 'click .js-issue-locations': 'showLocations', - 'click .js-issue-filter': 'filterSimilarIssues', - 'click .js-toggle': 'onIssueCheck' + 'click .js-issue-locations': 'showLocations' }; }, - notifyAndRender () { - const { onIssueChange } = this.options; - if (onIssueChange) { - onIssueChange(this.model.toJSON()); - } - - // if ConnectedIssue is used, this view can be destroyed just after onIssueChange() - if (!this.isDestroyed) { - this.render(); - } - }, - onRender () { this.$el.attr('data-key', this.model.get('key')); }, @@ -262,45 +243,19 @@ export default Marionette.ItemView.extend({ this.model.trigger('locations', this.model); }, - select () { - this.$el.addClass('selected'); - }, - - unselect () { - this.$el.removeClass('selected'); - }, - onTransition (transition) { if (transition === 'falsepositive' || transition === 'wontfix') { this.comment({ fromTransition: true }); } }, - handleClick (e) { - e.preventDefault(); - const { onClick } = this.options; - if (onClick) { - onClick(this.model.get('key')); - } - }, - - filterSimilarIssues (e) { - this.options.onFilterClick(e); - }, - - onIssueCheck (e) { - this.options.onCheck(e); - }, - serializeData () { const issueKey = encodeURIComponent(this.model.get('key')); return { ...Marionette.ItemView.prototype.serializeData.apply(this, arguments), permalink: window.baseUrl + '/issues/search#issues=' + issueKey, - hasSecondaryLocations: this.model.get('flows').length, - hasSimilarIssuesFilter: this.options.onFilterClick != null, - hasCheckbox: this.options.onCheck != null, - checked: this.options.checked + hasSecondaryLocations: this.model.get('flows').length }; } }); + diff --git a/server/sonar-web/src/main/js/components/issue/templates/issue.hbs b/server/sonar-web/src/main/js/components/issue/templates/issue.hbs index f951a40c0c4..a828ecf5e3e 100644 --- a/server/sonar-web/src/main/js/components/issue/templates/issue.hbs +++ b/server/sonar-web/src/main/js/components/issue/templates/issue.hbs @@ -35,15 +35,6 @@ <li class="issue-meta"> <a class="js-issue-permalink icon-link" href="{{permalink}}" target="_blank"></a> </li> - - {{#if hasSimilarIssuesFilter}} - <li class="issue-meta"> - <button class="button-link issue-action issue-action-with-options js-issue-filter" - aria-label="{{t "issue.filter_similar_issues"}}"> - <i class="icon-filter icon-half-transparent"></i> <i class="icon-dropdown"></i> - </button> - </li> - {{/if}} </ul> </td> </tr> @@ -174,9 +165,3 @@ <i class="issue-navigate-to-left icon-chevron-left"></i> <i class="issue-navigate-to-right icon-chevron-right"></i> </a> - -{{#if hasCheckbox}} - <div class="js-toggle issue-checkbox-container"> - <i class="issue-checkbox icon-checkbox {{#if checked}}icon-checkbox-checked{{/if}}"></i> - </div> -{{/if}} diff --git a/server/sonar-web/src/main/js/components/issue/types.js b/server/sonar-web/src/main/js/components/issue/types.js deleted file mode 100644 index dd0bbc1d2e6..00000000000 --- a/server/sonar-web/src/main/js/components/issue/types.js +++ /dev/null @@ -1,40 +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 -export type TextRange = { - startLine: number, - startOffset: number, - endLine: number, - endOffset: number -}; - -export type Issue = { - key: string, - flows: Array<{ - locations?: Array<{ - msg: string, - textRange?: TextRange - }> - }>, - line?: number, - message: string, - severity: string, - textRange: TextRange -}; diff --git a/server/sonar-web/src/main/js/components/shared/WithStore.js b/server/sonar-web/src/main/js/components/shared/WithStore.js deleted file mode 100644 index f3cb83233e2..00000000000 --- a/server/sonar-web/src/main/js/components/shared/WithStore.js +++ /dev/null @@ -1,44 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -// @flow -import React from 'react'; -import getStore from '../../app/utils/getStore'; - -export default class WithStore extends React.Component { - store: {}; - props: { children: Object }; - - static childContextTypes = { - store: React.PropTypes.object - }; - - constructor (props: { children: Object }) { - super(props); - this.store = getStore(); - } - - getChildContext () { - return { store: this.store }; - } - - render () { - return this.props.children; - } -} diff --git a/server/sonar-web/src/main/js/components/source-viewer/SourceViewer.js b/server/sonar-web/src/main/js/components/source-viewer/SourceViewer.js new file mode 100644 index 00000000000..0589bcd0643 --- /dev/null +++ b/server/sonar-web/src/main/js/components/source-viewer/SourceViewer.js @@ -0,0 +1,82 @@ +/* + * 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 BaseSourceViewer from './main'; +import { getPeriodDate, getPeriodLabel } from '../../helpers/periods'; + +export default class SourceViewer extends React.Component { + static propTypes = { + component: React.PropTypes.shape({ + id: React.PropTypes.string.isRequired + }).isRequired, + period: React.PropTypes.object, + line: React.PropTypes.oneOfType([React.PropTypes.number, React.PropTypes.string]) + }; + + componentDidMount () { + this.renderSourceViewer(); + } + + shouldComponentUpdate (nextProps) { + return nextProps.component.id !== this.props.component.id; + } + + componentWillUpdate () { + this.destroySourceViewer(); + } + + componentDidUpdate () { + this.renderSourceViewer(); + } + + componentWillUnmount () { + this.destroySourceViewer(); + } + + renderSourceViewer () { + this.sourceViewer = new BaseSourceViewer(); + this.sourceViewer.render().$el.appendTo(this.refs.container); + this.sourceViewer.open(this.props.component.id); + this.sourceViewer.on('loaded', this.handleLoad.bind(this)); + } + + destroySourceViewer () { + this.sourceViewer.destroy(); + } + + handleLoad () { + const { period, line } = this.props; + + if (period) { + const periodDate = getPeriodDate(period); + const periodLabel = getPeriodLabel(period); + this.sourceViewer.filterLinesByDate(periodDate, periodLabel); + } + + if (line) { + this.sourceViewer.highlightLine(line); + this.sourceViewer.scrollToLine(line); + } + } + + render () { + return <div ref="container"/>; + } +} 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 index 8b1725d09f3..58bfb9ce4f8 100644 --- a/server/sonar-web/src/main/js/components/source-viewer/main.js +++ b/server/sonar-web/src/main/js/components/source-viewer/main.js @@ -21,6 +21,7 @@ import $ from 'jquery'; import moment from 'moment'; import sortBy from 'lodash/sortBy'; import toPairs from 'lodash/toPairs'; +import Backbone from 'backbone'; import Marionette from 'backbone.marionette'; import Source from './source'; import Issues from '../issue/collections/issues'; @@ -402,7 +403,7 @@ export default Marionette.LayoutView.extend({ const row = this.model.get('source').find(row => row.line === line); const popup = new SCMPopupView({ triggerEl: $(e.currentTarget), - line: row + model: new Backbone.Model(row) }); popup.render(); }, @@ -421,8 +422,8 @@ export default Marionette.LayoutView.extend({ }; return $.get(url, options).done(data => { const popup = new CoveragePopupView({ - line: row, - tests: data.tests, + row, + collection: new Backbone.Collection(data.tests), triggerEl: $(e.currentTarget) }); popup.render(); @@ -467,11 +468,10 @@ export default Marionette.LayoutView.extend({ return isOk; }); const popup = new DuplicationPopupView({ - blocks, inRemovedComponent, - component: this.model.toJSON(), - files: this.model.get('duplicationFiles'), - triggerEl: $(e.currentTarget) + triggerEl: $(e.currentTarget), + model: this.model, + collection: new Backbone.Collection(blocks) }); popup.render(); }, @@ -498,7 +498,8 @@ export default Marionette.LayoutView.extend({ const popup = new LineActionsPopupView({ line, triggerEl: $(e.currentTarget), - component: this.model.toJSON() + model: this.model, + row: $(e.currentTarget).closest('.source-line') }); popup.render(); }, diff --git a/server/sonar-web/src/main/js/components/source-viewer/measures-overlay.js b/server/sonar-web/src/main/js/components/source-viewer/measures-overlay.js index 4baf170a2e8..a01d69b0f85 100644 --- a/server/sonar-web/src/main/js/components/source-viewer/measures-overlay.js +++ b/server/sonar-web/src/main/js/components/source-viewer/measures-overlay.js @@ -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.model.get('isUnitTest')) { requests.push(this.requestTests()); } Promise.all(requests).then(() => this.render()); @@ -282,3 +282,4 @@ export default ModalView.extend({ }; } }); + 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 index aba02a8e1de..9b7181a6463 100644 --- 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 @@ -50,8 +50,8 @@ export default Marionette.ItemView.extend({ }, openInWorkspace () { - const key = this.options.parent.model.get('key'); - Workspace.openComponent({ key }); + const uuid = this.options.parent.model.id; + Workspace.openComponent({ uuid }); }, showRawSource () { @@ -66,3 +66,4 @@ export default Marionette.ItemView.extend({ }; } }); + diff --git a/server/sonar-web/src/main/js/components/source-viewer/popups/coverage-popup.js b/server/sonar-web/src/main/js/components/source-viewer/popups/coverage-popup.js index 68fd0ccc388..1440241e42a 100644 --- a/server/sonar-web/src/main/js/components/source-viewer/popups/coverage-popup.js +++ b/server/sonar-web/src/main/js/components/source-viewer/popups/coverage-popup.js @@ -27,7 +27,7 @@ export default Popup.extend({ template: Template, events: { - 'click a[data-key]': 'goToFile' + 'click a[data-id]': 'goToFile' }, onRender () { @@ -37,19 +37,19 @@ export default Popup.extend({ goToFile (e) { e.stopPropagation(); - const key = $(e.currentTarget).data('key'); - Workspace.openComponent({ key }); + const id = $(e.currentTarget).data('id'); + Workspace.openComponent({ uuid: id }); }, serializeData () { - const row = this.options.line || {}; - const tests = groupBy(this.options.tests, 'fileKey'); - const testFiles = Object.keys(tests).map(fileKey => { - const testSet = tests[fileKey]; + const row = this.options.row || {}; + const tests = groupBy(this.collection.toJSON(), 'fileId'); + const testFiles = Object.keys(tests).map(fileId => { + const testSet = tests[fileId]; const test = testSet[0]; return { file: { - key: test.fileKey, + id: test.fileId, longName: test.fileName }, tests: testSet @@ -58,3 +58,4 @@ export default Popup.extend({ return { testFiles, row }; } }); + diff --git a/server/sonar-web/src/main/js/components/source-viewer/popups/duplication-popup.js b/server/sonar-web/src/main/js/components/source-viewer/popups/duplication-popup.js index da542333a30..24ad94fe254 100644 --- a/server/sonar-web/src/main/js/components/source-viewer/popups/duplication-popup.js +++ b/server/sonar-web/src/main/js/components/source-viewer/popups/duplication-popup.js @@ -28,35 +28,37 @@ export default Popup.extend({ template: Template, events: { - 'click a[data-key]': 'goToFile' + 'click a[data-uuid]': 'goToFile' }, goToFile (e) { e.stopPropagation(); - const key = $(e.currentTarget).data('key'); + const uuid = $(e.currentTarget).data('uuid'); const line = $(e.currentTarget).data('line'); - Workspace.openComponent({ key, line }); + Workspace.openComponent({ uuid, line }); }, serializeData () { const that = this; - const groupedBlocks = groupBy(this.options.blocks, '_ref'); + const files = this.model.get('duplicationFiles'); + const groupedBlocks = groupBy(this.collection.toJSON(), '_ref'); let duplications = Object.keys(groupedBlocks).map(fileRef => { return { blocks: groupedBlocks[fileRef], - file: this.options.files[fileRef] + file: files[fileRef] }; }); duplications = sortBy(duplications, d => { - const a = d.file.projectName !== that.options.component.projectName; - const b = d.file.subProjectName !== that.options.component.subProjectName; - const c = d.file.key !== that.options.component.key; + const a = d.file.projectName !== that.model.get('projectName'); + const b = d.file.subProjectName !== that.model.get('subProjectName'); + const c = d.file.key !== that.model.get('key'); return '' + a + b + c; }); return { duplications, - component: this.options.component, + component: this.model.toJSON(), inRemovedComponent: this.options.inRemovedComponent }; } }); + diff --git a/server/sonar-web/src/main/js/components/source-viewer/popups/line-actions-popup.js b/server/sonar-web/src/main/js/components/source-viewer/popups/line-actions-popup.js index a2d94f568b8..aa89585bc44 100644 --- a/server/sonar-web/src/main/js/components/source-viewer/popups/line-actions-popup.js +++ b/server/sonar-web/src/main/js/components/source-viewer/popups/line-actions-popup.js @@ -29,9 +29,9 @@ export default Popup.extend({ getPermalink (e) { e.preventDefault(); - const { component, line } = this.options; - const url = `${window.baseUrl}/component/index?id=${encodeURIComponent(component.key)}&line=${line}`; + const url = + `${window.baseUrl}/component/index?id=${encodeURIComponent(this.model.key())}&line=${this.options.line}`; const windowParams = 'resizable=1,scrollbars=1,status=1'; - window.open(url, component.name, windowParams); + window.open(url, this.model.get('name'), windowParams); } }); diff --git a/server/sonar-web/src/main/js/components/source-viewer/popups/scm-popup.js b/server/sonar-web/src/main/js/components/source-viewer/popups/scm-popup.js index f140e37c56b..755a866baec 100644 --- a/server/sonar-web/src/main/js/components/source-viewer/popups/scm-popup.js +++ b/server/sonar-web/src/main/js/components/source-viewer/popups/scm-popup.js @@ -34,12 +34,6 @@ export default Popup.extend({ onClick (e) { e.stopPropagation(); - }, - - serializeData () { - return { - ...Popup.prototype.serializeData.apply(this, arguments), - line: this.options.line - }; } }); + diff --git a/server/sonar-web/src/main/js/components/source-viewer/source.js b/server/sonar-web/src/main/js/components/source-viewer/source.js index 3cb1198e328..43009061a22 100644 --- a/server/sonar-web/src/main/js/components/source-viewer/source.js +++ b/server/sonar-web/src/main/js/components/source-viewer/source.js @@ -96,3 +96,4 @@ export default Backbone.Model.extend({ return source.some(line => line.coverageStatus != null); } }); + diff --git a/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-coverage-popup.hbs b/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-coverage-popup.hbs index 57c6301119e..a0e7b62896e 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/source-viewer/templates/source-viewer-coverage-popup.hbs @@ -15,7 +15,7 @@ {{#each testFiles}} <div class="bubble-popup-section"> - <a class="component-viewer-popup-test-file link-action" data-key="{{file.key}}" title="{{file.longName}}"> + <a class="component-viewer-popup-test-file link-action" data-id="{{file.id}}" title="{{file.longName}}"> <span>{{collapsePath file.longName}}</span> </a> <ul class="bubble-popup-list"> @@ -24,7 +24,7 @@ <i class="component-viewer-popup-test-status {{testStatusIconClass status}}"></i> <span class="component-viewer-popup-test-name"> <a class="component-viewer-popup-test-file link-action" title="{{name}}" - data-key="{{../file.key}}" data-method="{{name}}"> + data-id="{{../file.id}}" data-method="{{name}}"> {{name}} </a> </span> diff --git a/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-duplication-popup.hbs b/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-duplication-popup.hbs index ea8fc2b2349..9b0783c6655 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/source-viewer/templates/source-viewer-duplication-popup.hbs @@ -21,7 +21,7 @@ {{#notEq file.key ../component.key}} <div class="component-name-path"> - <a class="link-action" data-key="{{file.key}}" title="{{file.name}}"> + <a class="link-action" data-uuid="{{file.uuid}}" title="{{file.name}}"> <span>{{collapsedDirFromPath file.name}}</span><span class="component-name-file">{{fileFromPath file.name}}</span> </a> @@ -31,7 +31,7 @@ <div class="component-name-path"> Lines: {{#joinEach blocks ','}} - <a class="link-action" data-key="{{../file.key}}" data-line="{{this.from}}"> + <a class="link-action" data-uuid="{{../file.uuid}}" data-line="{{this.from}}"> {{this.from}} – {{sum from size -1}} </a> {{/joinEach}} 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 index a4532354481..e276c7e938b 100644 --- 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 @@ -16,7 +16,7 @@ <div class="component-name-path"> {{qualifierIcon q}} <span>{{collapsedDirFromPath path}}</span><span class="component-name-file">{{fileFromPath path}}</span> - {{#if canMarkAsFavorite}} + {{#if canMarkAsFavourite}} <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> diff --git a/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-measures.hbs b/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-measures.hbs index 0df076390c9..a3f4df55605 100644 --- a/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-measures.hbs +++ b/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-measures.hbs @@ -19,16 +19,7 @@ {{/unless}} </div> - {{#eq q 'UTS'}} - <div class="source-viewer-measures"> - <div class="source-viewer-measures-section"> - {{> 'measures/_source-viewer-measures-tests'}} - </div> - </div> - <div class="source-viewer-measures"> - {{> 'measures/_source-viewer-measures-test-cases'}} - </div> - {{else}} + {{#unless isUnitTest}} <div class="source-viewer-measures"> <div class="source-viewer-measures-section"> <div class="source-viewer-measures-card"> @@ -52,7 +43,16 @@ {{> 'measures/_source-viewer-measures-duplications'}} </div> </div> - {{/eq}} + {{else}} + <div class="source-viewer-measures"> + <div class="source-viewer-measures-section"> + {{> 'measures/_source-viewer-measures-tests'}} + </div> + </div> + <div class="source-viewer-measures"> + {{> 'measures/_source-viewer-measures-test-cases'}} + </div> + {{/unless}} <div class="spacer-bottom"> </div> diff --git a/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-scm-popup.hbs b/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-scm-popup.hbs index dd82aca528c..768ea72341d 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/source-viewer/templates/source-viewer-scm-popup.hbs @@ -1,13 +1,13 @@ <div class="bubble-popup-container"> <div class="bubble-popup-section"> - {{line.scmAuthor}} + {{scmAuthor}} </div> <div class="bubble-popup-section"> - {{dt line.scmDate}} + {{dt scmDate}} </div> - {{#if line.scmRevision}} + {{#if scmRevision}} <div class="bubble-popup-section"> - {{line.scmRevision}} + {{scmRevision}} </div> {{/if}} </div> diff --git a/server/sonar-web/src/main/js/components/workspace/main.js b/server/sonar-web/src/main/js/components/workspace/main.js index 4e1170bac38..30082332e4b 100644 --- a/server/sonar-web/src/main/js/components/workspace/main.js +++ b/server/sonar-web/src/main/js/components/workspace/main.js @@ -99,8 +99,7 @@ Workspace.prototype = { that.closeComponentViewer(); m.destroy(); }); - this.viewerView.$el.appendTo(document.body); - this.viewerView.render(); + this.viewerView.render().$el.appendTo(document.body); }, showComponentViewer (model) { diff --git a/server/sonar-web/src/main/js/components/workspace/models/item.js b/server/sonar-web/src/main/js/components/workspace/models/item.js index 1dd6daf7fc5..0ecbef4ac33 100644 --- a/server/sonar-web/src/main/js/components/workspace/models/item.js +++ b/server/sonar-web/src/main/js/components/workspace/models/item.js @@ -25,8 +25,8 @@ export default Backbone.Model.extend({ if (!this.has('__type__')) { return 'type is missing'; } - if (this.get('__type__') === 'component' && !this.has('key')) { - return 'key is missing'; + if (this.get('__type__') === 'component' && !this.has('uuid')) { + return 'uuid is missing'; } if (this.get('__type__') === 'rule' && !this.has('key')) { return 'key is missing'; diff --git a/server/sonar-web/src/main/js/components/workspace/models/items.js b/server/sonar-web/src/main/js/components/workspace/models/items.js index 97ff41e2267..5d015e037ea 100644 --- a/server/sonar-web/src/main/js/components/workspace/models/items.js +++ b/server/sonar-web/src/main/js/components/workspace/models/items.js @@ -47,13 +47,16 @@ export default Backbone.Collection.extend({ }, has (model) { - const forComponent = model.isComponent() && this.findWhere({ key: model.get('key') }) != null; + const forComponent = model.isComponent() && this.findWhere({ uuid: model.get('uuid') }) != null; const forRule = model.isRule() && this.findWhere({ key: model.get('key') }) != null; return forComponent || forRule; }, add2 (model) { - const tryModel = this.findWhere({ key: model.get('key') }); + const tryModel = model.isComponent() ? + this.findWhere({ uuid: model.get('uuid') }) : + this.findWhere({ key: model.get('key') }); return tryModel != null ? tryModel : this.add(model); } }); + 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..924ea80ad7f 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 @@ -17,13 +17,9 @@ * 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 React from 'react'; -import { render } from 'react-dom'; import BaseView from './base-viewer-view'; -import SourceViewer from '../../SourceViewer/StandaloneSourceViewer'; +import SourceViewer from '../../source-viewer/main'; import Template from '../templates/workspace-viewer.hbs'; -import WithStore from '../../shared/WithStore'; export default BaseView.extend({ template: Template, @@ -33,39 +29,22 @@ export default BaseView.extend({ this.showViewer(); }, - scrollToLine (line) { - const row = this.$el.find(`.source-line[data-line-number="${line}"]`); - if (row.length > 0) { - const sourceViewer = this.$el.find('.source-viewer'); - let p = sourceViewer.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); - } - }, - showViewer () { - const { key, line } = this.model.toJSON(); - - const el = document.querySelector(this.viewerRegion.el); - - render(( - <WithStore> - <SourceViewer - component={key} - fromWorkspace={true} - highlightedLine={line} - onLoaded={component => { - this.model.set({ name: component.name, q: component.q }); - if (line) { - this.scrollToLine(line); - } - }}/> - </WithStore> - ), el); + const that = this; + const viewer = new SourceViewer(); + const options = this.model.toJSON(); + viewer.open(this.model.get('uuid'), { workspace: true }); + viewer.on('loaded', () => { + that.model.set({ + name: viewer.model.get('name'), + q: viewer.model.get('q') + }); + if (options.line != null) { + viewer.highlightLine(options.line); + viewer.scrollToLine(options.line); + } + }); + this.viewerRegion.show(viewer); } }); + diff --git a/server/sonar-web/src/main/js/helpers/issues.js b/server/sonar-web/src/main/js/helpers/issues.js deleted file mode 100644 index 3a1e509f790..00000000000 --- a/server/sonar-web/src/main/js/helpers/issues.js +++ /dev/null @@ -1,121 +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 sortBy from 'lodash/sortBy'; -import { SEVERITIES } from './constants'; - -type TextRange = { - startLine: number, - endLine: number, - startOffset: number, - endOffset: number -}; - -type Comment = { - login: string -}; - -type User = { - login: string -}; - -type RawIssue = { - assignee?: string, - author: string, - comments?: Array<Comment>, - component: string, - line?: number, - project: string, - rule: string, - status: string, - subProject?: string, - textRange?: TextRange -}; - -export const sortBySeverity = (issues: Array<*>) => ( - sortBy(issues, issue => SEVERITIES.indexOf(issue.severity)) -); - -const injectRelational = ( - issue: RawIssue | Comment, - source?: Array<*>, - baseField: string, - lookupField: string -) => { - const newFields = {}; - const baseValue = issue[baseField]; - if (baseValue != null && source != null) { - const lookupValue = source.find(candidate => candidate[lookupField] === baseValue); - if (lookupValue != null) { - Object.keys(lookupValue).forEach(key => { - const newKey = baseField + key.charAt(0).toUpperCase() + key.slice(1); - newFields[newKey] = lookupValue[key]; - }); - } - } - return newFields; -}; - -const injectCommentsRelational = (issue: RawIssue, users?: Array<User>) => { - if (!issue.comments) { - return {}; - } - const comments = issue.comments.map(comment => ({ - ...comment, - author: comment.login, - login: undefined, - ...injectRelational(comment, users, 'author', 'login') - })); - return { comments }; -}; - -const prepareClosed = (issue: RawIssue) => { - return issue.status === 'CLOSED' ? { flows: undefined } : {}; -}; - -const ensureTextRange = (issue: RawIssue) => { - return issue.line && !issue.textRange ? { - textRange: { - startLine: issue.line, - endLine: issue.line, - startOffset: 0, - endOffset: 999999 - } - } : {}; -}; - -export const parseIssueFromResponse = ( - issue: RawIssue, - components?: Array<*>, - users?: Array<*>, - rules?: Array<*> -) => { - return { - ...issue, - ...injectRelational(issue, components, 'component', 'key'), - ...injectRelational(issue, components, 'project', 'key'), - ...injectRelational(issue, components, 'subProject', 'key'), - ...injectRelational(issue, rules, 'rule', 'key'), - ...injectRelational(issue, users, 'assignee', 'login'), - ...injectCommentsRelational(issue, users), - ...prepareClosed(issue), - ...ensureTextRange(issue) - }; -}; diff --git a/server/sonar-web/src/main/js/helpers/request.js b/server/sonar-web/src/main/js/helpers/request.js index cbd5a4e7c01..80bb9e787cc 100644 --- a/server/sonar-web/src/main/js/helpers/request.js +++ b/server/sonar-web/src/main/js/helpers/request.js @@ -146,18 +146,19 @@ export function request (url: string): Request { * @returns {*} */ export function checkStatus (response: Response): Promise<Object> { - return new Promise((resolve, reject) => { - if (response.status === 401) { - // workaround cyclic dependencies - const handleRequiredAuthentication = require('../app/utils/handleRequiredAuthentication').default; - handleRequiredAuthentication(); - reject(); - } else if (response.status >= 200 && response.status < 300) { - resolve(response); - } else { - reject({ response }); - } - }); + if (response.status === 401) { + // workaround cyclic dependencies + const handleRequiredAuthentication = require('../app/utils/handleRequiredAuthentication').default; + handleRequiredAuthentication(); + return Promise.reject(); + } else if (response.status >= 200 && response.status < 300) { + return Promise.resolve(response); + } else { + const error = new Error(response.status); + // $FlowFixMe complains that `response` is not found + error.response = response; + throw error; + } } /** diff --git a/server/sonar-web/src/main/js/store/favorites/duck.js b/server/sonar-web/src/main/js/store/favorites/duck.js index ceeb119abfa..c97f715edbd 100644 --- a/server/sonar-web/src/main/js/store/favorites/duck.js +++ b/server/sonar-web/src/main/js/store/favorites/duck.js @@ -17,58 +17,32 @@ * 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 uniq from 'lodash/uniq'; import without from 'lodash/without'; -type Favorite = { key: string }; - -type ReceiveFavoritesAction = { - type: 'RECEIVE_FAVORITES', - favorites: Array<Favorite>, - notFavorites: Array<Favorite> -}; - -type AddFavoriteAction = { - type: 'ADD_FAVORITE', - componentKey: string -}; - -type RemoveFavoriteAction = { - type: 'REMOVE_FAVORITE', - componentKey: string -}; - -type Action = ReceiveFavoritesAction | AddFavoriteAction | RemoveFavoriteAction; - -type State = Array<string>; - export const actions = { RECEIVE_FAVORITES: 'RECEIVE_FAVORITES', ADD_FAVORITE: 'ADD_FAVORITE', REMOVE_FAVORITE: 'REMOVE_FAVORITE' }; -export const receiveFavorites = ( - favorites: Array<Favorite>, - notFavorites: Array<Favorite> = [] -): ReceiveFavoritesAction => ({ +export const receiveFavorites = (favorites, notFavorites = []) => ({ type: actions.RECEIVE_FAVORITES, favorites, notFavorites }); -export const addFavorite = (componentKey: string): AddFavoriteAction => ({ +export const addFavorite = componentKey => ({ type: actions.ADD_FAVORITE, componentKey }); -export const removeFavorite = (componentKey: string): RemoveFavoriteAction => ({ +export const removeFavorite = componentKey => ({ type: actions.REMOVE_FAVORITE, componentKey }); -export default (state: State = [], action: Action): State => { +export default (state = [], action = {}) => { if (action.type === actions.RECEIVE_FAVORITES) { const toAdd = action.favorites.map(f => f.key); const toRemove = action.notFavorites.map(f => f.key); @@ -86,6 +60,7 @@ export default (state: State = [], action: Action): State => { return state; }; -export const isFavorite = (state: State, componentKey: string) => ( +export const isFavorite = (state, componentKey) => ( state.includes(componentKey) ); + diff --git a/server/sonar-web/src/main/js/store/issues/duck.js b/server/sonar-web/src/main/js/store/issues/duck.js deleted file mode 100644 index 1126bcfd57f..00000000000 --- a/server/sonar-web/src/main/js/store/issues/duck.js +++ /dev/null @@ -1,52 +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 keyBy from 'lodash/keyBy'; - -type Issue = { key: string }; - -type ReceiveIssuesAction = { - type: 'RECEIVE_ISSUES', - issues: Array<Issue> -}; - -type Action = ReceiveIssuesAction; - -type State = { [key: string]: Issue }; - -export const receiveIssues = (issues: Array<Issue>): ReceiveIssuesAction => ({ - type: 'RECEIVE_ISSUES', - issues -}); - -const reducer = (state: State = {}, action: Action) => { - switch (action.type) { - case 'RECEIVE_ISSUES': - return { ...state, ...keyBy(action.issues, 'key') }; - default: - return state; - } -}; - -export default reducer; - -export const getIssueByKey = (state: State, key: string): ?Issue => ( - state[key] -); diff --git a/server/sonar-web/src/main/js/store/rootReducer.js b/server/sonar-web/src/main/js/store/rootReducer.js index 1b8539f84eb..aee309845c2 100644 --- a/server/sonar-web/src/main/js/store/rootReducer.js +++ b/server/sonar-web/src/main/js/store/rootReducer.js @@ -22,7 +22,6 @@ import appState from './appState/duck'; import components, * as fromComponents from './components/reducer'; import users, * as fromUsers from './users/reducer'; import favorites, * as fromFavorites from './favorites/duck'; -import issues, * as fromIssues from './issues/duck'; import languages, * as fromLanguages from './languages/reducer'; import measures, * as fromMeasures from './measures/reducer'; import notifications, * as fromNotifications from './notifications/duck'; @@ -41,7 +40,6 @@ export default combineReducers({ components, globalMessages, favorites, - issues, languages, measures, notifications, @@ -82,10 +80,6 @@ export const isFavorite = (state, componentKey) => ( fromFavorites.isFavorite(state.favorites, componentKey) ); -export const getIssueByKey = (state, key) => ( - fromIssues.getIssueByKey(state.issues, key) -); - export const getComponentMeasure = (state, componentKey, metricKey) => ( fromMeasures.getComponentMeasure(state.measures, componentKey, metricKey) ); diff --git a/server/sonar-web/src/main/less/components/issues.less b/server/sonar-web/src/main/less/components/issues.less index 8202664e4cc..b56858fe641 100644 --- a/server/sonar-web/src/main/less/components/issues.less +++ b/server/sonar-web/src/main/less/components/issues.less @@ -50,8 +50,7 @@ border-color: @issueBorderColor !important; } -.issue + .issue, -.issue-container + .issue-container { +.issue + .issue { margin-top: 5px; } diff --git a/server/sonar-web/src/main/less/components/source.less b/server/sonar-web/src/main/less/components/source.less index 9a89d87959a..18a2cfa5d1c 100644 --- a/server/sonar-web/src/main/less/components/source.less +++ b/server/sonar-web/src/main/less/components/source.less @@ -143,14 +143,6 @@ user-select: none; } -.source-meta:focus { - outline: none; -} - -.source-meta[role="button"] { - cursor: pointer; -} - .source-meta + .source-meta { border-left: 1px solid @barBackgroundColor; } @@ -162,6 +154,10 @@ color: @secondFontColor; text-align: right; + &[data-line-number] { + cursor: pointer; + } + &:before { content: attr(data-line-number); } @@ -211,6 +207,10 @@ .source-line-scm { padding: 0 5px; background-color: @barBackgroundColor; + + &[data-line-number] { + cursor: pointer; + } } .source-line-scm-inner { @@ -229,21 +229,29 @@ height: @source-line-height; } +.source-line-with-issues { + cursor: pointer; +} + .source-line-covered { background-color: @green !important; + cursor: pointer; } .source-line-uncovered { background-color: @red !important; + cursor: pointer; } .source-line-partially-covered { background-color: @orange !important; background-image: repeating-linear-gradient(45deg, rgba(255, 255, 255, .5) 4px, transparent 4px, transparent 8px, rgba(255, 255, 255, .5) 8px, rgba(255, 255, 255, .5) 12px, transparent 12px, transparent 16px, rgba(255, 255, 255, .5) 16px, rgba(255, 255, 255, .5) 20px) !important; + cursor: pointer; } .source-line-duplicated { background-color: @duplicationColor !important; + cursor: pointer; } diff --git a/server/sonar-web/src/main/less/pages/issues.less b/server/sonar-web/src/main/less/pages/issues.less index 98bf375c2ea..fd1ddd7431e 100644 --- a/server/sonar-web/src/main/less/pages/issues.less +++ b/server/sonar-web/src/main/less/pages/issues.less @@ -45,15 +45,11 @@ padding: 0 10px; } -.issues-workspace-list-item + .issues-workspace-list-item { - margin-top: 5px; -} - -.issues-workspace-list-component + .issues-workspace-list-item { +.issues-workspace-list-component + .issue { margin-top: 10px; } -.issues-workspace-list-item + .issues-workspace-list-component { +.issue + .issues-workspace-list-component { margin-top: 25px; } diff --git a/server/sonar-web/src/main/less/sonar-colorizer.less b/server/sonar-web/src/main/less/sonar-colorizer.less index 760aec28453..325bcad8177 100644 --- a/server/sonar-web/src/main/less/sonar-colorizer.less +++ b/server/sonar-web/src/main/less/sonar-colorizer.less @@ -70,11 +70,5 @@ cursor: pointer; } .highlighted { - background-color: #b3d4ff; - animation: highlightedFadeIn 0.3s forwards; -} - -@keyframes highlightedFadeIn { - from { background-color: transparent; } - to { background-color: #b3d4ff; } + background-color: #B3D4FF; } |