aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorStas Vilchik <vilchiks@gmail.com>2014-11-27 09:06:20 +0100
committerStas Vilchik <vilchiks@gmail.com>2014-11-27 16:15:41 +0100
commitbc6d4d9f8449781870e4451ffd5db47b63aef76a (patch)
tree91676cb8684b38fe5249917c26c9ba79b443fe18 /server
parent2d575bd89ffebc75899267c695a9bb55e974ba35 (diff)
downloadsonarqube-bc6d4d9f8449781870e4451ffd5db47b63aef76a.tar.gz
sonarqube-bc6d4d9f8449781870e4451ffd5db47b63aef76a.zip
SONAR-5877 Unified source viewer
Diffstat (limited to 'server')
-rw-r--r--server/sonar-web/Gruntfile.coffee6
-rw-r--r--server/sonar-web/src/main/coffee/common/overlay.coffee8
-rw-r--r--server/sonar-web/src/main/coffee/component-viewer/mixins/main-coverage.coffee9
-rw-r--r--server/sonar-web/src/main/coffee/component-viewer/source.coffee47
-rw-r--r--server/sonar-web/src/main/coffee/issue/issue-view.coffee1
-rw-r--r--server/sonar-web/src/main/coffee/issues/component-viewer/main.coffee210
-rw-r--r--server/sonar-web/src/main/coffee/issues/controller.coffee4
-rw-r--r--server/sonar-web/src/main/coffee/issues/layout.coffee8
-rw-r--r--server/sonar-web/src/main/coffee/source-viewer/popups/coverage-popup.coffee42
-rw-r--r--server/sonar-web/src/main/coffee/source-viewer/popups/duplication-popup.coffee55
-rw-r--r--server/sonar-web/src/main/coffee/source-viewer/popups/line-actions-popup.coffee40
-rw-r--r--server/sonar-web/src/main/coffee/source-viewer/source.coffee51
-rw-r--r--server/sonar-web/src/main/coffee/source-viewer/viewer.coffee360
-rw-r--r--server/sonar-web/src/main/hbs/component-viewer/cw-code-expand.hbs19
-rw-r--r--server/sonar-web/src/main/hbs/component-viewer/cw-source.hbs112
-rw-r--r--server/sonar-web/src/main/hbs/source-viewer/source-viewer-coverage-popup.hbs27
-rw-r--r--server/sonar-web/src/main/hbs/source-viewer/source-viewer-duplication-popup.hbs31
-rw-r--r--server/sonar-web/src/main/hbs/source-viewer/source-viewer-line-options-popup.hbs13
-rw-r--r--server/sonar-web/src/main/hbs/source-viewer/source-viewer.hbs51
-rw-r--r--server/sonar-web/src/main/js/common/handlebars-extensions.js5
-rw-r--r--server/sonar-web/src/main/js/common/jquery-isolated-scroll.js14
-rw-r--r--server/sonar-web/src/main/js/tests/e2e/tests/component-viewer-coverage-spec.js48
-rw-r--r--server/sonar-web/src/main/js/tests/e2e/tests/component-viewer-duplications-spec.js18
-rw-r--r--server/sonar-web/src/main/js/tests/e2e/tests/component-viewer-favorite-spec.js4
-rw-r--r--server/sonar-web/src/main/js/tests/e2e/tests/component-viewer-issues-spec.js8
-rw-r--r--server/sonar-web/src/main/js/tests/e2e/tests/component-viewer-lines-filters-spec.js8
-rw-r--r--server/sonar-web/src/main/js/tests/e2e/tests/component-viewer-link-to-raw-spec.js2
-rw-r--r--server/sonar-web/src/main/js/tests/e2e/tests/component-viewer-select-tab-by-metric-spec.js20
-rw-r--r--server/sonar-web/src/main/js/tests/e2e/tests/component-viewer-spec.js116
-rw-r--r--server/sonar-web/src/main/less/component-viewer-source-colorizer.less7
-rw-r--r--server/sonar-web/src/main/less/components.less2
-rw-r--r--server/sonar-web/src/main/less/components/code-source.less92
-rw-r--r--server/sonar-web/src/main/less/components/issues.less10
-rw-r--r--server/sonar-web/src/main/less/components/source.less146
-rw-r--r--server/sonar-web/src/main/less/issues.less5
-rw-r--r--server/sonar-web/src/main/less/mixins.less7
36 files changed, 1132 insertions, 474 deletions
diff --git a/server/sonar-web/Gruntfile.coffee b/server/sonar-web/Gruntfile.coffee
index d5a53f5350e..1127dbaf1cf 100644
--- a/server/sonar-web/Gruntfile.coffee
+++ b/server/sonar-web/Gruntfile.coffee
@@ -98,6 +98,7 @@ module.exports = (grunt) ->
'<%= pkg.assets %>js/common/inputs.js'
'<%= pkg.assets %>js/common/dialogs.js'
'<%= pkg.assets %>js/common/processes.js'
+ '<%= pkg.assets %>js/common/jquery-isolated-scroll.js'
'<%= pkg.assets %>js/application.js'
'<%= pkg.assets %>js/csv.js'
'<%= pkg.assets %>js/dashboard.js'
@@ -133,6 +134,7 @@ module.exports = (grunt) ->
'<%= pkg.assets %>js/common/inputs.js'
'<%= pkg.assets %>js/common/dialogs.js'
'<%= pkg.assets %>js/common/processes.js'
+ '<%= pkg.assets %>js/common/jquery-isolated-scroll.js'
'<%= pkg.assets %>js/application.js'
'<%= pkg.assets %>js/csv.js'
'<%= pkg.assets %>js/dashboard.js'
@@ -251,6 +253,9 @@ module.exports = (grunt) ->
'<%= pkg.assets %>js/templates/component-viewer.js': [
'<%= pkg.sources %>hbs/component-viewer/**/*.hbs'
]
+ '<%= pkg.assets %>js/templates/source-viewer.js': [
+ '<%= pkg.sources %>hbs/source-viewer/**/*.hbs'
+ ]
'<%= pkg.assets %>js/templates/issue.js': [
'<%= pkg.sources %>hbs/common/**/*.hbs'
'<%= pkg.sources %>hbs/issue/**/*.hbs'
@@ -316,6 +321,7 @@ module.exports = (grunt) ->
options:
test: true
verbose: true
+ 'fail-fast': true
src: ['<%= pkg.sources %>js/tests/e2e/tests/<%= grunt.option("spec") %>-spec.js']
diff --git a/server/sonar-web/src/main/coffee/common/overlay.coffee b/server/sonar-web/src/main/coffee/common/overlay.coffee
index 9abcf4d6d96..70574bef9ae 100644
--- a/server/sonar-web/src/main/coffee/common/overlay.coffee
+++ b/server/sonar-web/src/main/coffee/common/overlay.coffee
@@ -6,14 +6,6 @@ define [
$ = jQuery
- $.fn.isolatedScroll = ->
- @on 'wheel', (e) ->
- delta = -e.originalEvent.deltaY
- bottomOverflow = @scrollTop + $(@).outerHeight() - @scrollHeight >= 0
- topOverflow = @scrollTop <= 0
- e.preventDefault() if (delta < 0 && bottomOverflow) || (delta > 0 && topOverflow)
- @
-
class extends Marionette.ItemView
className: 'overlay-popup'
diff --git a/server/sonar-web/src/main/coffee/component-viewer/mixins/main-coverage.coffee b/server/sonar-web/src/main/coffee/component-viewer/mixins/main-coverage.coffee
index f0223d27486..ac7b29ffd9e 100644
--- a/server/sonar-web/src/main/coffee/component-viewer/mixins/main-coverage.coffee
+++ b/server/sonar-web/src/main/coffee/component-viewer/mixins/main-coverage.coffee
@@ -25,10 +25,13 @@ define [], () ->
testCases: c[2]
branches: c[3]
coveredBranches: c[4]
+ line.coverageStatus = 'partially-covered'
+ line.coverageStatus = 'covered' if c[1] && c[3] == c[4]
+ line.coverageStatus = 'uncovered' if !c[1] || c[4] == 0
if line.coverage.branches? && line.coverage.coveredBranches?
- line.coverage.branchCoverageStatus = 'green' if line.coverage.branches == line.coverage.coveredBranches
- line.coverage.branchCoverageStatus = 'orange' if line.coverage.branches > line.coverage.coveredBranches
- line.coverage.branchCoverageStatus = 'red' if line.coverage.coveredBranches == 0
+ line.coverage.branchCoverageStatus = 'covered' if line.coverage.branches == line.coverage.coveredBranches
+ line.coverage.branchCoverageStatus = 'partially-covered' if line.coverage.branches > line.coverage.coveredBranches
+ line.coverage.branchCoverageStatus = 'uncovered' if line.coverage.coveredBranches == 0
@source.set 'formattedSource', formattedSource
diff --git a/server/sonar-web/src/main/coffee/component-viewer/source.coffee b/server/sonar-web/src/main/coffee/component-viewer/source.coffee
index 17b0b11296d..9bdc668be20 100644
--- a/server/sonar-web/src/main/coffee/component-viewer/source.coffee
+++ b/server/sonar-web/src/main/coffee/component-viewer/source.coffee
@@ -41,11 +41,10 @@ define [
'click .js-line-actions': 'highlightLine'
- 'click .coverage-tests': 'showCoveragePopup'
+ 'click .source-line-covered': 'showCoveragePopup'
+ 'click .source-line-partially-covered': 'showCoveragePopup'
- 'click .duplication-exists': 'showDuplicationPopup'
- 'mouseenter .duplication-exists': 'duplicationMouseEnter'
- 'mouseleave .duplication-exists': 'duplicationMouseLeave'
+ 'click .source-line-duplications-extra': 'showDuplicationPopup'
'click .js-expand': 'expandBlock'
'click .js-expand-all': 'expandAll'
@@ -78,24 +77,36 @@ define [
renderExpandButtons: ->
- rows = @$('.row[data-line-number]')
+ rows = @$('.source-line[data-line-number]')
rows.get().forEach (row) =>
line = $(row).data 'line-number'
linePrev = $(row).prev('[data-line-number]').data 'line-number'
if line? && linePrev? && (linePrev + 1) < line
- expand = @expandTemplate from: linePrev, to: line, settings: @options.main.settings.toJSON()
+ expand = @expandTemplate
+ from: linePrev
+ to: line
+ settings: @options.main.settings.toJSON()
+ baseDuplications: @getBaseDuplications()
$(expand).insertBefore $(row)
firstShown = rows.first().data('line-number')
if firstShown > 1
- expand = @expandTemplate from: firstShown - EXPAND_LINES, to: firstShown, settings: @options.main.settings.toJSON()
+ expand = @expandTemplate
+ from: firstShown - EXPAND_LINES
+ to: firstShown
+ settings: @options.main.settings.toJSON()
+ baseDuplications: @getBaseDuplications()
$(expand).insertBefore rows.first()
lines = _.size @model.get 'source'
lines = Math.min lines, LINES_LIMIT
lastShown = rows.last().data('line-number')
if lastShown < lines
- expand = @expandTemplate from: lastShown, to: lines, settings: @options.main.settings.toJSON()
+ expand = @expandTemplate
+ from: lastShown
+ to: lines
+ settings: @options.main.settings.toJSON()
+ baseDuplications: @getBaseDuplications()
$(expand).insertAfter rows.last()
@delegateEvents()
@@ -114,8 +125,8 @@ define [
row = @$("##{@cid}-#{line}")
if row.length > 0
rendered += 1
- row.removeClass 'row-hidden'
- container = row.children('.line')
+ row.removeClass 'hidden'
+ container = row.children('.source-line-code')
container.addClass 'has-issues' if line > 0
if rendered < ISSUES_LIMIT
issueModel = new Issue issue
@@ -124,6 +135,7 @@ define [
if issues.length == 0
issues = $('<div class="issue-list"></div>').appendTo container
issueView.render().$el.appendTo issues
+ issueView.$el.prop('id', "issue-#{issue.key}").data('issue-key', issue.key)
issueView.on 'reset', =>
@updateIssue issueModel
@options.main.requestComponent(@options.main.key, false, false).done =>
@@ -155,12 +167,12 @@ define [
popup = new LineActionsPopupView
triggerEl: $(e.currentTarget)
main: @options.main
- row: $(e.currentTarget).closest '.row'
+ row: $(e.currentTarget).closest '.source-line'
popup.render()
highlightLine: (e) ->
- row = $(e.currentTarget).closest('.row')
+ row = $(e.currentTarget).closest('.source-line')
highlighted = row.is ".#{HIGHLIGHTED_ROW_CLASS}"
@$(".#{HIGHLIGHTED_ROW_CLASS}").removeClass HIGHLIGHTED_ROW_CLASS
@highlightedLine = null
@@ -195,7 +207,7 @@ define [
showCoveragePopup: (e) ->
e.stopPropagation()
$('body').click()
- line = $(e.currentTarget).closest('.row').data 'line-number'
+ line = $(e.currentTarget).closest('.source-line').data 'line-number'
$.get API_COVERAGE_TESTS, key: @options.main.component.get('key'), line: line, (data) =>
popup = new CoveragePopupView
model: new Backbone.Model data
@@ -308,6 +320,14 @@ define [
r
+ getBaseDuplications: ->
+ source = @model.get 'formattedSource'
+ baseDuplications = []
+ if source? && source.length > 0 && _.first(source).duplications?
+ baseDuplications = _.first(source).duplications
+ baseDuplications
+
+
serializeData: ->
uid: @cid
source: @prepareSource()
@@ -321,3 +341,4 @@ define [
issuesLimitReached: @model.get('activeIssues')?.length > ISSUES_LIMIT
linesLimit: LINES_LIMIT
linesLimitReached: _.size(@model.get 'source') > LINES_LIMIT
+ baseDuplications: @getBaseDuplications()
diff --git a/server/sonar-web/src/main/coffee/issue/issue-view.coffee b/server/sonar-web/src/main/coffee/issue/issue-view.coffee
index 2af05daf721..24ea55f22de 100644
--- a/server/sonar-web/src/main/coffee/issue/issue-view.coffee
+++ b/server/sonar-web/src/main/coffee/issue/issue-view.coffee
@@ -64,7 +64,6 @@ define [
onRender: ->
- @$el.prop('id', "issue-#{@model.id}").data('issue-key', @model.id)
resetIssue: (options, p) ->
diff --git a/server/sonar-web/src/main/coffee/issues/component-viewer/main.coffee b/server/sonar-web/src/main/coffee/issues/component-viewer/main.coffee
index 738826ecc0e..e3143ceebdd 100644
--- a/server/sonar-web/src/main/coffee/issues/component-viewer/main.coffee
+++ b/server/sonar-web/src/main/coffee/issues/component-viewer/main.coffee
@@ -2,48 +2,44 @@ define [
'backbone'
'backbone.marionette'
'templates/issues'
+ 'source-viewer/viewer'
'issues/models/issues'
'issues/component-viewer/issue-view'
], (
Backbone
Marionette
Templates
+ SourceViewer
Issues
IssueView
) ->
$ = jQuery
- API_SOURCES = "#{baseUrl}/api/sources/lines"
- LINES_AROUND = 200
TOP_OFFSET = 38
BOTTOM_OFFSET = 10
- ISSUES_LIMIT = 100
- class extends Marionette.ItemView
- template: Templates['issues-component-viewer']
+ class extends SourceViewer
-
- ui:
- sourceBeforeSpinner: '.js-component-viewer-source-before'
- sourceAfterSpinner: '.js-component-viewer-source-after'
-
-
- events:
- 'click .js-close-component-viewer': 'closeComponentViewer'
- 'click .sym': 'highlightUsages'
- 'click .code-issue': 'selectIssue'
+ events: ->
+ _.extend super,
+ 'click .js-close-component-viewer': 'closeComponentViewer'
+ 'click .sym': 'highlightUsages'
+ 'click .code-issue': 'selectIssue'
initialize: (options) ->
- @component = new Backbone.Model()
- @issues = new Issues()
- @issueViews = []
+ super
+ @listenTo @, 'loaded', @onLoaded
@listenTo options.app.state, 'change:selectedIndex', @select
- @loadSourceBeforeThrottled = _.throttle @loadSourceBefore, 1000
- @loadSourceAfterThrottled = _.throttle @loadSourceAfter, 1000
- @scrollTimer = null
+
+
+ onLoaded: ->
+ @bindScrollEvents()
+ @bindShortcuts()
+ if @baseIssue?
+ @scrollToLine @baseIssue.get 'line'
bindShortcuts: ->
@@ -85,106 +81,12 @@ define [
key.deleteScope 'componentViewer'
- bindScrollEvents: ->
- $(window).on 'scroll.issues-component-viewer', (=> @onScroll())
-
-
- unbindScrollEvents: ->
- $(window).off 'scroll.issues-component-viewer'
-
-
- disablePointerEvents: ->
- clearTimeout @scrollTimer
- $('body').addClass 'disabled-pointer-events'
- @scrollTimer = setTimeout (-> $('body').removeClass 'disabled-pointer-events'), 250
-
-
- onScroll: ->
- @disablePointerEvents()
-
- if @model.get('hasSourceBefore') && $(window).scrollTop() <= @ui.sourceBeforeSpinner.offset().top
- @loadSourceBeforeThrottled()
-
- if @model.get('hasSourceAfter') && $(window).scrollTop() + $(window).height() >= @ui.sourceAfterSpinner.offset().top
- @loadSourceAfterThrottled()
-
-
- loadSourceBefore: ->
- @unbindScrollEvents()
- source = @model.get 'source'
- firstLine = _.first(source).line
- @requestSources(firstLine - LINES_AROUND, firstLine - 1).done (data) =>
- source = data.sources.concat source
- @model.set
- source: source
- hasSourceBefore: data.sources.length == LINES_AROUND
- @render()
- @scrollToLine firstLine
- @bindScrollEvents() if @model.get('hasSourceBefore') || @model.get('hasSourceAfter')
-
-
- loadSourceAfter: ->
- @unbindScrollEvents()
- source = @model.get 'source'
- lastLine = _.last(source).line
- @requestSources(lastLine + 1, lastLine + LINES_AROUND)
- .done (data) =>
- source = source.concat data.sources
- @model.set
- source: source
- hasSourceAfter: data.sources.length == LINES_AROUND
- @render()
- @bindScrollEvents() if @model.get('hasSourceBefore') || @model.get('hasSourceAfter')
- .fail =>
- @model.set
- source: []
- hasSourceAfter: false
- @render()
-
-
onClose: ->
- @issueViews.forEach (view) -> view.close()
- @issuesView = []
+ super
@unbindScrollEvents()
@unbindShortcuts()
- onRender: ->
- @renderIssues()
-
-
- renderIssues: ->
- @hiddenIssues = false
- @issues.forEach (issue) =>
- @renderIssue issue
- if @hiddenIssues
- issues = @$('.issue').length
- warn = $('<div class="process-spinner shown">' + tp('issues.issues_limit_reached', issues) + '</div>')
- $('body').append warn
- setTimeout (-> warn.remove()), 3000
-
-
- addIssue: (issue) ->
- @renderIssue issue
-
-
- renderIssue: (issue) ->
- line = issue.get('line') || 0
- row = @$("[data-line-number=#{line}]")
- issueView = new IssueView app: @options.app, model: issue
- @issueViews.push issueView
- showBox = Math.abs(issue.get('index') - @model.get('issueIndex')) < ISSUES_LIMIT / 2
- @hiddenIssues = true unless showBox
- if showBox && row.length > 0
- row.removeClass 'hidden'
- line = row.find '.line'
- line.addClass 'has-issues'
- issues = line.find '.issue-list'
- if issues.length == 0
- issues = $('<div class="issue-list"></div>').appendTo line
- issueView.render().$el.appendTo issues
-
-
select: ->
selected = @options.app.state.get 'selectedIndex'
selectedIssue = @options.app.issues.at selected
@@ -231,49 +133,48 @@ define [
openFileByIssue: (issue) ->
+ @baseIssue = issue
componentKey = issue.get 'component'
componentUuid = issue.get 'componentUuid'
+ @open componentUuid, componentKey
+
+
+ linesLimit: ->
+ line = @LINES_LIMIT / 2
+ if @baseIssue? && @baseIssue.has('line')
+ line = Math.max line, @baseIssue.get('line')
+ from: line - @LINES_LIMIT / 2 + 1
+ to: line + @LINES_LIMIT / 2
- line = issue.get('line') || 0
- @model.set
- id: componentUuid
- key: componentKey
- issueLine: line
- issueIndex: issue.get 'index'
-
- @requestSources(line - LINES_AROUND, line + LINES_AROUND)
- .done (data) =>
- @model.set source: data.sources
- firstLine = _.first(data.sources).line
- lastLine = _.last(data.sources).line
- @model.set
- hasSourceBefore: firstLine > 1
- hasSourceAfter: lastLine == line + LINES_AROUND
- @requestIssues().done =>
- @issues.reset @options.app.issues.filter (issue) => issue.get('component') == componentKey
- @render()
- @bindScrollEvents()
- @bindShortcuts()
- @scrollToLine issue.get 'line'
- .fail =>
- @model.set source: []
- @model.set hasSourceBefore: false, hasSourceAfter: false
- @issues.reset @options.app.issues.filter (issue) => issue.get('component') == componentKey
- @render()
- @bindShortcuts()
- @scrollToLine issue.get 'line'
-
-
-
- requestSources: (lineFrom, lineTo) ->
- lineFrom = Math.max 0, lineFrom
- $.get API_SOURCES, uuid: @model.id, from: lineFrom, to: lineTo
+
+ limitIssues: (issues) ->
+ index = @ISSUES_LIMIT / 2
+ if @baseIssue? && @baseIssue.has('index')
+ index = Math.max index, @baseIssue.get('index')
+ x = issues.filter (issue) =>
+ Math.abs(issue.get('index') - index) <= @ISSUES_LIMIT / 2
+ x
requestIssues: ->
+ console.log 'Request issues'
if @options.app.issues.last().get('component') == @model.get('key')
- @options.app.controller.fetchNextPage()
- else $.Deferred().resolve().promise()
+ r = @options.app.controller.fetchNextPage()
+ else r = $.Deferred().resolve().promise()
+ r.done =>
+ @issues.reset @options.app.issues.filter (issue) => issue.get('component') == @model.key()
+ @issues.reset @limitIssues @issues
+ @addIssuesPerLineMeta @issues
+ console.log 'Issues loaded'
+
+
+ renderIssue: (issue) ->
+ issueView = new IssueView
+ el: '#issue-' + issue.get('key')
+ model: issue
+ app: @options.app
+ @issueViews.push issueView
+ issueView.render()
addNextIssuesPage: ->
@@ -298,8 +199,3 @@ define [
@$('.sym.highlighted').removeClass 'highlighted'
@$(".sym.#{key}").addClass 'highlighted' unless highlighted
-
- serializeData: ->
- hasSCM = _.some @model.get('source'), (row) -> row.scmAuthor?
- _.extend super,
- hasSCM: hasSCM
diff --git a/server/sonar-web/src/main/coffee/issues/controller.coffee b/server/sonar-web/src/main/coffee/issues/controller.coffee
index 6022dac9b9a..6d8d43b094f 100644
--- a/server/sonar-web/src/main/coffee/issues/controller.coffee
+++ b/server/sonar-web/src/main/coffee/issues/controller.coffee
@@ -202,9 +202,7 @@ define [
key.setScope 'componentViewer'
@options.app.issuesView.unbindScrollEvents()
@options.app.state.set 'component', @_prepareComponent(issue)
- @options.app.componentViewer = new ComponentViewer
- app: @options.app
- model: new ComponentViewerState()
+ @options.app.componentViewer = new ComponentViewer app: @options.app
@options.app.layout.workspaceComponentViewerRegion.show @options.app.componentViewer
@options.app.layout.showComponentViewer()
@options.app.componentViewer.openFileByIssue issue
diff --git a/server/sonar-web/src/main/coffee/issues/layout.coffee b/server/sonar-web/src/main/coffee/issues/layout.coffee
index 4e6d76e0fb6..a39d515384b 100644
--- a/server/sonar-web/src/main/coffee/issues/layout.coffee
+++ b/server/sonar-web/src/main/coffee/issues/layout.coffee
@@ -8,14 +8,6 @@ define [
$ = jQuery
- $.fn.isolatedScroll = ->
- @on 'wheel', (e) ->
- delta = -e.originalEvent.deltaY
- bottomOverflow = @scrollTop + $(@).outerHeight() - @scrollHeight >= 0
- topOverflow = @scrollTop <= 0
- e.preventDefault() if (delta < 0 && bottomOverflow) || (delta > 0 && topOverflow)
- @
-
class extends Marionette.Layout
template: Templates['issues-layout']
diff --git a/server/sonar-web/src/main/coffee/source-viewer/popups/coverage-popup.coffee b/server/sonar-web/src/main/coffee/source-viewer/popups/coverage-popup.coffee
new file mode 100644
index 00000000000..66b97c2addb
--- /dev/null
+++ b/server/sonar-web/src/main/coffee/source-viewer/popups/coverage-popup.coffee
@@ -0,0 +1,42 @@
+define [
+ 'backbone.marionette'
+ 'templates/source-viewer'
+ 'common/popup'
+ 'component-viewer/utils'
+], (
+ Marionette
+ Templates
+ Popup
+ utils
+) ->
+
+ $ = jQuery
+
+
+ class extends Popup
+ template: Templates['source-viewer-coverage-popup']
+
+
+ events:
+ 'click a[data-key]': 'goToFile'
+
+
+ onRender: ->
+ super
+ @$('.bubble-popup-container').isolatedScroll()
+
+
+ goToFile: (e) ->
+ el = $(e.currentTarget)
+ key = el.data 'key'
+ method = el.data 'method'
+ files = @model.get 'files'
+
+
+ serializeData: ->
+ files = @model.get 'files'
+ tests = _.groupBy @model.get('tests'), '_ref'
+ testFiles = _.map tests, (testSet, fileRef) ->
+ file: files[fileRef]
+ tests: testSet
+ testFiles: testFiles
diff --git a/server/sonar-web/src/main/coffee/source-viewer/popups/duplication-popup.coffee b/server/sonar-web/src/main/coffee/source-viewer/popups/duplication-popup.coffee
new file mode 100644
index 00000000000..378ae8d8022
--- /dev/null
+++ b/server/sonar-web/src/main/coffee/source-viewer/popups/duplication-popup.coffee
@@ -0,0 +1,55 @@
+define [
+ 'backbone.marionette'
+ 'templates/component-viewer'
+ 'common/popup'
+ 'component-viewer/utils'
+], (
+ Marionette
+ Templates
+ Popup
+ utils
+) ->
+
+ $ = jQuery
+
+
+ class extends Popup
+ template: Templates['source-viewer-duplication-popup']
+
+
+ events:
+ 'click a[data-key]': 'goToFile'
+
+
+ goToFile: (e) ->
+ key = $(e.currentTarget).data 'key'
+ line = $(e.currentTarget).data 'line'
+ files = @options.main.source.get('duplicationFiles')
+ options = @collection.map (item) ->
+ file = files[item.get('_ref')]
+ x = utils.splitLongName file.name
+ key: file.key
+ name: x.name
+ subname: x.dir
+ component:
+ projectName: file.projectName
+ subProjectName: file.subProjectName
+ active: file.key == key
+ options = _.uniq options, (item) -> item.key
+
+
+ serializeData: ->
+ files = @model.get 'duplicationFiles'
+ groupedBlocks = _.groupBy @collection.toJSON(), '_ref'
+ duplications = _.map groupedBlocks, (blocks, fileRef) ->
+ blocks: blocks
+ file: files[fileRef]
+
+ duplications = _.sortBy duplications, (d) =>
+ a = d.file.projectName != @model.get 'projectName'
+ b = d.file.subProjectName != @model.get 'subProjectName'
+ c = d.file.key != @model.get 'key'
+ '' + a + b + c
+
+ component: @model.toJSON()
+ duplications: duplications
diff --git a/server/sonar-web/src/main/coffee/source-viewer/popups/line-actions-popup.coffee b/server/sonar-web/src/main/coffee/source-viewer/popups/line-actions-popup.coffee
new file mode 100644
index 00000000000..f1e96f0d454
--- /dev/null
+++ b/server/sonar-web/src/main/coffee/source-viewer/popups/line-actions-popup.coffee
@@ -0,0 +1,40 @@
+define [
+ 'backbone.marionette'
+ 'templates/source-viewer'
+ 'common/popup'
+ 'issue/manual-issue-view'
+], (
+ Marionette
+ Templates
+ Popup
+ ManualIssueView
+) ->
+
+
+ class extends Popup
+ template: Templates['source-viewer-line-options-popup']
+
+
+ events:
+ 'click .js-get-permalink': 'getPermalink'
+ 'click .js-add-manual-issue': 'addManualIssue'
+
+
+ getPermalink: (e) ->
+ e.preventDefault()
+ url = "#{baseUrl}/component/index#component=#{encodeURIComponent(@model.key())}&line=#{@options.line}"
+ windowParams = 'resizable=1,scrollbars=1,status=1'
+ window.open url, @model.get('name'), windowParams
+
+
+ addManualIssue: (e) ->
+ e.preventDefault()
+ line = @options.line
+ component = @model.key()
+ manualIssueView = new ManualIssueView
+ line: line
+ component: component
+ rules: @model.get 'manual_rules'
+ manualIssueView.render().$el.appendTo @options.row.find('.source-line-code')
+ manualIssueView.on 'add', (issue) =>
+ @trigger 'onManualIssueAdded', issue
diff --git a/server/sonar-web/src/main/coffee/source-viewer/source.coffee b/server/sonar-web/src/main/coffee/source-viewer/source.coffee
new file mode 100644
index 00000000000..7badb90d1ec
--- /dev/null
+++ b/server/sonar-web/src/main/coffee/source-viewer/source.coffee
@@ -0,0 +1,51 @@
+define [
+ 'backbone'
+], (
+ Backbone
+) ->
+
+ class extends Backbone.Model
+ idAttribute: 'uuid'
+
+ defaults: ->
+ hasSource: false
+ hasCoverage: false
+ hasDuplications: false
+ hasSCM: false
+
+
+ key: ->
+ @get 'key'
+
+
+ addMeta: (meta) ->
+ source = @get 'source'
+ metaIdx = 0
+ metaLine = meta[metaIdx]
+ source.forEach (line) ->
+ while metaLine? && line.line > metaLine.line
+ metaIdx++
+ metaLine = meta[metaIdx]
+ if metaLine? && line.line == metaLine.line
+ _.extend line, metaLine
+ metaIdx++
+ metaLine = meta[metaIdx]
+ @set source: source
+
+
+ addDuplications: (duplications) ->
+ source = @get 'source'
+ return unless source
+ source.forEach (line) ->
+ lineDuplications = []
+ duplications.forEach (d, i) ->
+ duplicated = false
+ d.blocks.forEach (b) ->
+ if b._ref == '1'
+ lineFrom = b.from
+ lineTo = b.from + b.size
+ duplicated = true if line.line >= lineFrom && line.line <= lineTo
+ lineDuplications.push if duplicated then i + 1 else false
+ line.duplications = lineDuplications
+ @set source: source
+
diff --git a/server/sonar-web/src/main/coffee/source-viewer/viewer.coffee b/server/sonar-web/src/main/coffee/source-viewer/viewer.coffee
new file mode 100644
index 00000000000..17d94c95602
--- /dev/null
+++ b/server/sonar-web/src/main/coffee/source-viewer/viewer.coffee
@@ -0,0 +1,360 @@
+define [
+ 'backbone.marionette'
+ 'templates/source-viewer'
+ 'source-viewer/source'
+ 'issue/models/issue'
+ 'issue/collections/issues'
+ 'issue/issue-view'
+
+ 'source-viewer/popups/coverage-popup'
+ 'source-viewer/popups/duplication-popup'
+ 'source-viewer/popups/line-actions-popup'
+], (
+ Marionette
+ Templates
+ Source
+ Issue
+ Issues
+ IssueView
+
+ CoveragePopupView
+ DuplicationPopupView
+ LineActionsPopupView
+) ->
+
+ $ = jQuery
+
+ HIGHLIGHTED_ROW_CLASS = 'source-line-highlighted'
+
+ log = (message) ->
+ console.log 'Source Viewer:', message
+
+
+ class extends Marionette.ItemView
+ className: 'source'
+ template: Templates['source-viewer']
+
+ ISSUES_LIMIT: 100
+ LINES_LIMIT: 1000
+ LINES_AROUND: 500
+
+
+ ui:
+ sourceBeforeSpinner: '.js-component-viewer-source-before'
+ sourceAfterSpinner: '.js-component-viewer-source-after'
+
+
+ events: ->
+ 'click .source-line-covered': 'showCoveragePopup'
+ 'click .source-line-partially-covered': 'showCoveragePopup'
+
+ 'click .source-line-duplications': 'showDuplications'
+ 'click .source-line-duplications-extra': 'showDuplicationPopup'
+
+ 'click .source-line-number[data-line-number]': 'highlightLine'
+
+
+ initialize: ->
+ @model = new Source() unless @model?
+ @issues = new Issues()
+ @issueViews = []
+ @loadSourceBeforeThrottled = _.throttle @loadSourceBefore, 1000
+ @loadSourceAfterThrottled = _.throttle @loadSourceAfter, 1000
+ @scrollTimer = null
+
+
+ onRender: ->
+ log 'Render'
+ @renderIssues()
+
+
+ onClose: ->
+ @issueViews.forEach (view) -> view.close()
+ @issueViews = []
+
+
+ open: (id, key) ->
+ @model.clear()
+ @model.set uuid: id, key: key
+ @requestComponent().done =>
+ @requestSource()
+ .done =>
+ @requestCoverage().done =>
+ @requestDuplications().done =>
+ @requestIssues().done =>
+ @render()
+ @trigger 'loaded'
+ .fail =>
+ @model.set source: [{ line: 0 }]
+ @requestIssues().done =>
+ @render()
+ @trigger 'loaded'
+
+
+ requestComponent: ->
+ log 'Request component details...'
+ url = "#{baseUrl}/api/components/app"
+ options = key: @model.key()
+ $.get url, options, (data) =>
+ @model.set data
+ log 'Component loaded'
+
+
+ linesLimit: ->
+ from: 1
+ to: @LINES_LIMIT
+
+
+ requestSource: ->
+ log 'Request source...'
+ url = "#{baseUrl}/api/sources/lines"
+ options = _.extend { uuid: @model.id }, @linesLimit()
+ $.get url, options, (data) =>
+ source = data.sources || []
+ if source.length == 0 || (source.length > 0 && _.first(source).line == 1)
+ source.unshift { line: 0 }
+ firstLine = _.first(source).line
+ @model.set
+ source: source
+ hasSourceBefore: firstLine > 1
+ hasSourceAfter: true
+ log 'Source loaded'
+
+
+ requestCoverage: ->
+ log 'Request coverage'
+ url = "#{baseUrl}/api/coverage/show"
+ options = key: @model.key()
+ $.get url, options, (data) =>
+ hasCoverage = data? && data.coverage?
+ @model.set hasCoverage: hasCoverage
+ if hasCoverage
+ coverage = data.coverage.map (c) ->
+ status = 'partially-covered'
+ status = 'covered' if c[1] && c[3] == c[4]
+ status = 'uncovered' if !c[1] || c[4] == 0
+ line: +c[0]
+ covered: status
+ else coverage = []
+ @model.addMeta coverage
+ log 'Coverage loaded'
+
+
+ requestDuplications: ->
+ log 'Request duplications'
+ url = "#{baseUrl}/api/duplications/show"
+ options = key: @model.key()
+ $.get url, options, (data) =>
+ hasDuplications = data? && data.duplications?
+ if hasDuplications
+ duplications = {}
+ data.duplications.forEach (d, i) ->
+ d.blocks.forEach (b) ->
+ if b._ref == '1'
+ lineFrom = b.from
+ lineTo = b.from + b.size
+ duplications[i] = true for i in [lineFrom..lineTo]
+ duplications = _.pairs(duplications).map (line) ->
+ line: +line[0]
+ duplicated: line[1]
+ else duplications = []
+ @model.addMeta duplications
+ @model.addDuplications data.duplications
+ @model.set
+ duplications: data.duplications
+ duplicationFiles: data.files
+ log 'Duplications loaded'
+
+
+ requestIssues: ->
+ log 'Request issues'
+ options = data:
+ componentUuids: @model.id
+ extra_fields: 'actions,transitions,assigneeName,actionPlanName'
+ resolved: false
+ s: 'FILE_LINE'
+ asc: true
+ @issues.fetch(options).done =>
+ @issues.reset @limitIssues @issues
+ @addIssuesPerLineMeta @issues
+ log 'Issues loaded'
+
+
+ addIssuesPerLineMeta: (issues) ->
+ lines = {}
+ issues.forEach (issue) ->
+ line = issue.get('line') || 0
+ lines[line] = [] unless _.isArray lines[line]
+ lines[line].push issue.toJSON()
+ issuesPerLine = _.pairs(lines).map (line) ->
+ line: +line[0]
+ issues: line[1]
+ @model.addMeta issuesPerLine
+
+
+ limitIssues: (issues) ->
+ issues.first @ISSUES_LIMIT
+
+
+ renderIssues: ->
+ log 'Render issues'
+ @issues.forEach @renderIssue, @
+ log 'Issues rendered'
+
+
+ renderIssue: (issue) ->
+ issueView = new IssueView
+ el: '#issue-' + issue.get('key')
+ model: issue
+ @issueViews.push issueView
+ issueView.render()
+
+
+ addIssue: (issue) ->
+ line = issue.get('line') || 0
+ code = @$(".source-line-code[data-line-number=#{line}]")
+ issueList = code.find('.issue-list')
+ unless issueList.length > 0
+ issueList = $('<div class="issue-list"></div>')
+ code.append issueList
+ issueList.append "<div class=\"issue\" id=\"issue-#{issue.id}\"></div>"
+ @renderIssue issue
+
+
+ showSpinner: ->
+ hideSpinner: ->
+ resetShowBlocks: ->
+
+
+ showCoveragePopup: (e) ->
+ r = window.process.addBackgroundProcess()
+ e.stopPropagation()
+ $('body').click()
+ line = $(e.currentTarget).data 'line-number'
+ url = "#{baseUrl}/api/tests/test_cases"
+ options =
+ key: @model.key()
+ line: line
+ $.get url, options
+ .done (data) =>
+ popup = new CoveragePopupView
+ model: new Backbone.Model data
+ triggerEl: $(e.currentTarget)
+ popup.render()
+ window.process.finishBackgroundProcess r
+ .fail ->
+ window.process.failBackgroundProcess r
+
+
+ showDuplications: ->
+ @$('.source-line-duplications').addClass 'hidden'
+ @$('.source-line-duplications-extra').removeClass 'hidden'
+
+
+ showDuplicationPopup: (e) ->
+ e.stopPropagation()
+ $('body').click()
+ index = $(e.currentTarget).data 'index'
+ line = $(e.currentTarget).data 'line-number'
+ blocks = @model.get('duplications')[index - 1].blocks
+ blocks = _.filter blocks, (b) ->
+ (b._ref != '1') || (b._ref == '1' && b.from > line) || (b._ref == '1' && b.from + b.size < line)
+ popup = new DuplicationPopupView
+ triggerEl: $(e.currentTarget)
+ model: @model
+ collection: new Backbone.Collection blocks
+ popup.render()
+
+
+ showLineActionsPopup: (e) ->
+ e.stopPropagation()
+ $('body').click()
+ line = $(e.currentTarget).data 'line-number'
+ popup = new LineActionsPopupView
+ triggerEl: $(e.currentTarget)
+ model: @model
+ line: line
+ row: $(e.currentTarget).closest '.source-line'
+ popup.on 'onManualIssueAdded', (data) =>
+ @addIssue new Issue(data)
+ popup.render()
+
+
+ highlightLine: (e) ->
+ row = $(e.currentTarget).closest('.source-line')
+ highlighted = row.is ".#{HIGHLIGHTED_ROW_CLASS}"
+ @$(".#{HIGHLIGHTED_ROW_CLASS}").removeClass HIGHLIGHTED_ROW_CLASS
+ unless highlighted
+ row.addClass HIGHLIGHTED_ROW_CLASS
+ @showLineActionsPopup(e)
+
+
+ bindScrollEvents: ->
+ @$el.scrollParent().on 'scroll.source-viewer', (=> @onScroll())
+
+
+ unbindScrollEvents: ->
+ @$el.scrollParent().off 'scroll.source-viewer'
+
+
+ disablePointerEvents: ->
+ clearTimeout @scrollTimer
+ $('body').addClass 'disabled-pointer-events'
+ @scrollTimer = setTimeout (-> $('body').removeClass 'disabled-pointer-events'), 250
+
+
+ onScroll: ->
+ @disablePointerEvents()
+
+ p = @$el.scrollParent()
+ p = $(window) if p.is(document)
+ pTopOffset = if p.offset()? then p.offset().top else 0
+ if @model.get('hasSourceBefore') && (p.scrollTop() + pTopOffset <= @ui.sourceBeforeSpinner.offset().top)
+ @loadSourceBeforeThrottled()
+
+ if @model.get('hasSourceAfter') && (p.scrollTop() + pTopOffset + p.height() >= @ui.sourceAfterSpinner.offset().top)
+ @loadSourceAfterThrottled()
+
+
+ loadSourceBefore: ->
+ @unbindScrollEvents()
+ source = @model.get 'source'
+ firstLine = _.first(source).line
+ url = "#{baseUrl}/api/sources/lines"
+ options =
+ uuid: @model.id
+ from: firstLine - @LINES_AROUND
+ to: firstLine - 1
+ $.get url, options, (data) =>
+ source = (data.sources || []).concat source
+ if source.length == 0 || (source.length > 0 && _.first(source).line == 1)
+ source.unshift { line: 0 }
+ @model.set
+ source: source
+ hasSourceBefore: data.sources.length == @LINES_AROUND
+ @render()
+ @scrollToLine firstLine
+ @bindScrollEvents() if @model.get('hasSourceBefore') || @model.get('hasSourceAfter')
+
+
+ loadSourceAfter: ->
+ @unbindScrollEvents()
+ source = @model.get 'source'
+ lastLine = _.last(source).line
+ url = "#{baseUrl}/api/sources/lines"
+ options =
+ uuid: @model.id
+ from: lastLine + 1
+ to: lastLine + @LINES_AROUND
+ $.get url, options
+ .done (data) =>
+ source = source.concat data.sources
+ @model.set
+ source: source
+ hasSourceAfter: data.sources.length == @LINES_AROUND
+ @render()
+ @bindScrollEvents() if @model.get('hasSourceBefore') || @model.get('hasSourceAfter')
+ .fail =>
+ @model.set hasSourceAfter: false
+ @render()
+ @bindScrollEvents() if @model.get('hasSourceBefore') || @model.get('hasSourceAfter')
diff --git a/server/sonar-web/src/main/hbs/component-viewer/cw-code-expand.hbs b/server/sonar-web/src/main/hbs/component-viewer/cw-code-expand.hbs
index 0bfd7428146..10d494cd695 100644
--- a/server/sonar-web/src/main/hbs/component-viewer/cw-code-expand.hbs
+++ b/server/sonar-web/src/main/hbs/component-viewer/cw-code-expand.hbs
@@ -1,16 +1,17 @@
-<tr class="row row-expand">
+<tr class="source-line source-line-expand">
+ <td class="source-meta source-line-number"></td>
+ {{#if settings.scm}}
+ <td class="source-meta source-line-scm"></td>
+ {{/if}}
{{#if settings.coverage}}
- <td class="stat coverage-tests"></td>
- <td class="stat coverage-conditions"></td>
+ <td class="source-meta source-line-coverage"></td>
{{/if}}
{{#if settings.duplications}}
- <td class="stat"></td>
- {{/if}}
- {{#if settings.scm}}
- <td class="stat"></td>
+ {{#each baseDuplications}}
+ <td class="source-meta source-line-duplications-extra"></td>
+ {{/each}}
{{/if}}
- <td class="stat lid">
+ <td class="source-line-code">
<button class="button-clean js-expand" data-from="{{from}}" data-to="{{to}}"><i class="icon-expand"></i></button>
</td>
- <td class="line"></td>
</tr>
diff --git a/server/sonar-web/src/main/hbs/component-viewer/cw-source.hbs b/server/sonar-web/src/main/hbs/component-viewer/cw-source.hbs
index 2498a590d5c..47a371da8df 100644
--- a/server/sonar-web/src/main/hbs/component-viewer/cw-source.hbs
+++ b/server/sonar-web/src/main/hbs/component-viewer/cw-source.hbs
@@ -16,72 +16,70 @@
<p class="marginbottom10 js-duplications-in-deleted-files">{{t 'duplications.dups_found_on_deleted_resource'}}</p>
{{/if}}
- <table class="code">
- {{#if showZeroLine}}
- <tr class="row row-hidden" data-line-number="0" id="{{uid}}-0">
- {{#if settings.coverage}}
- <td class="stat coverage-tests"></td>
- <td class="stat coverage-conditions"></td>
- {{/if}}
- {{#if settings.duplications}}
- <td class="stat"></td>
- {{/if}}
- {{#if settings.scm}}
- <td class="stat"></td>
- {{/if}}
- <td class="stat lid"></td>
- <td class="line"></td>
- </tr>
- {{/if}}
-
- {{#each source}}
- {{#if show}}
- <tr class="row" data-line-number="{{lineNumber}}" id="{{../../uid}}-{{lineNumber}}">
-
- <td class="stat lid js-line-actions" title="{{t 'component_viewer.line_actions'}}">{{lineNumber}}</td>
-
- {{#if ../../settings.coverage}}
- <td class="stat {{#if coverage}}coverage-{{#if coverage.covered}}green{{else}}red{{/if}}{{/if}}">
- {{#if coverage}}
- <span class="coverage-tests" title="{{tp 'coverage_viewer.line_covered_by_x_tests' coverage.testCases}}">
- {{coverage.testCases}}
- </span>
- {{/if}}
- </td>
+ <div class="source">
+ <table class="source-table">
+ {{#if showZeroLine}}
+ <tr class="source-line hidden" data-line-number="0" id="{{uid}}-0">
+ <td class="source-meta source-line-number" title="{{t 'component_viewer.line_actions'}}"></td>
- <td class="stat {{#if coverage}}{{#if coverage.branchCoverageStatus}}coverage-{{coverage.branchCoverageStatus}}{{/if}}{{/if}}">
- {{#if coverage}}
- {{#if coverage.branches}}
- <span class="coverage-branches" title="{{tp 'coverage_viewer.x_covered_conditions' coverage.coveredBranches}}">
- {{coverage.coveredBranches}}/{{coverage.branches}}
- </span>
- {{/if}}
- {{/if}}
- </td>
+ {{#if settings.scm}}
+ <td class="source-meta source-line-scm"></td>
{{/if}}
- {{#if ../../settings.duplications}}
- <td class="stat duplications">
- {{#each duplications}}
- <span class="duplication {{#if this}}duplication-exists{{/if}}" data-index="{{this}}"></span>
- {{/each}}
+ {{#if settings.coverage}}
+ <td class="source-meta source-line-coverage">
+ <div class="source-line-bar"></div>
</td>
{{/if}}
- {{#if ../../settings.scm}}
- <td class="stat {{#if scm}}scm{{/if}}">
- {{#if scm}}
- {{#ifSCMChanged ../../../../source ../../../lineNumber}}
- <span class="scm-author" title="{{dt scm.date}}&#013;{{scm.author}}">{{scm.author}}</span>
- {{/ifSCMChanged}}
- {{/if}}
- </td>
+ {{#if settings.duplications}}
+ {{#each baseDuplications}}
+ <td class="source-meta source-line-duplications-extra">
+ <div class="source-line-bar"></div>
+ </td>
+ {{/each}}
{{/if}}
- <td class="line"><pre>{{{code}}}</pre></td>
+ <td class="source-line-code"></td>
</tr>
{{/if}}
- {{/each}}
- </table>
+
+ {{#each source}}
+ {{#if show}}
+ <tr class="source-line" data-line-number="{{lineNumber}}" id="{{../../uid}}-{{lineNumber}}">
+ <td class="source-meta source-line-number js-line-actions" title="{{t 'component_viewer.line_actions'}}"
+ {{#if lineNumber}}data-line-number="{{lineNumber}}"{{/if}}></td>
+
+ {{#if ../../settings.scm}}
+ <td class="source-meta {{#if scm}}source-line-scm{{/if}}">
+ {{#if scm}}
+ {{#ifSCMChanged ../../../../source ../../../lineNumber}}
+ <div class="source-line-scm-inner" title="{{scm.author}}&#013;{{dt scm.date}}" data-author="{{scm.author}}"></div>
+ {{/ifSCMChanged}}
+ {{/if}}
+ </td>
+ {{/if}}
+
+ {{#if ../../settings.coverage}}
+ <td class="source-meta source-line-coverage {{#if coverage}}source-line-{{coverageStatus}}{{/if}}">
+ <div class="source-line-bar"></div>
+ </td>
+ {{/if}}
+
+ {{#if ../../settings.duplications}}
+ {{#each duplications}}
+ <td class="source-meta source-line-duplications-extra {{#if this}}source-line-duplicated{{/if}}"
+ data-index="{{this}}" data-line-number="{{line}}">
+ <div class="source-line-bar"></div>
+ </td>
+ {{/each}}
+ {{/if}}
+
+ <td class="source-line-code code"><pre>{{{code}}}</pre></td>
+ </tr>
+ {{/if}}
+ {{/each}}
+ </table>
+ </div>
{{/if}}
diff --git a/server/sonar-web/src/main/hbs/source-viewer/source-viewer-coverage-popup.hbs b/server/sonar-web/src/main/hbs/source-viewer/source-viewer-coverage-popup.hbs
new file mode 100644
index 00000000000..be7d99c755a
--- /dev/null
+++ b/server/sonar-web/src/main/hbs/source-viewer/source-viewer-coverage-popup.hbs
@@ -0,0 +1,27 @@
+<div class="bubble-popup-container">
+ <div class="bubble-popup-title">{{t 'component_viewer.transition.coverage'}}</div>
+
+ {{#each testFiles}}
+ <div class="bubble-popup-section">
+ <a class="component-viewer-popup-test-file link-action" data-key="{{file.key}}" title="{{file.longName}}">
+ {{file.longName}}
+ </a>
+ <ul class="bubble-popup-list">
+ {{#each tests}}
+ <li class="component-viewer-popup-test" title="{{name}}">
+ <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}}">
+ {{name}}
+ </a>
+ </span>
+ <span class="component-viewer-popup-test-duration">{{durationInMs}}ms</span>
+ </li>
+ {{/each}}
+ </ul>
+ </div>
+ {{/each}}
+</div>
+
+<div class="bubble-popup-arrow"></div>
diff --git a/server/sonar-web/src/main/hbs/source-viewer/source-viewer-duplication-popup.hbs b/server/sonar-web/src/main/hbs/source-viewer/source-viewer-duplication-popup.hbs
new file mode 100644
index 00000000000..bf134d7fd62
--- /dev/null
+++ b/server/sonar-web/src/main/hbs/source-viewer/source-viewer-duplication-popup.hbs
@@ -0,0 +1,31 @@
+<div class="bubble-popup-container">
+ <div class="bubble-popup-title">{{t 'component_viewer.transition.duplication'}}</div>
+ {{#each duplications}}
+ <div class="bubble-popup-section">
+ {{#notEqComponents file ../component}}
+ <div class="component-viewer-popup-label" title="{{projectFullName file}}">
+ <i class="icon-qualifier-trk"></i> {{projectFullName file}}
+ </div>
+ {{/notEqComponents}}
+
+ {{#notEq file.key ../component.key}}
+ <a class="link-action" data-key="{{file.key}}" title="{{file.name}}">
+ {{file.name}}
+ </a>
+ {{/notEq}}
+
+ <div class="component-viewer-popup-duplications">
+ Lines:
+ {{#joinEach blocks ','}}
+ <a class="link-action" data-key="{{../file.key}}" data-line="{{from}}">
+ {{from}} – {{sum from size}}
+ </a>
+ {{/joinEach}}
+ </div>
+ </div>
+ {{else}}
+ {{t 'duplications.block_was_duplicated_by_a_deleted_resource'}}
+ {{/each}}
+</div>
+
+<div class="bubble-popup-arrow"></div>
diff --git a/server/sonar-web/src/main/hbs/source-viewer/source-viewer-line-options-popup.hbs b/server/sonar-web/src/main/hbs/source-viewer/source-viewer-line-options-popup.hbs
new file mode 100644
index 00000000000..facf64020ea
--- /dev/null
+++ b/server/sonar-web/src/main/hbs/source-viewer/source-viewer-line-options-popup.hbs
@@ -0,0 +1,13 @@
+<div class="bubble-popup-container">
+ <div class="bubble-popup-section">
+ <a href="#" class="js-get-permalink link-action">{{t 'component_viewer.get_permalink'}}</a>
+ </div>
+
+ {{#if canCreateManualIssue}}
+ <div class="bubble-popup-section">
+ <a href="#" class="js-add-manual-issue link-action">{{t 'component_viewer.add_manual_issue'}}</a>
+ </div>
+ {{/if}}
+</div>
+
+<div class="bubble-popup-arrow"></div>
diff --git a/server/sonar-web/src/main/hbs/source-viewer/source-viewer.hbs b/server/sonar-web/src/main/hbs/source-viewer/source-viewer.hbs
new file mode 100644
index 00000000000..3d56acd24a2
--- /dev/null
+++ b/server/sonar-web/src/main/hbs/source-viewer/source-viewer.hbs
@@ -0,0 +1,51 @@
+{{#if hasSourceBefore}}
+ <i class="spinner js-component-viewer-source-before"></i>
+{{/if}}
+
+<table class="source-table">
+ {{#each source}}
+ <tr class="source-line {{#eq line 0}}{{#empty issues}}hidden{{/empty}}{{/eq}}">
+ <td class="source-meta source-line-number" {{#if line}}data-line-number="{{line}}"{{/if}}></td>
+
+ <td class="source-meta source-line-scm">
+ {{#ifSCMChanged2 ../source line}}
+ <div class="source-line-scm-inner" title="{{scmAuthor}} {{scmDate}}" data-author="{{scmAuthor}}"></div>
+ {{/ifSCMChanged2}}
+ </td>
+
+ <td class="source-meta source-line-coverage {{#notNull covered}}source-line-{{covered}}{{/notNull}}"
+ data-line-number="{{line}}">
+ <div class="source-line-bar"></div>
+ </td>
+
+ <td class="source-meta source-line-duplications {{#if duplicated}}source-line-duplicated{{/if}}">
+ <div class="source-line-bar"></div>
+ </td>
+
+ {{#each duplications}}
+ <td class="source-meta source-line-duplications-extra hidden {{#if this}}source-line-duplicated{{/if}}"
+ data-index="{{this}}" data-line-number="{{line}}">
+ <div class="source-line-bar"></div>
+ </td>
+ {{/each}}
+
+ <td class="source-line-code" data-line-number="{{line}}">
+ {{#notNull code}}
+ <pre>{{#if code}}{{{code}}}{{else}}&nbsp;{{/if}}</pre>
+ {{/notNull}}
+
+ {{#notEmpty issues}}
+ <div class="issue-list">
+ {{#each issues}}
+ <div class="issue" id="issue-{{key}}"></div>
+ {{/each}}
+ </div>
+ {{/notEmpty}}
+ </td>
+ </tr>
+ {{/each}}
+</table>
+
+{{#if hasSourceAfter}}
+ <i class="spinner js-component-viewer-source-after"></i>
+{{/if}}
diff --git a/server/sonar-web/src/main/js/common/handlebars-extensions.js b/server/sonar-web/src/main/js/common/handlebars-extensions.js
index 5f19ef85298..2c851270ee9 100644
--- a/server/sonar-web/src/main/js/common/handlebars-extensions.js
+++ b/server/sonar-web/src/main/js/common/handlebars-extensions.js
@@ -147,6 +147,11 @@ define(['handlebars'], function (Handlebars) {
return cond ? options.fn(this) : options.inverse(this);
});
+ Handlebars.registerHelper('empty', function(array, options) {
+ var cond = _.isArray(array) && array.length > 0;
+ return cond ? options.inverse(this) : options.fn(this);
+ });
+
Handlebars.registerHelper('all', function() {
var args = Array.prototype.slice.call(arguments, 0, -1),
options = arguments[arguments.length - 1],
diff --git a/server/sonar-web/src/main/js/common/jquery-isolated-scroll.js b/server/sonar-web/src/main/js/common/jquery-isolated-scroll.js
new file mode 100644
index 00000000000..1eeb1c4b1fc
--- /dev/null
+++ b/server/sonar-web/src/main/js/common/jquery-isolated-scroll.js
@@ -0,0 +1,14 @@
+;(function ($) {
+
+ $.fn.isolatedScroll = function () {
+ this.on('wheel', function (e) {
+ var delta = -e.originalEvent.deltaY;
+ var bottomOverflow = this.scrollTop + $(this).outerHeight() - this.scrollHeight >= 0;
+ var topOverflow = this.scrollTop <= 0;
+ if ((delta < 0 && bottomOverflow) || (delta > 0 && topOverflow)) {
+ e.preventDefault();
+ }
+ });
+ };
+
+})(window.jQuery);
diff --git a/server/sonar-web/src/main/js/tests/e2e/tests/component-viewer-coverage-spec.js b/server/sonar-web/src/main/js/tests/e2e/tests/component-viewer-coverage-spec.js
index fe396025304..bac92d8b5d1 100644
--- a/server/sonar-web/src/main/js/tests/e2e/tests/component-viewer-coverage-spec.js
+++ b/server/sonar-web/src/main/js/tests/e2e/tests/component-viewer-coverage-spec.js
@@ -17,7 +17,7 @@ casper.test.begin(testName('Coverage Filters'), function (test) {
})
.then(function () {
- casper.waitForSelector('.component-viewer-source .row');
+ casper.waitForSelector('.component-viewer-source .source-line');
})
.then(function () {
@@ -27,41 +27,41 @@ casper.test.begin(testName('Coverage Filters'), function (test) {
.then(function () {
casper.click('.js-filter-lines-to-cover');
- casper.waitForSelector('.coverage-green', function () {
- test.assertElementCount('.coverage-green', 149);
- test.assertElementCount('.coverage-red', 51);
- test.assertElementCount('.coverage-orange', 2);
- test.assertElementCount('.component-viewer-source .row', 369);
+ casper.waitForSelector('.source-line-covered', function () {
+ test.assertElementCount('.source-line-covered', 142);
+ test.assertElementCount('.source-line-uncovered', 50);
+ test.assertElementCount('.source-line-partially-covered', 2);
+ test.assertElementCount('.component-viewer-source .source-line', 369);
});
})
.then(function () {
casper.click('.js-filter-uncovered-lines');
- casper.waitForSelector('.coverage-green', function () {
- test.assertElementCount('.coverage-green', 18);
- test.assertElementCount('.coverage-red', 51);
- test.assertElementCount('.coverage-orange', 0);
- test.assertElementCount('.component-viewer-source .row', 136);
+ casper.waitForSelector('.source-line-covered', function () {
+ test.assertElementCount('.source-line-covered', 18);
+ test.assertElementCount('.source-line-uncovered', 50);
+ test.assertElementCount('.source-line-partially-covered', 0);
+ test.assertElementCount('.component-viewer-source .source-line', 136);
});
})
.then(function () {
casper.click('.js-filter-branches-to-cover');
- casper.waitForSelector('.coverage-green', function () {
- test.assertElementCount('.coverage-green', 26);
- test.assertElementCount('.coverage-red', 4);
- test.assertElementCount('.coverage-orange', 2);
- test.assertElementCount('.component-viewer-source .row', 33);
+ casper.waitForSelector('.source-line-covered', function () {
+ test.assertElementCount('.source-line-covered', 19);
+ test.assertElementCount('.source-line-uncovered', 3);
+ test.assertElementCount('.source-line-partially-covered', 2);
+ test.assertElementCount('.component-viewer-source .source-line', 33);
});
})
.then(function () {
casper.click('.js-filter-uncovered-branches');
- casper.waitForSelector('.coverage-green', function () {
- test.assertElementCount('.coverage-green', 6);
- test.assertElementCount('.coverage-red', 4);
- test.assertElementCount('.coverage-orange', 2);
- test.assertElementCount('.component-viewer-source .row', 13);
+ casper.waitForSelector('.source-line-covered', function () {
+ test.assertElementCount('.source-line-covered', 4);
+ test.assertElementCount('.source-line-uncovered', 3);
+ test.assertElementCount('.source-line-partially-covered', 2);
+ test.assertElementCount('.component-viewer-source .source-line', 13);
});
})
@@ -83,13 +83,13 @@ casper.test.begin(testName('Go From Coverage to Test File'), function (test) {
})
.then(function () {
- casper.waitForSelector('.component-viewer-source .row');
+ casper.waitForSelector('.component-viewer-source .source-line');
})
.then(function () {
casper.click('.js-toggle-coverage');
- casper.waitForSelector('.coverage-green', function () {
- casper.click('.coverage-green .coverage-tests');
+ casper.waitForSelector('.source-line-covered', function () {
+ casper.click('.source-line-covered');
casper.waitForSelector('.bubble-popup', function () {
test.assertSelectorContains('.bubble-popup', 'src/test/java/org/sonar/batch/issue/IssueCacheTest.java');
test.assertSelectorContains('.bubble-popup', 'should_update_existing_issue');
diff --git a/server/sonar-web/src/main/js/tests/e2e/tests/component-viewer-duplications-spec.js b/server/sonar-web/src/main/js/tests/e2e/tests/component-viewer-duplications-spec.js
index a0fc4533732..fbce585badd 100644
--- a/server/sonar-web/src/main/js/tests/e2e/tests/component-viewer-duplications-spec.js
+++ b/server/sonar-web/src/main/js/tests/e2e/tests/component-viewer-duplications-spec.js
@@ -17,20 +17,20 @@ casper.test.begin(testName('Filters'), function (test) {
})
.then(function () {
- casper.waitForSelector('.component-viewer-source .row');
+ casper.waitForSelector('.component-viewer-source .source-line');
})
.then(function () {
casper.click('.js-header-tab-duplications');
- casper.waitForSelector('.duplication-exists', function () {
+ casper.waitForSelector('.source-line-duplicated', function () {
test.assertExists('.js-filter-duplications.active');
- test.assertElementCount('.component-viewer-source .row', 39);
+ test.assertElementCount('.component-viewer-source .source-line', 39);
});
})
.then(function () {
casper.click('.js-filter-duplications');
- test.assertElementCount('.component-viewer-source .row', 520);
+ test.assertElementCount('.component-viewer-source .source-line', 520);
})
.run(function () {
@@ -53,15 +53,15 @@ casper.test.begin(testName('Cross-Project'), function (test) {
})
.then(function () {
- casper.waitForSelector('.component-viewer-source .row');
+ casper.waitForSelector('.component-viewer-source .source-line');
})
.then(function () {
casper.click('.js-header-tab-duplications');
casper.waitForSelector('.js-filter-duplications', function () {
casper.click('.js-filter-duplications');
- casper.waitForSelector('.duplication-exists', function () {
- casper.click('.duplication-exists');
+ casper.waitForSelector('.source-line-duplicated', function () {
+ casper.click('.source-line-duplicated');
casper.waitForSelector('.bubble-popup', function () {
test.assertSelectorContains('.bubble-popup', 'JavaScript');
test.assertSelectorContains('.bubble-popup', 'JavaScript :: Sonar Plugin');
@@ -92,12 +92,12 @@ casper.test.begin(testName('In Deleted Files'), function (test) {
})
.then(function () {
- casper.waitForSelector('.component-viewer-source .row');
+ casper.waitForSelector('.component-viewer-source .source-line');
})
.then(function () {
casper.click('.js-toggle-duplications');
- casper.waitForSelector('.duplication-exists', function () {
+ casper.waitForSelector('.source-line-duplicated', function () {
test.assertExists('.js-duplications-in-deleted-files');
});
})
diff --git a/server/sonar-web/src/main/js/tests/e2e/tests/component-viewer-favorite-spec.js b/server/sonar-web/src/main/js/tests/e2e/tests/component-viewer-favorite-spec.js
index 8776419f7b0..b3cce4f92c2 100644
--- a/server/sonar-web/src/main/js/tests/e2e/tests/component-viewer-favorite-spec.js
+++ b/server/sonar-web/src/main/js/tests/e2e/tests/component-viewer-favorite-spec.js
@@ -17,7 +17,7 @@ casper.test.begin(testName('Mark as Favorite'), function (test) {
})
.then(function () {
- casper.waitForSelector('.component-viewer-source .row');
+ casper.waitForSelector('.component-viewer-source .source-line');
})
.then(function () {
@@ -49,7 +49,7 @@ casper.test.begin(testName('Don\'t Show Favorite If Not Logged In'), function (t
})
.then(function () {
- casper.waitForSelector('.component-viewer-source .row');
+ casper.waitForSelector('.component-viewer-source .source-line');
})
.then(function () {
diff --git a/server/sonar-web/src/main/js/tests/e2e/tests/component-viewer-issues-spec.js b/server/sonar-web/src/main/js/tests/e2e/tests/component-viewer-issues-spec.js
index a1c716ba5de..3ea8cb5d1cd 100644
--- a/server/sonar-web/src/main/js/tests/e2e/tests/component-viewer-issues-spec.js
+++ b/server/sonar-web/src/main/js/tests/e2e/tests/component-viewer-issues-spec.js
@@ -17,7 +17,7 @@ casper.test.begin(testName('Filters'), function (test) {
})
.then(function () {
- casper.waitForSelector('.component-viewer-source .row');
+ casper.waitForSelector('.component-viewer-source .source-line');
})
.then(function () {
@@ -116,7 +116,7 @@ casper.test.begin(testName('On File Level'), function (test) {
})
.then(function () {
- casper.waitForSelector('.component-viewer-source .row');
+ casper.waitForSelector('.component-viewer-source .source-line');
})
.then(function () {
@@ -130,7 +130,7 @@ casper.test.begin(testName('On File Level'), function (test) {
})
.then(function () {
- test.assertVisible('.component-viewer-source .row[data-line-number="0"]');
+ test.assertVisible('.component-viewer-source .source-line[data-line-number="0"]');
test.assertExists('#issue-20002ec7-b647-44da-bdf5-4d9fbf4b7c58');
})
@@ -152,7 +152,7 @@ casper.test.begin(testName('Bulk Change Link Exists'), function (test) {
})
.then(function () {
- casper.waitForSelector('.component-viewer-source .row');
+ casper.waitForSelector('.component-viewer-source .source-line');
})
.then(function () {
diff --git a/server/sonar-web/src/main/js/tests/e2e/tests/component-viewer-lines-filters-spec.js b/server/sonar-web/src/main/js/tests/e2e/tests/component-viewer-lines-filters-spec.js
index 2960f784d25..92b0e485907 100644
--- a/server/sonar-web/src/main/js/tests/e2e/tests/component-viewer-lines-filters-spec.js
+++ b/server/sonar-web/src/main/js/tests/e2e/tests/component-viewer-lines-filters-spec.js
@@ -16,7 +16,7 @@ casper.test.begin(testName('Lines Filters'), function (test) {
})
.then(function () {
- casper.waitForSelector('.component-viewer-source .row');
+ casper.waitForSelector('.component-viewer-source .source-line');
})
.then(function () {
@@ -26,12 +26,12 @@ casper.test.begin(testName('Lines Filters'), function (test) {
.then(function () {
casper.click('.js-filter-ncloc');
- test.assertElementCount('.component-viewer-source .row', 451);
+ test.assertElementCount('.component-viewer-source .source-line', 451);
})
.then(function () {
casper.click('.js-filter-ncloc');
- test.assertElementCount('.component-viewer-source .row', 520);
+ test.assertElementCount('.component-viewer-source .source-line', 520);
})
.run(function () {
@@ -51,7 +51,7 @@ casper.test.begin(testName('Do Not Show Ncloc Filter If No Data'), function (tes
})
.then(function () {
- casper.waitForSelector('.component-viewer-source .row');
+ casper.waitForSelector('.component-viewer-source .source-line');
})
.then(function () {
diff --git a/server/sonar-web/src/main/js/tests/e2e/tests/component-viewer-link-to-raw-spec.js b/server/sonar-web/src/main/js/tests/e2e/tests/component-viewer-link-to-raw-spec.js
index 4249ea35d7d..019cdca2879 100644
--- a/server/sonar-web/src/main/js/tests/e2e/tests/component-viewer-link-to-raw-spec.js
+++ b/server/sonar-web/src/main/js/tests/e2e/tests/component-viewer-link-to-raw-spec.js
@@ -15,7 +15,7 @@ casper.test.begin(testName('Link to Raw'), function (test) {
})
.then(function () {
- casper.waitForSelector('.component-viewer-source .row');
+ casper.waitForSelector('.component-viewer-source .source-line');
})
.then(function () {
diff --git a/server/sonar-web/src/main/js/tests/e2e/tests/component-viewer-select-tab-by-metric-spec.js b/server/sonar-web/src/main/js/tests/e2e/tests/component-viewer-select-tab-by-metric-spec.js
index 57ea4a016fc..f47a365a41e 100644
--- a/server/sonar-web/src/main/js/tests/e2e/tests/component-viewer-select-tab-by-metric-spec.js
+++ b/server/sonar-web/src/main/js/tests/e2e/tests/component-viewer-select-tab-by-metric-spec.js
@@ -21,7 +21,7 @@ casper.test.begin(testName('sqale_index'), function (test) {
})
.then(function () {
- casper.waitForSelector('.component-viewer-source .row');
+ casper.waitForSelector('.component-viewer-source .source-line');
})
.then(function () {
@@ -29,7 +29,7 @@ casper.test.begin(testName('sqale_index'), function (test) {
test.assertExists('.js-toggle-issues.active');
test.assertExists('.component-viewer-header-expanded-bar.active');
test.assertExists('.js-filter-unresolved-issues.active');
- test.assertElementCount('.component-viewer-source .row', 56);
+ test.assertElementCount('.component-viewer-source .source-line', 56);
});
})
@@ -55,7 +55,7 @@ casper.test.begin(testName('minor_violations'), function (test) {
})
.then(function () {
- casper.waitForSelector('.component-viewer-source .row');
+ casper.waitForSelector('.component-viewer-source .source-line');
})
.then(function () {
@@ -63,7 +63,7 @@ casper.test.begin(testName('minor_violations'), function (test) {
test.assertExists('.js-toggle-issues.active');
test.assertExists('.component-viewer-header-expanded-bar.active');
test.assertExists('.js-filter-MINOR-issues.active');
- test.assertElementCount('.component-viewer-source .row', 11);
+ test.assertElementCount('.component-viewer-source .source-line', 11);
});
})
@@ -89,15 +89,15 @@ casper.test.begin(testName('line_coverage'), function (test) {
})
.then(function () {
- casper.waitForSelector('.component-viewer-source .row');
+ casper.waitForSelector('.component-viewer-source .source-line');
})
.then(function () {
- casper.waitForSelector('.coverage-green', function () {
+ casper.waitForSelector('.source-line-covered', function () {
test.assertExists('.js-toggle-coverage.active');
test.assertExists('.component-viewer-header-expanded-bar.active');
test.assertExists('.js-filter-lines-to-cover.active');
- test.assertElementCount('.component-viewer-source .row', 369);
+ test.assertElementCount('.component-viewer-source .source-line', 369);
});
})
@@ -123,15 +123,15 @@ casper.test.begin(testName('duplicated_lines'), function (test) {
})
.then(function () {
- casper.waitForSelector('.component-viewer-source .row');
+ casper.waitForSelector('.component-viewer-source .source-line');
})
.then(function () {
- casper.waitForSelector('.duplication-exists', function () {
+ casper.waitForSelector('.source-line-duplicated', function () {
test.assertExists('.js-toggle-duplications.active');
test.assertExists('.component-viewer-header-expanded-bar.active');
test.assertExists('.js-filter-duplications.active');
- test.assertElementCount('.component-viewer-source .row', 39);
+ test.assertElementCount('.component-viewer-source .source-line', 39);
});
})
diff --git a/server/sonar-web/src/main/js/tests/e2e/tests/component-viewer-spec.js b/server/sonar-web/src/main/js/tests/e2e/tests/component-viewer-spec.js
index a296d84cb84..8aee5a10dce 100644
--- a/server/sonar-web/src/main/js/tests/e2e/tests/component-viewer-spec.js
+++ b/server/sonar-web/src/main/js/tests/e2e/tests/component-viewer-spec.js
@@ -16,7 +16,7 @@ casper.test.begin(testName('Base'), function (test) {
})
.then(function () {
- casper.waitForSelector('.component-viewer-source .row', function () {
+ casper.waitForSelector('.component-viewer-source .source-line', function () {
// Check header elements
test.assertExists('.component-viewer-header');
test.assertSelectorContains('.component-viewer-header-component-project', 'SonarQube');
@@ -35,7 +35,7 @@ casper.test.begin(testName('Base'), function (test) {
test.assertExists('.js-header-tab-scm');
// Check source
- test.assertElementCount('.component-viewer-source .row', 520);
+ test.assertElementCount('.component-viewer-source .source-line', 520);
test.assertSelectorContains('.component-viewer-source', 'public class Cache');
// Check workspace
@@ -64,7 +64,7 @@ casper.test.begin(testName('Decoration'), function (test) {
})
.then(function () {
- casper.waitForSelector('.component-viewer-source .row');
+ casper.waitForSelector('.component-viewer-source .source-line');
})
.then(function () {
@@ -84,16 +84,14 @@ casper.test.begin(testName('Decoration'), function (test) {
.then(function () {
// Check coverage decoration
casper.click('.js-toggle-coverage');
- casper.waitForSelector('.coverage-green', function () {
- test.assertElementCount('.coverage-green', 149);
- test.assertSelectorContains('.coverage-green', '27');
- test.assertElementCount('.coverage-red', 51);
- test.assertElementCount('.coverage-orange', 2);
- test.assertSelectorContains('.coverage-orange', '1/2');
+ casper.waitForSelector('.source-line-covered', function () {
+ test.assertElementCount('.source-line-covered', 142);
+ test.assertElementCount('.source-line-uncovered', 50);
+ test.assertElementCount('.source-line-partially-covered', 2);
casper.click('.js-toggle-coverage');
- casper.waitWhileSelector('.coverage-green', function () {
- test.assertDoesntExist('.coverage-green');
+ casper.waitWhileSelector('.source-line-covered', function () {
+ test.assertDoesntExist('.source-line-covered');
});
});
})
@@ -101,12 +99,12 @@ casper.test.begin(testName('Decoration'), function (test) {
.then(function () {
// Check duplications decoration
casper.click('.js-toggle-duplications');
- casper.waitForSelector('.duplication-exists', function () {
- test.assertElementCount('.duplication-exists', 32);
+ casper.waitForSelector('.source-line-duplicated', function () {
+ test.assertElementCount('.source-line-duplicated', 32);
casper.click('.js-toggle-duplications');
- casper.waitWhileSelector('.duplication-exists', function () {
- test.assertDoesntExist('.duplication-exists');
+ casper.waitWhileSelector('.source-line-duplicated', function () {
+ test.assertDoesntExist('.source-line-duplicated');
});
});
})
@@ -114,14 +112,14 @@ casper.test.begin(testName('Decoration'), function (test) {
.then(function () {
// Check scm decoration
casper.click('.js-toggle-scm');
- casper.waitForSelector('.scm-author', function () {
- test.assertElementCount('.scm-author', 182);
- test.assertSelectorContains('.scm-author', 'simon.brandhof@gmail.com');
- test.assertSelectorContains('.scm-author', 'julien.henry@sonarsource.com');
+ casper.waitForSelector('.source-line-scm-inner', function () {
+ test.assertElementCount('.source-line-scm-inner', 182);
+ test.assertExists('.source-line-scm-inner[data-author="simon.brandhof@gmail.com"]');
+ test.assertExists('.source-line-scm-inner[data-author="julien.henry@sonarsource.com"]');
casper.click('.js-toggle-scm');
- casper.waitWhileSelector('.scm-author', function () {
- test.assertDoesntExist('.scm-author');
+ casper.waitWhileSelector('.source-line-scm-inner', function () {
+ test.assertDoesntExist('.source-line-scm-inner');
});
});
})
@@ -141,10 +139,14 @@ casper.test.begin(testName('Header'), function (test) {
lib.mockRequestFromFile('/api/components/app', 'app.json');
lib.mockRequestFromFile('/api/sources/show', 'source.json');
lib.mockRequestFromFile('/api/resources', 'resources.json');
+ lib.mockRequestFromFile('/api/issues/search', 'issues.json');
+ lib.mockRequestFromFile('/api/coverage/show', 'coverage.json');
+ lib.mockRequestFromFile('/api/duplications/show', 'duplications.json');
+ lib.mockRequestFromFile('/api/sources/scm', 'scm.json');
})
.then(function () {
- casper.waitForSelector('.component-viewer-source .row');
+ casper.waitForSelector('.component-viewer-source .source-line');
})
.then(function () {
@@ -244,7 +246,7 @@ casper.test.begin(testName('Test File'), function (test) {
})
.then(function () {
- casper.waitForSelector('.component-viewer-source .row');
+ casper.waitForSelector('.component-viewer-source .source-line');
})
.then(function () {
@@ -284,13 +286,13 @@ casper.test.begin(testName('Go From Coverage to Test File'), function (test) {
})
.then(function () {
- casper.waitForSelector('.component-viewer-source .row');
+ casper.waitForSelector('.component-viewer-source .source-line');
})
.then(function () {
casper.click('.js-toggle-coverage');
- casper.waitForSelector('.coverage-green', function () {
- casper.click('.coverage-green .coverage-tests');
+ casper.waitForSelector('.source-line-covered', function () {
+ casper.click('.source-line-covered');
casper.waitForSelector('.bubble-popup', function () {
test.assertSelectorContains('.bubble-popup', 'src/test/java/org/sonar/batch/issue/IssueCacheTest.java');
test.assertSelectorContains('.bubble-popup', 'should_update_existing_issue');
@@ -329,7 +331,7 @@ casper.test.begin(testName('Coverage Filters'), function (test) {
})
.then(function () {
- casper.waitForSelector('.component-viewer-source .row');
+ casper.waitForSelector('.component-viewer-source .source-line');
})
.then(function () {
@@ -339,41 +341,41 @@ casper.test.begin(testName('Coverage Filters'), function (test) {
.then(function () {
casper.click('.js-filter-lines-to-cover');
- casper.waitForSelector('.coverage-green', function () {
- test.assertElementCount('.coverage-green', 149);
- test.assertElementCount('.coverage-red', 51);
- test.assertElementCount('.coverage-orange', 2);
- test.assertElementCount('.component-viewer-source .row', 369);
+ casper.waitForSelector('.source-line-covered', function () {
+ test.assertElementCount('.source-line-covered', 142);
+ test.assertElementCount('.source-line-uncovered', 50);
+ test.assertElementCount('.source-line-partially-covered', 2);
+ test.assertElementCount('.component-viewer-source .source-line', 369);
});
})
.then(function () {
casper.click('.js-filter-uncovered-lines');
- casper.waitForSelector('.coverage-green', function () {
- test.assertElementCount('.coverage-green', 18);
- test.assertElementCount('.coverage-red', 51);
- test.assertElementCount('.coverage-orange', 0);
- test.assertElementCount('.component-viewer-source .row', 136);
+ casper.waitForSelector('.source-line-covered', function () {
+ test.assertElementCount('.source-line-covered', 18);
+ test.assertElementCount('.source-line-uncovered', 50);
+ test.assertElementCount('.source-line-partially-covered', 0);
+ test.assertElementCount('.component-viewer-source .source-line', 136);
});
})
.then(function () {
casper.click('.js-filter-branches-to-cover');
- casper.waitForSelector('.coverage-green', function () {
- test.assertElementCount('.coverage-green', 26);
- test.assertElementCount('.coverage-red', 4);
- test.assertElementCount('.coverage-orange', 2);
- test.assertElementCount('.component-viewer-source .row', 33);
+ casper.waitForSelector('.source-line-covered', function () {
+ test.assertElementCount('.source-line-covered', 19);
+ test.assertElementCount('.source-line-uncovered', 3);
+ test.assertElementCount('.source-line-partially-covered', 2);
+ test.assertElementCount('.component-viewer-source .source-line', 33);
});
})
.then(function () {
casper.click('.js-filter-uncovered-branches');
- casper.waitForSelector('.coverage-green', function () {
- test.assertElementCount('.coverage-green', 6);
- test.assertElementCount('.coverage-red', 4);
- test.assertElementCount('.coverage-orange', 2);
- test.assertElementCount('.component-viewer-source .row', 13);
+ casper.waitForSelector('.source-line-covered', function () {
+ test.assertElementCount('.source-line-covered', 4);
+ test.assertElementCount('.source-line-uncovered', 3);
+ test.assertElementCount('.source-line-partially-covered', 2);
+ test.assertElementCount('.component-viewer-source .source-line', 13);
});
})
@@ -397,19 +399,19 @@ casper.test.begin(testName('Ability to Deselect Filters'), function (test) {
})
.then(function () {
- casper.waitForSelector('.component-viewer-source .row');
+ casper.waitForSelector('.component-viewer-source .source-line');
})
.then(function () {
casper.click('.js-header-tab-issues');
var testFilter = '.js-filter-unresolved-issues';
casper.waitForSelector(testFilter + '.active', function () {
- lib.waitForElementCount('.component-viewer-source .row', 56, function () {
+ lib.waitForElementCount('.component-viewer-source .source-line', 56, function () {
casper.click(testFilter);
- lib.waitForElementCount('.component-viewer-source .row', 520, function () {
+ lib.waitForElementCount('.component-viewer-source .source-line', 520, function () {
test.assertDoesntExist(testFilter + '.active');
casper.click(testFilter);
- lib.waitForElementCount('.component-viewer-source .row', 56, function () {
+ lib.waitForElementCount('.component-viewer-source .source-line', 56, function () {
test.assertExists(testFilter + '.active');
});
});
@@ -421,12 +423,12 @@ casper.test.begin(testName('Ability to Deselect Filters'), function (test) {
casper.click('.js-header-tab-coverage');
var testFilter = '.js-filter-lines-to-cover';
casper.waitForSelector(testFilter + '.active', function () {
- lib.waitForElementCount('.component-viewer-source .row', 369, function () {
+ lib.waitForElementCount('.component-viewer-source .source-line', 369, function () {
casper.click(testFilter);
- lib.waitForElementCount('.component-viewer-source .row', 520, function () {
+ lib.waitForElementCount('.component-viewer-source .source-line', 520, function () {
test.assertDoesntExist(testFilter + '.active');
casper.click(testFilter);
- lib.waitForElementCount('.component-viewer-source .row', 369, function () {
+ lib.waitForElementCount('.component-viewer-source .source-line', 369, function () {
test.assertExists(testFilter + '.active');
});
});
@@ -438,12 +440,12 @@ casper.test.begin(testName('Ability to Deselect Filters'), function (test) {
casper.click('.js-header-tab-duplications');
var testFilter = '.js-filter-duplications';
casper.waitForSelector(testFilter + '.active', function () {
- lib.waitForElementCount('.component-viewer-source .row', 39, function () {
+ lib.waitForElementCount('.component-viewer-source .source-line', 39, function () {
casper.click(testFilter);
- lib.waitForElementCount('.component-viewer-source .row', 520, function () {
+ lib.waitForElementCount('.component-viewer-source .source-line', 520, function () {
test.assertDoesntExist(testFilter + '.active');
casper.click(testFilter);
- lib.waitForElementCount('.component-viewer-source .row', 39, function () {
+ lib.waitForElementCount('.component-viewer-source .source-line', 39, function () {
test.assertExists(testFilter + '.active');
});
});
diff --git a/server/sonar-web/src/main/less/component-viewer-source-colorizer.less b/server/sonar-web/src/main/less/component-viewer-source-colorizer.less
index 8ca1788ef74..9c5d4027839 100644
--- a/server/sonar-web/src/main/less/component-viewer-source-colorizer.less
+++ b/server/sonar-web/src/main/less/component-viewer-source-colorizer.less
@@ -1,13 +1,6 @@
@import (reference) 'variables';
@import (reference) 'mixins';
-.code pre {
- padding: 0;
- font-family: @monoFontFamily;
- font-size: 12px;
- line-height: 16px;
-}
-
/* constants */
.code .c {
font-style: normal;
diff --git a/server/sonar-web/src/main/less/components.less b/server/sonar-web/src/main/less/components.less
index f53ed457f0e..834d9d6912b 100644
--- a/server/sonar-web/src/main/less/components.less
+++ b/server/sonar-web/src/main/less/components.less
@@ -1,5 +1,5 @@
@import "components/component-issues";
-@import "components/code-source";
+@import "components/source";
@import "components/facets";
@import "components/modals";
@import "components/issues";
diff --git a/server/sonar-web/src/main/less/components/code-source.less b/server/sonar-web/src/main/less/components/code-source.less
deleted file mode 100644
index a657c5bc2b0..00000000000
--- a/server/sonar-web/src/main/less/components/code-source.less
+++ /dev/null
@@ -1,92 +0,0 @@
-@import (reference) "../mixins";
-@import (reference) "../variables";
-
-.code-source {
-
- .code {
- width: 100%;
- border: 1px solid @barBorderColor;
- }
-
- .code th {
- height: 30px;
- .box-sizing(border-box);
- background-color: @barBackgroundColor;
-
- &.lid {
- border-right: 1px solid @barBorderColor;
- }
-
- &.stat {
- padding-top: 4px;
- padding-bottom: 4px;
- border-left: none;
- border-right: none;
- border-bottom: 1px solid @barBorderColor;
- }
- }
-
- .code .stat {
- vertical-align: top;
- min-width: 12px;
- padding: 1px 5px;
- background-color: @barBackgroundColor;
- color: #888;
- font-size: @smallFontSize;
- line-height: 16px;
- text-align: right;
- cursor: default;
- white-space: nowrap;
- }
-
- .code .lid {
- min-width: 18px;
- padding-left: 10px;
- padding-right: 10px;
- cursor: pointer;
- }
-
- .code .scm {
- line-height: 16px;
- padding-top: 0;
- padding-bottom: 0;
- text-align: left;
-
- .scm-date {
- display: inline-block;
- vertical-align: middle;
- padding: 2px 4px;
- line-height: 1;
- background-color: @barBorderColor;
- }
-
- .scm-author {
- display: inline-block;
- vertical-align: middle;
- max-width: 40px;
- padding: 2px 0;
- line-height: 1;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
-
- }
-
- .code .row:hover {
- td.stat { background-color: @barBorderColor; }
- td.line { background-color: @barBackgroundColor; }
- }
-
- .code .row-highlighted,
- .code .row-highlighted:hover {
- td.stat { background-color: #fdf190; }
- td.line, .code-issues, .code-issue { background-color: #fff8c2; }
- }
-
- .code td.line {
- width: 100%;
- padding: 1px 5px;
- }
-
-}
diff --git a/server/sonar-web/src/main/less/components/issues.less b/server/sonar-web/src/main/less/components/issues.less
index a709e9ab9a8..dd439dbccca 100644
--- a/server/sonar-web/src/main/less/components/issues.less
+++ b/server/sonar-web/src/main/less/components/issues.less
@@ -9,17 +9,19 @@
.issue-list {
- margin: 5px -5px;
- border-top: 1px solid @barBorderColor;
- border-bottom: 1px solid @barBorderColor;
+ margin: 10px 0;
+ background-color: #ffeaea;
}
.issue {
position: relative;
+ max-width: 900px;
padding-top: @topPadding;
padding-bottom: @bottomPadding;
- background-color: @barBackgroundColor;
+ border: 1px solid transparent;
+ border-left-width: 3px;
+ background-color: #ffeaea;
}
.issue.selected {
diff --git a/server/sonar-web/src/main/less/components/source.less b/server/sonar-web/src/main/less/components/source.less
new file mode 100644
index 00000000000..7ef086a2aaa
--- /dev/null
+++ b/server/sonar-web/src/main/less/components/source.less
@@ -0,0 +1,146 @@
+@import (reference) "../mixins";
+@import (reference) "../variables";
+
+@lineHeight: 18px;
+@duplicationColor: #f3ca8e;
+
+.source {
+ width: 100%;
+ border: 1px solid @barBorderColor;
+ overflow-x: auto;
+ overflow-y: hidden;
+}
+
+.source-table {
+ width: 100%;
+ border: none;
+ border-collapse: collapse;
+}
+
+.source-line:hover {
+ .source-line-number,
+ .source-line-coverage,
+ .source-line-duplications,
+ .source-line-duplications-extra,
+ .source-line-scm {
+ border-color: darken(@barBackgroundColor, 4%);
+ background-color: darken(@barBackgroundColor, 4%);
+ }
+
+ .source-line-code {
+ background-color: darken(#fff, 4%);
+ }
+}
+
+.source-line-highlighted,
+.source-line-highlighted:hover {
+ .source-line-number,
+ .source-line-coverage,
+ .source-line-duplications,
+ .source-line-duplications-extra,
+ .source-line-scm {
+ border-color: #fdf190 !important;
+ background-color: #fdf190;
+ }
+
+ .source-line-code {
+ background-color: #fff8c2;
+ }
+}
+
+.source-line-expand {
+ .source-line-code {
+ background: url(../images/gray-stripes.png) repeat;
+ }
+}
+
+.source pre {
+ height: @lineHeight;
+ padding: 0;
+}
+
+.source pre,
+.source-meta {
+ line-height: @lineHeight;
+ font-family: @monoFontFamily;
+ font-size: 12px;
+}
+
+.source-line-code {
+ padding: 0 10px;
+}
+
+.source-meta {
+ vertical-align: top;
+ width: 1px;
+ .user-select(none);
+}
+
+.source-meta + .source-meta {
+ border-left: 1px solid @barBackgroundColor;
+}
+
+.source-line-number {
+ min-width: 18px;
+ padding: 0 10px;
+ background-color: @barBackgroundColor;
+ color: @secondFontColor;
+ text-align: right;
+
+ &[data-line-number] {
+ cursor: pointer;
+ }
+
+ &:before {
+ content: attr(data-line-number);
+ }
+}
+
+.source-line-coverage {
+ background-color: @barBackgroundColor;
+}
+
+.source-line-duplications,
+.source-line-duplications-extra {
+ background-color: @barBackgroundColor;
+}
+
+.source-line-scm {
+ padding: 0 5px;
+ background-color: @barBackgroundColor;
+}
+
+.source-line-scm-inner {
+ max-width: 40px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+
+ &:before {
+ content: attr(data-author);
+ }
+}
+
+.source-line-bar {
+ width: 5px;
+ height: @lineHeight;
+}
+
+.source-line-covered {
+ background-color: @green !important;
+ cursor: pointer;
+}
+
+.source-line-uncovered {
+ background-color: @red !important;
+}
+
+.source-line-partially-covered {
+ background-color: @orange !important;
+ cursor: pointer;
+}
+
+.source-line-duplicated {
+ background-color: @duplicationColor !important;
+ cursor: pointer;
+}
diff --git a/server/sonar-web/src/main/less/issues.less b/server/sonar-web/src/main/less/issues.less
index cd20bb1d1da..845f1632858 100644
--- a/server/sonar-web/src/main/less/issues.less
+++ b/server/sonar-web/src/main/less/issues.less
@@ -35,6 +35,7 @@
.issues-side {
position: fixed;
+ z-index: 100;
width: @sideWidth;
top: 30px; left: 0; bottom: 0;
.box-sizing(border-box);
@@ -291,6 +292,10 @@
.issues-workspace-list {
padding: 0 5px;
+
+ .issue {
+ max-width: none;
+ }
}
.issues-workspace-list-more {
diff --git a/server/sonar-web/src/main/less/mixins.less b/server/sonar-web/src/main/less/mixins.less
index 66349f4b655..efe411f7d72 100644
--- a/server/sonar-web/src/main/less/mixins.less
+++ b/server/sonar-web/src/main/less/mixins.less
@@ -140,6 +140,13 @@
background-image: linear-gradient(@angle, rgba(255,255,255,.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,.15) 50%, rgba(255,255,255,.15) 75%, transparent 75%, transparent);
}
+.user-select(@mode) {
+ -webkit-user-select: @mode;
+ -moz-user-select: @mode;
+ -ms-user-select: @mode;
+ user-select: @mode;
+}
+
.trans(@property: all, @options: @defaultTransitionOptions) {
transition: @property @options;
}