aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorStas Vilchik <vilchiks@gmail.com>2014-11-27 17:14:37 +0100
committerStas Vilchik <vilchiks@gmail.com>2014-11-28 09:49:36 +0100
commitd910309632d358d87f4a6988fb70e606661e7e47 (patch)
tree5f02b29725e42cb5a7fc674426f423aa96d792df
parentf2c17b30ebc3c39977215d698ede4f8578535b80 (diff)
downloadsonarqube-d910309632d358d87f4a6988fb70e606661e7e47.tar.gz
sonarqube-d910309632d358d87f4a6988fb70e606661e7e47.zip
Migrate source viewer to js
-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/js/source-viewer/popups/coverage-popup.js42
-rw-r--r--server/sonar-web/src/main/js/source-viewer/popups/duplication-popup.js65
-rw-r--r--server/sonar-web/src/main/js/source-viewer/popups/line-actions-popup.js41
-rw-r--r--server/sonar-web/src/main/js/source-viewer/source.js62
-rw-r--r--server/sonar-web/src/main/js/source-viewer/viewer.js489
10 files changed, 699 insertions, 548 deletions
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
deleted file mode 100644
index 66b97c2addb..00000000000
--- a/server/sonar-web/src/main/coffee/source-viewer/popups/coverage-popup.coffee
+++ /dev/null
@@ -1,42 +0,0 @@
-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
deleted file mode 100644
index 378ae8d8022..00000000000
--- a/server/sonar-web/src/main/coffee/source-viewer/popups/duplication-popup.coffee
+++ /dev/null
@@ -1,55 +0,0 @@
-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
deleted file mode 100644
index f1e96f0d454..00000000000
--- a/server/sonar-web/src/main/coffee/source-viewer/popups/line-actions-popup.coffee
+++ /dev/null
@@ -1,40 +0,0 @@
-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
deleted file mode 100644
index 7badb90d1ec..00000000000
--- a/server/sonar-web/src/main/coffee/source-viewer/source.coffee
+++ /dev/null
@@ -1,51 +0,0 @@
-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
deleted file mode 100644
index 17d94c95602..00000000000
--- a/server/sonar-web/src/main/coffee/source-viewer/viewer.coffee
+++ /dev/null
@@ -1,360 +0,0 @@
-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/js/source-viewer/popups/coverage-popup.js b/server/sonar-web/src/main/js/source-viewer/popups/coverage-popup.js
new file mode 100644
index 00000000000..f0a59152634
--- /dev/null
+++ b/server/sonar-web/src/main/js/source-viewer/popups/coverage-popup.js
@@ -0,0 +1,42 @@
+define([
+ 'backbone.marionette',
+ 'templates/source-viewer',
+ 'common/popup',
+ 'component-viewer/utils'
+], function (Marionette, Templates, Popup, utils) {
+
+ var $ = jQuery;
+
+ return Popup.extend({
+ template: Templates['source-viewer-coverage-popup'],
+
+ events: {
+ 'click a[data-key]': 'goToFile'
+ },
+
+ onRender: function () {
+ Popup.prototype.onRender.apply(this, arguments);
+ this.$('.bubble-popup-container').isolatedScroll();
+ },
+
+ goToFile: function (e) {
+ // TODO Implement this
+ var el = $(e.currentTarget),
+ key = el.data('key'),
+ method = el.data('method'),
+ files = this.model.get('files');
+ },
+
+ serializeData: function () {
+ var files = this.model.get('files'),
+ tests = _.groupBy(this.model.get('tests'), '_ref'),
+ testFiles = _.map(tests, function (testSet, fileRef) {
+ return {
+ file: files[fileRef],
+ tests: testSet
+ };
+ });
+ return { testFiles: testFiles };
+ }
+ });
+});
diff --git a/server/sonar-web/src/main/js/source-viewer/popups/duplication-popup.js b/server/sonar-web/src/main/js/source-viewer/popups/duplication-popup.js
new file mode 100644
index 00000000000..40dbe8c0ebe
--- /dev/null
+++ b/server/sonar-web/src/main/js/source-viewer/popups/duplication-popup.js
@@ -0,0 +1,65 @@
+define([
+ 'backbone.marionette',
+ 'templates/component-viewer',
+ 'common/popup',
+ 'component-viewer/utils'
+], function (Marionette, Templates, Popup, utils) {
+
+ var $ = jQuery;
+
+ return Popup.extend({
+ template: Templates['source-viewer-duplication-popup'],
+
+ events: {
+ 'click a[data-key]': 'goToFile'
+ },
+
+ goToFile: function (e) {
+ var key = $(e.currentTarget).data('key'),
+ line = $(e.currentTarget).data('line'),
+ files = this.options.main.source.get('duplicationFiles'),
+ options = this.collection.map(function (item) {
+ var file = files[item.get('_ref')],
+ x = utils.splitLongName(file.name);
+ return {
+ key: file.key,
+ name: x.name,
+ subname: x.dir,
+ component: {
+ projectName: file.projectName,
+ subProjectName: file.subProjectName
+ },
+ active: file.key === key
+ };
+ });
+ return _.uniq(options, function (item) {
+ return item.key;
+ });
+ },
+
+ serializeData: function () {
+ var duplications, files, groupedBlocks;
+ files = this.model.get('duplicationFiles');
+ groupedBlocks = _.groupBy(this.collection.toJSON(), '_ref');
+ duplications = _.map(groupedBlocks, function (blocks, fileRef) {
+ return {
+ blocks: blocks,
+ file: files[fileRef]
+ };
+ });
+ duplications = _.sortBy(duplications, (function (_this) {
+ return function (d) {
+ var a, b, c;
+ a = d.file.projectName !== _this.model.get('projectName');
+ b = d.file.subProjectName !== _this.model.get('subProjectName');
+ c = d.file.key !== _this.model.get('key');
+ return '' + a + b + c;
+ };
+ })(this));
+ return {
+ component: this.model.toJSON(),
+ duplications: duplications
+ };
+ }
+ });
+});
diff --git a/server/sonar-web/src/main/js/source-viewer/popups/line-actions-popup.js b/server/sonar-web/src/main/js/source-viewer/popups/line-actions-popup.js
new file mode 100644
index 00000000000..3931272bc73
--- /dev/null
+++ b/server/sonar-web/src/main/js/source-viewer/popups/line-actions-popup.js
@@ -0,0 +1,41 @@
+define([
+ 'backbone.marionette',
+ 'templates/source-viewer',
+ 'common/popup',
+ 'issue/manual-issue-view'
+], function (Marionette, Templates, Popup, ManualIssueView) {
+
+ return Popup.extend({
+ template: Templates['source-viewer-line-options-popup'],
+
+ events: {
+ 'click .js-get-permalink': 'getPermalink',
+ 'click .js-add-manual-issue': 'addManualIssue'
+ },
+
+ getPermalink: function (e) {
+ e.preventDefault();
+ var url = baseUrl + '/component/index#component=' +
+ (encodeURIComponent(this.model.key())) + '&line=' + this.options.line,
+ windowParams = 'resizable=1,scrollbars=1,status=1';
+ window.open(url, this.model.get('name'), windowParams);
+ },
+
+ addManualIssue: function (e) {
+ e.preventDefault();
+ var that = this,
+ line = this.options.line,
+ component = this.model.key(),
+ manualIssueView = new ManualIssueView({
+ line: line,
+ component: component,
+ rules: this.model.get('manual_rules')
+ });
+ manualIssueView.render().$el.appendTo(this.options.row.find('.source-line-code'));
+ manualIssueView.on('add', function (issue) {
+ that.trigger('onManualIssueAdded', issue);
+ });
+ }
+ });
+});
+
diff --git a/server/sonar-web/src/main/js/source-viewer/source.js b/server/sonar-web/src/main/js/source-viewer/source.js
new file mode 100644
index 00000000000..1ded0471cb7
--- /dev/null
+++ b/server/sonar-web/src/main/js/source-viewer/source.js
@@ -0,0 +1,62 @@
+define([
+ 'backbone'
+], function (Backbone) {
+
+ return Backbone.Model.extend({
+ idAttribute: 'uuid',
+
+ defaults: function () {
+ return {
+ hasSource: false,
+ hasCoverage: false,
+ hasDuplications: false,
+ hasSCM: false
+ };
+ },
+
+ key: function () {
+ return this.get('key');
+ },
+
+ addMeta: function (meta) {
+ var source = this.get('source'),
+ metaIdx = 0,
+ metaLine = meta[metaIdx];
+ source.forEach(function (line) {
+ while (metaLine != null && line.line > metaLine.line) {
+ metaLine = meta[++metaIdx];
+ }
+ if (metaLine != null && line.line === metaLine.line) {
+ _.extend(line, metaLine);
+ metaLine = meta[++metaIdx];
+ }
+ });
+ this.set({ source: source });
+ },
+
+ addDuplications: function (duplications) {
+ var source = this.get('source');
+ if (source != null) {
+ source.forEach(function (line) {
+ var lineDuplications = [];
+ duplications.forEach(function (d, i) {
+ var duplicated = false;
+ d.blocks.forEach(function (b) {
+ if (b._ref === '1') {
+ var lineFrom = b.from,
+ lineTo = b.from + b.size;
+ if (line.line >= lineFrom && line.line <= lineTo) {
+ duplicated = true;
+ }
+ }
+ });
+ lineDuplications.push(duplicated ? i + 1 : false);
+ });
+ line.duplications = lineDuplications;
+ });
+ }
+ this.set({ source: source });
+ }
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/source-viewer/viewer.js b/server/sonar-web/src/main/js/source-viewer/viewer.js
new file mode 100644
index 00000000000..3d9330faa25
--- /dev/null
+++ b/server/sonar-web/src/main/js/source-viewer/viewer.js
@@ -0,0 +1,489 @@
+define([
+ 'backbone',
+ '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'
+], function (Backbone, Marionette, Templates, Source, Issue, Issues, IssueView, CoveragePopupView, DuplicationPopupView, LineActionsPopupView) {
+
+ var $ = jQuery,
+ HIGHLIGHTED_ROW_CLASS = 'source-line-highlighted',
+ log = function (message) {
+ return console.log('Source Viewer:', message);
+ };
+
+ return Marionette.ItemView.extend({
+ 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: function () {
+ return {
+ '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: function () {
+ if (this.model == null) {
+ this.model = new Source();
+ }
+ this.issues = new Issues();
+ this.issueViews = [];
+ this.loadSourceBeforeThrottled = _.throttle(this.loadSourceBefore, 1000);
+ this.loadSourceAfterThrottled = _.throttle(this.loadSourceAfter, 1000);
+ this.scrollTimer = null;
+ },
+
+ onRender: function () {
+ log('Render');
+ this.renderIssues();
+ return this;
+ },
+
+ onClose: function () {
+ this.issueViews.forEach(function (view) {
+ return view.close();
+ });
+ this.issueViews = [];
+ },
+
+ open: function (id, key) {
+ var that = this;
+ this.model.clear();
+ this.model.set({
+ uuid: id,
+ key: key
+ });
+ this.requestComponent().done(function () {
+ that.requestSource()
+ .done(function () {
+ that.requestCoverage().done(function () {
+ that.requestDuplications().done(function () {
+ that.requestIssues().done(function () {
+ that.render();
+ that.trigger('loaded');
+ });
+ });
+ });
+ })
+ .fail(function () {
+ that.model.set({
+ source: [
+ { line: 0 }
+ ]
+ });
+ that.requestIssues().done(function () {
+ that.render();
+ that.trigger('loaded');
+ });
+ });
+ });
+ return this;
+ },
+
+ requestComponent: function () {
+ log('Request component details...');
+ var that = this,
+ url = baseUrl + '/api/components/app',
+ options = { key: this.model.key() };
+ return $.get(url, options).done(function (data) {
+ that.model.set(data);
+ });
+ },
+
+ linesLimit: function () {
+ return {
+ from: 1,
+ to: this.LINES_LIMIT
+ };
+ },
+
+ requestSource: function () {
+ var options, url;
+ log('Request source...');
+ url = '' + baseUrl + '/api/sources/lines';
+ options = _.extend({
+ uuid: this.model.id
+ }, this.linesLimit());
+ return $.get(url, options, (function (_this) {
+ return function (data) {
+ var firstLine, source;
+ source = data.sources || [];
+ if (source.length === 0 || (source.length > 0 && _.first(source).line === 1)) {
+ source.unshift({
+ line: 0
+ });
+ }
+ firstLine = _.first(source).line;
+ _this.model.set({
+ source: source,
+ hasSourceBefore: firstLine > 1,
+ hasSourceAfter: true
+ });
+ return log('Source loaded');
+ };
+ })(this));
+ },
+
+ requestCoverage: function () {
+ var options, url;
+ log('Request coverage');
+ url = '' + baseUrl + '/api/coverage/show';
+ options = {
+ key: this.model.key()
+ };
+ return $.get(url, options, (function (_this) {
+ return function (data) {
+ var coverage, hasCoverage;
+ hasCoverage = (data != null) && (data.coverage != null);
+ _this.model.set({
+ hasCoverage: hasCoverage
+ });
+ if (hasCoverage) {
+ coverage = data.coverage.map(function (c) {
+ var status;
+ status = 'partially-covered';
+ if (c[1] && c[3] === c[4]) {
+ status = 'covered';
+ }
+ if (!c[1] || c[4] === 0) {
+ status = 'uncovered';
+ }
+ return {
+ line: +c[0],
+ covered: status
+ };
+ });
+ } else {
+ coverage = [];
+ }
+ _this.model.addMeta(coverage);
+ return log('Coverage loaded');
+ };
+ })(this));
+ },
+
+ requestDuplications: function () {
+ var options, url;
+ log('Request duplications');
+ url = '' + baseUrl + '/api/duplications/show';
+ options = {
+ key: this.model.key()
+ };
+ return $.get(url, options, (function (_this) {
+ return function (data) {
+ var duplications, hasDuplications;
+ hasDuplications = (data != null) && (data.duplications != null);
+ if (hasDuplications) {
+ duplications = {};
+ data.duplications.forEach(function (d, i) {
+ return d.blocks.forEach(function (b) {
+ var lineFrom, lineTo, _i, _results;
+ if (b._ref === '1') {
+ lineFrom = b.from;
+ lineTo = b.from + b.size;
+ _results = [];
+ for (i = _i = lineFrom; lineFrom <= lineTo ? _i <= lineTo : _i >= lineTo; i = lineFrom <= lineTo ? ++_i : --_i) {
+ _results.push(duplications[i] = true);
+ }
+ return _results;
+ }
+ });
+ });
+ duplications = _.pairs(duplications).map(function (line) {
+ return {
+ line: +line[0],
+ duplicated: line[1]
+ };
+ });
+ } else {
+ duplications = [];
+ }
+ _this.model.addMeta(duplications);
+ _this.model.addDuplications(data.duplications);
+ _this.model.set({
+ duplications: data.duplications,
+ duplicationFiles: data.files
+ });
+ return log('Duplications loaded');
+ };
+ })(this));
+ },
+
+ requestIssues: function () {
+ var options;
+ log('Request issues');
+ options = {
+ data: {
+ componentUuids: this.model.id,
+ extra_fields: 'actions,transitions,assigneeName,actionPlanName',
+ resolved: false,
+ s: 'FILE_LINE',
+ asc: true
+ }
+ };
+ return this.issues.fetch(options).done((function (_this) {
+ return function () {
+ _this.issues.reset(_this.limitIssues(_this.issues));
+ _this.addIssuesPerLineMeta(_this.issues);
+ return log('Issues loaded');
+ };
+ })(this));
+ },
+
+ addIssuesPerLineMeta: function (issues) {
+ var issuesPerLine, lines;
+ lines = {};
+ issues.forEach(function (issue) {
+ var line;
+ line = issue.get('line') || 0;
+ if (!_.isArray(lines[line])) {
+ lines[line] = [];
+ }
+ return lines[line].push(issue.toJSON());
+ });
+ issuesPerLine = _.pairs(lines).map(function (line) {
+ return {
+ line: +line[0],
+ issues: line[1]
+ };
+ });
+ return this.model.addMeta(issuesPerLine);
+ },
+
+ limitIssues: function (issues) {
+ return issues.first(this.ISSUES_LIMIT);
+ },
+
+ renderIssues: function () {
+ log('Render issues');
+ this.issues.forEach(this.renderIssue, this);
+ return log('Issues rendered');
+ },
+
+ renderIssue: function (issue) {
+ var issueView;
+ issueView = new IssueView({
+ el: '#issue-' + issue.get('key'),
+ model: issue
+ });
+ this.issueViews.push(issueView);
+ return issueView.render();
+ },
+
+ addIssue: function (issue) {
+ var code, issueList, line;
+ line = issue.get('line') || 0;
+ code = this.$('.source-line-code[data-line-number=' + line + ']');
+ issueList = code.find('.issue-list');
+ if (issueList.length === 0) {
+ issueList = $('<div class="issue-list"></div>');
+ code.append(issueList);
+ }
+ issueList.append('<div class="issue" id="issue-' + issue.id + '"></div>');
+ return this.renderIssue(issue);
+ },
+
+ showCoveragePopup: function (e) {
+ var line, options, r, url;
+ r = window.process.addBackgroundProcess();
+ e.stopPropagation();
+ $('body').click();
+ line = $(e.currentTarget).data('line-number');
+ url = '' + baseUrl + '/api/tests/test_cases';
+ options = {
+ key: this.model.key(),
+ line: line
+ };
+ return $.get(url, options).done((function (_this) {
+ return function (data) {
+ var popup;
+ popup = new CoveragePopupView({
+ model: new Backbone.Model(data),
+ triggerEl: $(e.currentTarget)
+ });
+ popup.render();
+ return window.process.finishBackgroundProcess(r);
+ };
+ })(this)).fail(function () {
+ return window.process.failBackgroundProcess(r);
+ });
+ },
+
+ showDuplications: function () {
+ this.$('.source-line-duplications').addClass('hidden');
+ return this.$('.source-line-duplications-extra').removeClass('hidden');
+ },
+
+ showDuplicationPopup: function (e) {
+ var blocks, index, line, popup;
+ e.stopPropagation();
+ $('body').click();
+ index = $(e.currentTarget).data('index');
+ line = $(e.currentTarget).data('line-number');
+ blocks = this.model.get('duplications')[index - 1].blocks;
+ blocks = _.filter(blocks, function (b) {
+ return (b._ref !== '1') || (b._ref === '1' && b.from > line) || (b._ref === '1' && b.from + b.size < line);
+ });
+ popup = new DuplicationPopupView({
+ triggerEl: $(e.currentTarget),
+ model: this.model,
+ collection: new Backbone.Collection(blocks)
+ });
+ return popup.render();
+ },
+
+ showLineActionsPopup: function (e) {
+ var line, popup;
+ e.stopPropagation();
+ $('body').click();
+ line = $(e.currentTarget).data('line-number');
+ popup = new LineActionsPopupView({
+ triggerEl: $(e.currentTarget),
+ model: this.model,
+ line: line,
+ row: $(e.currentTarget).closest('.source-line')
+ });
+ popup.on('onManualIssueAdded', (function (_this) {
+ return function (data) {
+ return _this.addIssue(new Issue(data));
+ };
+ })(this));
+ return popup.render();
+ },
+
+ highlightLine: function (e) {
+ var highlighted, row;
+ row = $(e.currentTarget).closest('.source-line');
+ highlighted = row.is('.' + HIGHLIGHTED_ROW_CLASS);
+ this.$('.' + HIGHLIGHTED_ROW_CLASS).removeClass(HIGHLIGHTED_ROW_CLASS);
+ if (!highlighted) {
+ row.addClass(HIGHLIGHTED_ROW_CLASS);
+ return this.showLineActionsPopup(e);
+ }
+ },
+
+ bindScrollEvents: function () {
+ return this.$el.scrollParent().on('scroll.source-viewer', ((function (_this) {
+ return function () {
+ return _this.onScroll();
+ };
+ })(this)));
+ },
+
+ unbindScrollEvents: function () {
+ return this.$el.scrollParent().off('scroll.source-viewer');
+ },
+
+ disablePointerEvents: function () {
+ clearTimeout(this.scrollTimer);
+ $('body').addClass('disabled-pointer-events');
+ this.scrollTimer = setTimeout((function () {
+ return $('body').removeClass('disabled-pointer-events');
+ }), 250);
+ },
+
+ onScroll: function () {
+ var p, pTopOffset;
+ this.disablePointerEvents();
+ p = this.$el.scrollParent();
+ if (p.is(document)) {
+ p = $(window);
+ }
+ pTopOffset = p.offset() != null ? p.offset().top : 0;
+ if (this.model.get('hasSourceBefore') && (p.scrollTop() + pTopOffset <= this.ui.sourceBeforeSpinner.offset().top)) {
+ this.loadSourceBeforeThrottled();
+ }
+ if (this.model.get('hasSourceAfter') && (p.scrollTop() + pTopOffset + p.height() >= this.ui.sourceAfterSpinner.offset().top)) {
+ return this.loadSourceAfterThrottled();
+ }
+ },
+
+ loadSourceBefore: function () {
+ var firstLine, options, source, url;
+ this.unbindScrollEvents();
+ source = this.model.get('source');
+ firstLine = _.first(source).line;
+ url = '' + baseUrl + '/api/sources/lines';
+ options = {
+ uuid: this.model.id,
+ from: firstLine - this.LINES_AROUND,
+ to: firstLine - 1
+ };
+ return $.get(url, options, (function (_this) {
+ return function (data) {
+ source = (data.sources || []).concat(source);
+ if (source.length === 0 || (source.length > 0 && _.first(source).line === 1)) {
+ source.unshift({
+ line: 0
+ });
+ }
+ _this.model.set({
+ source: source,
+ hasSourceBefore: data.sources.length === _this.LINES_AROUND
+ });
+ _this.render();
+ _this.scrollToLine(firstLine);
+ if (_this.model.get('hasSourceBefore') || _this.model.get('hasSourceAfter')) {
+ return _this.bindScrollEvents();
+ }
+ };
+ })(this));
+ },
+
+ loadSourceAfter: function () {
+ var lastLine, options, source, url;
+ this.unbindScrollEvents();
+ source = this.model.get('source');
+ lastLine = _.last(source).line;
+ url = '' + baseUrl + '/api/sources/lines';
+ options = {
+ uuid: this.model.id,
+ from: lastLine + 1,
+ to: lastLine + this.LINES_AROUND
+ };
+ return $.get(url, options).done((function (_this) {
+ return function (data) {
+ source = source.concat(data.sources);
+ _this.model.set({
+ source: source,
+ hasSourceAfter: data.sources.length === _this.LINES_AROUND
+ });
+ _this.render();
+ if (_this.model.get('hasSourceBefore') || _this.model.get('hasSourceAfter')) {
+ return _this.bindScrollEvents();
+ }
+ };
+ })(this)).fail((function (_this) {
+ return function () {
+ _this.model.set({
+ hasSourceAfter: false
+ });
+ _this.render();
+ if (_this.model.get('hasSourceBefore') || _this.model.get('hasSourceAfter')) {
+ return _this.bindScrollEvents();
+ }
+ };
+ })(this));
+ }
+ });
+
+});
+