From: Stas Vilchik Date: Thu, 27 Nov 2014 08:06:20 +0000 (+0100) Subject: SONAR-5877 Unified source viewer X-Git-Tag: 5.0-RC1~186 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=bc6d4d9f8449781870e4451ffd5db47b63aef76a;p=sonarqube.git SONAR-5877 Unified source viewer --- 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 = $('
').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 = $('
' + tp('issues.issues_limit_reached', issues) + '
') - $('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 = $('
').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 = $('
') + code.append issueList + issueList.append "
" + @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 @@ - + + + {{#if settings.scm}} + + {{/if}} {{#if settings.coverage}} - - + {{/if}} {{#if settings.duplications}} - - {{/if}} - {{#if settings.scm}} - + {{#each baseDuplications}} + + {{/each}} {{/if}} - + - 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 @@

{{t 'duplications.dups_found_on_deleted_resource'}}

{{/if}} - - {{#if showZeroLine}} - - {{#if settings.coverage}} - - - {{/if}} - {{#if settings.duplications}} - - {{/if}} - {{#if settings.scm}} - - {{/if}} - - - - {{/if}} - - {{#each source}} - {{#if show}} - - - - - {{#if ../../settings.coverage}} - +
+
{{lineNumber}} - {{#if coverage}} - - {{coverage.testCases}} - - {{/if}} -
+ {{#if showZeroLine}} + + - + {{#if settings.scm}} + {{/if}} - {{#if ../../settings.duplications}} - {{/if}} - {{#if ../../settings.scm}} - + {{#if settings.duplications}} + {{#each baseDuplications}} + + {{/each}} {{/if}} - + {{/if}} - {{/each}} -
+ + {{#each source}} + {{#if show}} + + + + {{#if ../../settings.scm}} + + {{#if scm}} + {{#ifSCMChanged ../../../../source ../../../lineNumber}} +
+ {{/ifSCMChanged}} + {{/if}} + + {{/if}} + + {{#if ../../settings.coverage}} + +
+ + {{/if}} + + {{#if ../../settings.duplications}} + {{#each duplications}} + +
+ + {{/each}} + {{/if}} + +
{{{code}}}
+ + {{/if}} + {{/each}} + + {{/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 @@ +
+
{{t 'component_viewer.transition.coverage'}}
+ + {{#each testFiles}} +
+ + {{file.longName}} + +
    + {{#each tests}} +
  • + + + + {{name}} + + + {{durationInMs}}ms +
  • + {{/each}} +
+
+ {{/each}} +
+ +
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 @@ +
+
{{t 'component_viewer.transition.duplication'}}
+ {{#each duplications}} +
+ {{#notEqComponents file ../component}} +
+ {{projectFullName file}} +
+ {{/notEqComponents}} + + {{#notEq file.key ../component.key}} + + {{file.name}} + + {{/notEq}} + +
+ Lines: + {{#joinEach blocks ','}} + + {{from}} – {{sum from size}} + + {{/joinEach}} +
+
+ {{else}} + {{t 'duplications.block_was_duplicated_by_a_deleted_resource'}} + {{/each}} +
+ +
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 @@ +
+ + + {{#if canCreateManualIssue}} + + {{/if}} +
+ +
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}} + +{{/if}} + + + {{#each source}} + + + + + + + + + + {{#each duplications}} + + {{/each}} + + + + {{/each}} +
+ {{#ifSCMChanged2 ../source line}} +
+ {{/ifSCMChanged2}} +
+
+
+
+
+ {{#notNull code}} +
{{#if code}}{{{code}}}{{else}} {{/if}}
+ {{/notNull}} + + {{#notEmpty issues}} +
+ {{#each issues}} +
+ {{/each}} +
+ {{/notEmpty}} +
+ +{{#if hasSourceAfter}} + +{{/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; }