@@ -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) { |
@@ -7,7 +7,7 @@ | |||
"project": "com.sonarsource:java-markdown", | |||
"projectName": "Java Markdown", | |||
"fav": false, | |||
"canMarkAsFavorite": true, | |||
"canMarkAsFavourite": true, | |||
"canCreateManualIssue": true, | |||
"measures": { | |||
"lines": "786", |
@@ -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); | |||
} | |||
} |
@@ -10,6 +10,6 @@ | |||
"project": "org.sonarsource.sonarqube:sonarqube", | |||
"projectName": "SonarQube", | |||
"fav": false, | |||
"canMarkAsFavorite": true, | |||
"canMarkAsFavourite": true, | |||
"measures": {} | |||
} |
@@ -10,7 +10,7 @@ | |||
"project": "org.sonarsource.sonarqube:sonarqube", | |||
"projectName": "SonarQube", | |||
"fav": false, | |||
"canMarkAsFavorite": true, | |||
"canMarkAsFavourite": true, | |||
"measures": { | |||
"lines": "200.0", | |||
"coverage": "95.4", |
@@ -10,7 +10,7 @@ | |||
"project": "org.sonarsource.sonarqube:sonarqube", | |||
"projectName": "SonarQube", | |||
"fav": false, | |||
"canMarkAsFavorite": true, | |||
"canMarkAsFavourite": true, | |||
"measures": { | |||
"coverage": "95.4" | |||
} |
@@ -13,8 +13,7 @@ | |||
"globals": { | |||
"key": true, | |||
"d3": true, | |||
"baseUrl": true, | |||
"SyntheticInputEvent": true | |||
"baseUrl": true | |||
}, | |||
"parser": "babel-eslint", |
@@ -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) | |||
); |
@@ -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 }; | |||
}); | |||
} | |||
@@ -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> |
@@ -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 ( |
@@ -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); | |||
} |
@@ -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> |
@@ -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> |
@@ -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; | |||
} | |||
@@ -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> | |||
); | |||
} |
@@ -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); |
@@ -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(); | |||
} | |||
}); | |||
@@ -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) { |
@@ -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> |
@@ -0,0 +1,6 @@ | |||
<li class="issue-meta"> | |||
<button class="button-link issue-action issue-action-with-options js-issue-filter" | |||
aria-label="{{t "issue.filter_similar_issues"}}"> | |||
<i class="icon-filter icon-half-transparent"></i> <i class="icon-dropdown"></i> | |||
</button> | |||
</li> |
@@ -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 | |||
}; | |||
} | |||
}); |
@@ -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 }); |
@@ -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> | |||
); | |||
} |
@@ -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); |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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); |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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); |
@@ -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}/> | |||
); | |||
} | |||
} |
@@ -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; |
@@ -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(''); | |||
}; |
@@ -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; | |||
}; |
@@ -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; | |||
}; |
@@ -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; |
@@ -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 | |||
}> | |||
}; |
@@ -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'); | |||
} | |||
}); | |||
@@ -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); |
@@ -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 | |||
}; | |||
} | |||
}); | |||
@@ -35,15 +35,6 @@ | |||
<li class="issue-meta"> | |||
<a class="js-issue-permalink icon-link" href="{{permalink}}" target="_blank"></a> | |||
</li> | |||
{{#if hasSimilarIssuesFilter}} | |||
<li class="issue-meta"> | |||
<button class="button-link issue-action issue-action-with-options js-issue-filter" | |||
aria-label="{{t "issue.filter_similar_issues"}}"> | |||
<i class="icon-filter icon-half-transparent"></i> <i class="icon-dropdown"></i> | |||
</button> | |||
</li> | |||
{{/if}} | |||
</ul> | |||
</td> | |||
</tr> | |||
@@ -174,9 +165,3 @@ | |||
<i class="issue-navigate-to-left icon-chevron-left"></i> | |||
<i class="issue-navigate-to-right icon-chevron-right"></i> | |||
</a> | |||
{{#if hasCheckbox}} | |||
<div class="js-toggle issue-checkbox-container"> | |||
<i class="issue-checkbox icon-checkbox {{#if checked}}icon-checkbox-checked{{/if}}"></i> | |||
</div> | |||
{{/if}} |
@@ -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 | |||
}; |
@@ -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; | |||
} | |||
} |
@@ -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"/>; | |||
} | |||
} |
@@ -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(); | |||
}, |
@@ -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({ | |||
}; | |||
} | |||
}); | |||
@@ -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({ | |||
}; | |||
} | |||
}); | |||
@@ -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 }; | |||
} | |||
}); | |||
@@ -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 | |||
}; | |||
} | |||
}); | |||
@@ -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); | |||
} | |||
}); |
@@ -34,12 +34,6 @@ export default Popup.extend({ | |||
onClick (e) { | |||
e.stopPropagation(); | |||
}, | |||
serializeData () { | |||
return { | |||
...Popup.prototype.serializeData.apply(this, arguments), | |||
line: this.options.line | |||
}; | |||
} | |||
}); | |||
@@ -96,3 +96,4 @@ export default Backbone.Model.extend({ | |||
return source.some(line => line.coverageStatus != null); | |||
} | |||
}); | |||
@@ -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> |
@@ -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}} |
@@ -16,7 +16,7 @@ | |||
<div class="component-name-path"> | |||
{{qualifierIcon q}} <span>{{collapsedDirFromPath path}}</span><span class="component-name-file">{{fileFromPath path}}</span> | |||
{{#if canMarkAsFavorite}} | |||
{{#if canMarkAsFavourite}} | |||
<a class="js-favorite component-name-favorite {{#if fav}}icon-favorite{{else}}icon-not-favorite{{/if}}" | |||
title="{{#if fav}}{{t 'click_to_remove_from_favorites'}}{{else}}{{t 'click_to_add_to_favorites'}}{{/if}}"> | |||
</a> |
@@ -19,16 +19,7 @@ | |||
{{/unless}} | |||
</div> | |||
{{#eq q 'UTS'}} | |||
<div class="source-viewer-measures"> | |||
<div class="source-viewer-measures-section"> | |||
{{> 'measures/_source-viewer-measures-tests'}} | |||
</div> | |||
</div> | |||
<div class="source-viewer-measures"> | |||
{{> 'measures/_source-viewer-measures-test-cases'}} | |||
</div> | |||
{{else}} | |||
{{#unless isUnitTest}} | |||
<div class="source-viewer-measures"> | |||
<div class="source-viewer-measures-section"> | |||
<div class="source-viewer-measures-card"> | |||
@@ -52,7 +43,16 @@ | |||
{{> 'measures/_source-viewer-measures-duplications'}} | |||
</div> | |||
</div> | |||
{{/eq}} | |||
{{else}} | |||
<div class="source-viewer-measures"> | |||
<div class="source-viewer-measures-section"> | |||
{{> 'measures/_source-viewer-measures-tests'}} | |||
</div> | |||
</div> | |||
<div class="source-viewer-measures"> | |||
{{> 'measures/_source-viewer-measures-test-cases'}} | |||
</div> | |||
{{/unless}} | |||
<div class="spacer-bottom"> </div> |
@@ -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> |
@@ -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) { |
@@ -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'; |
@@ -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,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); | |||
} | |||
}); | |||
@@ -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) | |||
}; | |||
}; |
@@ -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; | |||
} | |||
} | |||
/** |
@@ -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) | |||
); | |||
@@ -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] | |||
); |
@@ -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) | |||
); |
@@ -50,8 +50,7 @@ | |||
border-color: @issueBorderColor !important; | |||
} | |||
.issue + .issue, | |||
.issue-container + .issue-container { | |||
.issue + .issue { | |||
margin-top: 5px; | |||
} | |||
@@ -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; | |||
} | |||
@@ -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; | |||
} | |||
@@ -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; | |||
} |