Bladeren bron

Revert "refactor source viewer (#1705)"

This reverts commit ce9f0892fc.
tags/6.4-RC1
Stas Vilchik 7 jaren geleden
bovenliggende
commit
b6baff8775
71 gewijzigde bestanden met toevoegingen van 509 en 2811 verwijderingen
  1. 7
    19
      server/sonar-server/src/main/java/org/sonar/server/component/ws/AppAction.java
  2. 1
    1
      server/sonar-server/src/main/resources/org/sonar/server/component/ws/app-example.json
  3. 1
    1
      server/sonar-server/src/test/java/org/sonar/server/component/ws/ComponentsWsTest.java
  4. 1
    1
      server/sonar-server/src/test/resources/org/sonar/server/component/ws/AppActionTest/app.json
  5. 1
    1
      server/sonar-server/src/test/resources/org/sonar/server/component/ws/AppActionTest/app_with_measures.json
  6. 1
    1
      server/sonar-server/src/test/resources/org/sonar/server/component/ws/AppActionTest/app_with_ut_measure.json
  7. 1
    2
      server/sonar-web/.eslintrc
  8. 0
    23
      server/sonar-web/src/main/js/api/components.js
  9. 4
    18
      server/sonar-web/src/main/js/api/issues.js
  10. 2
    2
      server/sonar-web/src/main/js/apps/code/components/App.js
  11. 1
    1
      server/sonar-web/src/main/js/apps/code/components/ComponentPin.js
  12. 1
    1
      server/sonar-web/src/main/js/apps/component-measures/components/bubbleChart/BubbleChart.js
  13. 3
    14
      server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ListView.js
  14. 3
    14
      server/sonar-web/src/main/js/apps/component-measures/details/drilldown/TreeView.js
  15. 1
    1
      server/sonar-web/src/main/js/apps/component-measures/details/treemap/MeasureTreemap.js
  16. 17
    28
      server/sonar-web/src/main/js/apps/component/components/App.js
  17. 13
    7
      server/sonar-web/src/main/js/apps/issues/component-viewer/issue-view.js
  18. 143
    71
      server/sonar-web/src/main/js/apps/issues/component-viewer/main.js
  19. 8
    1
      server/sonar-web/src/main/js/apps/issues/controller.js
  20. 3
    0
      server/sonar-web/src/main/js/apps/issues/templates/issues-issue-checkbox.hbs
  21. 6
    0
      server/sonar-web/src/main/js/apps/issues/templates/issues-issue-filter.hbs
  22. 46
    44
      server/sonar-web/src/main/js/apps/issues/workspace-list-item-view.js
  23. 23
    0
      server/sonar-web/src/main/js/apps/issues/workspace-list-view.js
  24. 2
    2
      server/sonar-web/src/main/js/apps/overview/components/App.js
  25. 0
    47
      server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.js
  26. 0
    499
      server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js
  27. 0
    222
      server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.js
  28. 0
    185
      server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.js
  29. 0
    44
      server/sonar-web/src/main/js/components/SourceViewer/SourceViewerIssuesIndicator.js
  30. 0
    377
      server/sonar-web/src/main/js/components/SourceViewer/SourceViewerLine.js
  31. 0
    47
      server/sonar-web/src/main/js/components/SourceViewer/StandaloneSourceViewer.js
  32. 0
    50
      server/sonar-web/src/main/js/components/SourceViewer/StandaloneSourceViewerBase.js
  33. 0
    37
      server/sonar-web/src/main/js/components/SourceViewer/helpers/getCoverageStatus.js
  34. 0
    115
      server/sonar-web/src/main/js/components/SourceViewer/helpers/highlight.js
  35. 0
    119
      server/sonar-web/src/main/js/components/SourceViewer/helpers/indexing.js
  36. 0
    59
      server/sonar-web/src/main/js/components/SourceViewer/helpers/issueLocations.js
  37. 0
    76
      server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.js
  38. 0
    40
      server/sonar-web/src/main/js/components/SourceViewer/types.js
  39. 10
    12
      server/sonar-web/src/main/js/components/common/popup.js
  40. 0
    133
      server/sonar-web/src/main/js/components/issue/Issue.js
  41. 5
    50
      server/sonar-web/src/main/js/components/issue/issue-view.js
  42. 0
    15
      server/sonar-web/src/main/js/components/issue/templates/issue.hbs
  43. 0
    40
      server/sonar-web/src/main/js/components/issue/types.js
  44. 0
    44
      server/sonar-web/src/main/js/components/shared/WithStore.js
  45. 82
    0
      server/sonar-web/src/main/js/components/source-viewer/SourceViewer.js
  46. 9
    8
      server/sonar-web/src/main/js/components/source-viewer/main.js
  47. 2
    1
      server/sonar-web/src/main/js/components/source-viewer/measures-overlay.js
  48. 3
    2
      server/sonar-web/src/main/js/components/source-viewer/more-actions.js
  49. 9
    8
      server/sonar-web/src/main/js/components/source-viewer/popups/coverage-popup.js
  50. 11
    9
      server/sonar-web/src/main/js/components/source-viewer/popups/duplication-popup.js
  51. 3
    3
      server/sonar-web/src/main/js/components/source-viewer/popups/line-actions-popup.js
  52. 1
    7
      server/sonar-web/src/main/js/components/source-viewer/popups/scm-popup.js
  53. 1
    0
      server/sonar-web/src/main/js/components/source-viewer/source.js
  54. 2
    2
      server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-coverage-popup.hbs
  55. 2
    2
      server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-duplication-popup.hbs
  56. 1
    1
      server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-header.hbs
  57. 11
    11
      server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-measures.hbs
  58. 4
    4
      server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-scm-popup.hbs
  59. 1
    2
      server/sonar-web/src/main/js/components/workspace/main.js
  60. 2
    2
      server/sonar-web/src/main/js/components/workspace/models/item.js
  61. 5
    2
      server/sonar-web/src/main/js/components/workspace/models/items.js
  62. 17
    38
      server/sonar-web/src/main/js/components/workspace/views/viewer-view.js
  63. 0
    121
      server/sonar-web/src/main/js/helpers/issues.js
  64. 13
    12
      server/sonar-web/src/main/js/helpers/request.js
  65. 6
    31
      server/sonar-web/src/main/js/store/favorites/duck.js
  66. 0
    52
      server/sonar-web/src/main/js/store/issues/duck.js
  67. 0
    6
      server/sonar-web/src/main/js/store/rootReducer.js
  68. 1
    2
      server/sonar-web/src/main/less/components/issues.less
  69. 16
    8
      server/sonar-web/src/main/less/components/source.less
  70. 2
    6
      server/sonar-web/src/main/less/pages/issues.less
  71. 1
    7
      server/sonar-web/src/main/less/sonar-colorizer.less

+ 7
- 19
server/sonar-server/src/main/java/org/sonar/server/component/ws/AppAction.java Bestand weergeven

@@ -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) {

+ 1
- 1
server/sonar-server/src/main/resources/org/sonar/server/component/ws/app-example.json Bestand weergeven

@@ -7,7 +7,7 @@
"project": "com.sonarsource:java-markdown",
"projectName": "Java Markdown",
"fav": false,
"canMarkAsFavorite": true,
"canMarkAsFavourite": true,
"canCreateManualIssue": true,
"measures": {
"lines": "786",

+ 1
- 1
server/sonar-server/src/test/java/org/sonar/server/component/ws/ComponentsWsTest.java Bestand weergeven

@@ -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);
}
}

+ 1
- 1
server/sonar-server/src/test/resources/org/sonar/server/component/ws/AppActionTest/app.json Bestand weergeven

@@ -10,6 +10,6 @@
"project": "org.sonarsource.sonarqube:sonarqube",
"projectName": "SonarQube",
"fav": false,
"canMarkAsFavorite": true,
"canMarkAsFavourite": true,
"measures": {}
}

+ 1
- 1
server/sonar-server/src/test/resources/org/sonar/server/component/ws/AppActionTest/app_with_measures.json Bestand weergeven

@@ -10,7 +10,7 @@
"project": "org.sonarsource.sonarqube:sonarqube",
"projectName": "SonarQube",
"fav": false,
"canMarkAsFavorite": true,
"canMarkAsFavourite": true,
"measures": {
"lines": "200.0",
"coverage": "95.4",

+ 1
- 1
server/sonar-server/src/test/resources/org/sonar/server/component/ws/AppActionTest/app_with_ut_measure.json Bestand weergeven

@@ -10,7 +10,7 @@
"project": "org.sonarsource.sonarqube:sonarqube",
"projectName": "SonarQube",
"fav": false,
"canMarkAsFavorite": true,
"canMarkAsFavourite": true,
"measures": {
"coverage": "95.4"
}

+ 1
- 2
server/sonar-web/.eslintrc Bestand weergeven

@@ -13,8 +13,7 @@
"globals": {
"key": true,
"d3": true,
"baseUrl": true,
"SyntheticInputEvent": true
"baseUrl": true
},

"parser": "babel-eslint",

+ 0
- 23
server/sonar-web/src/main/js/api/components.js Bestand weergeven

@@ -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)
);

+ 4
- 18
server/sonar-web/src/main/js/api/issues.js Bestand weergeven

@@ -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 };
});
}


+ 2
- 2
server/sonar-web/src/main/js/apps/code/components/App.js Bestand weergeven

@@ -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>

+ 1
- 1
server/sonar-web/src/main/js/apps/code/components/ComponentPin.js Bestand weergeven

@@ -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 (

+ 1
- 1
server/sonar-web/src/main/js/apps/component-measures/components/bubbleChart/BubbleChart.js Bestand weergeven

@@ -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);
}

+ 3
- 14
server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ListView.js Bestand weergeven

@@ -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>

+ 3
- 14
server/sonar-web/src/main/js/apps/component-measures/details/drilldown/TreeView.js Bestand weergeven

@@ -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>

+ 1
- 1
server/sonar-web/src/main/js/apps/component-measures/details/treemap/MeasureTreemap.js Bestand weergeven

@@ -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;
}


+ 17
- 28
server/sonar-web/src/main/js/apps/component/components/App.js Bestand weergeven

@@ -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>
);
}

server/sonar-web/src/main/js/components/issue/ConnectedIssue.js → server/sonar-web/src/main/js/apps/issues/component-viewer/issue-view.js Bestand weergeven

@@ -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);

+ 143
- 71
server/sonar-web/src/main/js/apps/issues/component-viewer/main.js Bestand weergeven

@@ -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();
}
});


+ 8
- 1
server/sonar-web/src/main/js/apps/issues/controller.js Bestand weergeven

@@ -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) {

+ 3
- 0
server/sonar-web/src/main/js/apps/issues/templates/issues-issue-checkbox.hbs Bestand weergeven

@@ -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>

+ 6
- 0
server/sonar-web/src/main/js/apps/issues/templates/issues-issue-filter.hbs Bestand weergeven

@@ -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>&nbsp;<i class="icon-dropdown"></i>
</button>
</li>

+ 46
- 44
server/sonar-web/src/main/js/apps/issues/workspace-list-item-view.js Bestand weergeven

@@ -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
};
}
});

+ 23
- 0
server/sonar-web/src/main/js/apps/issues/workspace-list-view.js Bestand weergeven

@@ -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 });

+ 2
- 2
server/sonar-web/src/main/js/apps/overview/components/App.js Bestand weergeven

@@ -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>
);
}

+ 0
- 47
server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.js Bestand weergeven

@@ -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);

+ 0
- 499
server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js Bestand weergeven

@@ -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>
);
}
}

+ 0
- 222
server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.js Bestand weergeven

@@ -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>
);
}
}

+ 0
- 185
server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.js Bestand weergeven

@@ -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>
);
}
}

+ 0
- 44
server/sonar-web/src/main/js/components/SourceViewer/SourceViewerIssuesIndicator.js Bestand weergeven

@@ -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);

+ 0
- 377
server/sonar-web/src/main/js/components/SourceViewer/SourceViewerLine.js Bestand weergeven

@@ -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>
);
}
}

+ 0
- 47
server/sonar-web/src/main/js/components/SourceViewer/StandaloneSourceViewer.js Bestand weergeven

@@ -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);

+ 0
- 50
server/sonar-web/src/main/js/components/SourceViewer/StandaloneSourceViewerBase.js Bestand weergeven

@@ -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}/>
);
}
}

+ 0
- 37
server/sonar-web/src/main/js/components/SourceViewer/helpers/getCoverageStatus.js Bestand weergeven

@@ -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;

+ 0
- 115
server/sonar-web/src/main/js/components/SourceViewer/helpers/highlight.js Bestand weergeven

@@ -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('');
};

+ 0
- 119
server/sonar-web/src/main/js/components/SourceViewer/helpers/indexing.js Bestand weergeven

@@ -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;
};

+ 0
- 59
server/sonar-web/src/main/js/components/SourceViewer/helpers/issueLocations.js Bestand weergeven

@@ -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;
};

+ 0
- 76
server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.js Bestand weergeven

@@ -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;

+ 0
- 40
server/sonar-web/src/main/js/components/SourceViewer/types.js Bestand weergeven

@@ -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
}>
};

+ 10
- 12
server/sonar-web/src/main/js/components/common/popup.js Bestand weergeven

@@ -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');
}
});


+ 0
- 133
server/sonar-web/src/main/js/components/issue/Issue.js Bestand weergeven

@@ -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);

+ 5
- 50
server/sonar-web/src/main/js/components/issue/issue-view.js Bestand weergeven

@@ -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
};
}
});


+ 0
- 15
server/sonar-web/src/main/js/components/issue/templates/issue.hbs Bestand weergeven

@@ -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>&nbsp;<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}}

+ 0
- 40
server/sonar-web/src/main/js/components/issue/types.js Bestand weergeven

@@ -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
};

+ 0
- 44
server/sonar-web/src/main/js/components/shared/WithStore.js Bestand weergeven

@@ -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;
}
}

+ 82
- 0
server/sonar-web/src/main/js/components/source-viewer/SourceViewer.js Bestand weergeven

@@ -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"/>;
}
}

+ 9
- 8
server/sonar-web/src/main/js/components/source-viewer/main.js Bestand weergeven

@@ -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();
},

+ 2
- 1
server/sonar-web/src/main/js/components/source-viewer/measures-overlay.js Bestand weergeven

@@ -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({
};
}
});


+ 3
- 2
server/sonar-web/src/main/js/components/source-viewer/more-actions.js Bestand weergeven

@@ -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({
};
}
});


+ 9
- 8
server/sonar-web/src/main/js/components/source-viewer/popups/coverage-popup.js Bestand weergeven

@@ -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 };
}
});


+ 11
- 9
server/sonar-web/src/main/js/components/source-viewer/popups/duplication-popup.js Bestand weergeven

@@ -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
};
}
});


+ 3
- 3
server/sonar-web/src/main/js/components/source-viewer/popups/line-actions-popup.js Bestand weergeven

@@ -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);
}
});

+ 1
- 7
server/sonar-web/src/main/js/components/source-viewer/popups/scm-popup.js Bestand weergeven

@@ -34,12 +34,6 @@ export default Popup.extend({

onClick (e) {
e.stopPropagation();
},

serializeData () {
return {
...Popup.prototype.serializeData.apply(this, arguments),
line: this.options.line
};
}
});


+ 1
- 0
server/sonar-web/src/main/js/components/source-viewer/source.js Bestand weergeven

@@ -96,3 +96,4 @@ export default Backbone.Model.extend({
return source.some(line => line.coverageStatus != null);
}
});


+ 2
- 2
server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-coverage-popup.hbs Bestand weergeven

@@ -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>

+ 2
- 2
server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-duplication-popup.hbs Bestand weergeven

@@ -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}}

+ 1
- 1
server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-header.hbs Bestand weergeven

@@ -16,7 +16,7 @@
<div class="component-name-path">
{{qualifierIcon q}}&nbsp;<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>

+ 11
- 11
server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-measures.hbs Bestand weergeven

@@ -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">&nbsp;</div>

+ 4
- 4
server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-scm-popup.hbs Bestand weergeven

@@ -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>

+ 1
- 2
server/sonar-web/src/main/js/components/workspace/main.js Bestand weergeven

@@ -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) {

+ 2
- 2
server/sonar-web/src/main/js/components/workspace/models/item.js Bestand weergeven

@@ -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';

+ 5
- 2
server/sonar-web/src/main/js/components/workspace/models/items.js Bestand weergeven

@@ -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);
}
});


+ 17
- 38
server/sonar-web/src/main/js/components/workspace/views/viewer-view.js Bestand weergeven

@@ -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);
}
});


+ 0
- 121
server/sonar-web/src/main/js/helpers/issues.js Bestand weergeven

@@ -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)
};
};

+ 13
- 12
server/sonar-web/src/main/js/helpers/request.js Bestand weergeven

@@ -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;
}
}

/**

+ 6
- 31
server/sonar-web/src/main/js/store/favorites/duck.js Bestand weergeven

@@ -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)
);


+ 0
- 52
server/sonar-web/src/main/js/store/issues/duck.js Bestand weergeven

@@ -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]
);

+ 0
- 6
server/sonar-web/src/main/js/store/rootReducer.js Bestand weergeven

@@ -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)
);

+ 1
- 2
server/sonar-web/src/main/less/components/issues.less Bestand weergeven

@@ -50,8 +50,7 @@
border-color: @issueBorderColor !important;
}

.issue + .issue,
.issue-container + .issue-container {
.issue + .issue {
margin-top: 5px;
}


+ 16
- 8
server/sonar-web/src/main/less/components/source.less Bestand weergeven

@@ -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;
}



+ 2
- 6
server/sonar-web/src/main/less/pages/issues.less Bestand weergeven

@@ -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;
}


+ 1
- 7
server/sonar-web/src/main/less/sonar-colorizer.less Bestand weergeven

@@ -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;
}

Laden…
Annuleren
Opslaan