From 91e29d47a06b3f5c7dd48bd19447a639b3b7de4c Mon Sep 17 00:00:00 2001 From: Stas Vilchik Date: Wed, 12 Aug 2015 11:23:22 +0200 Subject: [PATCH] SONAR-6765 SONAR-6766 show multiple issue locations and execution flows --- .../js/apps/issues/component-viewer/main.js | 2 + .../main/js/components/issue/issue-view.js | 7 +- .../js/components/issue/templates/issue.hbs | 13 +++- .../code-with-issue-locations-helper.js | 32 +++++---- .../main/js/components/source-viewer/main.js | 65 ++++++++++++++++++- .../templates/source-viewer-flow-location.hbs | 1 + .../src/main/less/components/source.less | 45 +++++++++++-- .../sonar-web/src/main/less/init/icons.less | 9 +++ .../sonar-web/src/main/less/pages/issues.less | 5 ++ server/sonar-web/src/main/less/variables.less | 2 + .../issues-with-precise-location.json | 12 ++-- server/sonar-web/test/intern.js | 3 +- .../test/medium/source-viewer.spec.js | 44 ++++++++++++- .../code-with-issue-locations-helper.spec.js | 56 ++++++++++++++++ 14 files changed, 265 insertions(+), 31 deletions(-) create mode 100644 server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-flow-location.hbs create mode 100644 server/sonar-web/test/unit/code-with-issue-locations-helper.spec.js diff --git a/server/sonar-web/src/main/js/apps/issues/component-viewer/main.js b/server/sonar-web/src/main/js/apps/issues/component-viewer/main.js index 1e79d92e45d..8792113e213 100644 --- a/server/sonar-web/src/main/js/apps/issues/component-viewer/main.js +++ b/server/sonar-web/src/main/js/apps/issues/component-viewer/main.js @@ -24,6 +24,7 @@ define([ SourceViewer.prototype.onLoaded.apply(this, arguments); this.bindShortcuts(); if (this.baseIssue != null) { + this.baseIssue.trigger('locations', this.baseIssue); return this.scrollToLine(this.baseIssue.get('line')); } }, @@ -83,6 +84,7 @@ define([ var selected = this.options.app.state.get('selectedIndex'), selectedIssue = this.options.app.list.at(selected); if (selectedIssue.get('component') === this.model.get('key')) { + selectedIssue.trigger('locations', selectedIssue); return this.scrollToIssue(selectedIssue.get('key')); } else { this.unbindShortcuts(); diff --git a/server/sonar-web/src/main/js/components/issue/issue-view.js b/server/sonar-web/src/main/js/components/issue/issue-view.js index 14e83e689e4..aad34a2d04c 100644 --- a/server/sonar-web/src/main/js/components/issue/issue-view.js +++ b/server/sonar-web/src/main/js/components/issue/issue-view.js @@ -37,7 +37,8 @@ define([ 'click .js-issue-plan': 'plan', 'click .js-issue-show-changelog': 'showChangeLog', 'click .js-issue-rule': 'showRule', - 'click .js-issue-edit-tags': 'editTags' + 'click .js-issue-edit-tags': 'editTags', + 'click .js-issue-locations': 'showLocations' }; }, @@ -217,6 +218,10 @@ define([ this.popup.render(); }, + showLocations: function () { + this.model.trigger('locations', this.model); + }, + serializeData: function () { var issueKey = encodeURIComponent(this.model.get('key')); return _.extend(Marionette.ItemView.prototype.serializeData.apply(this, arguments), { diff --git a/server/sonar-web/src/main/js/components/issue/templates/issue.hbs b/server/sonar-web/src/main/js/components/issue/templates/issue.hbs index 930e5ca5693..06e878f7b8f 100644 --- a/server/sonar-web/src/main/js/components/issue/templates/issue.hbs +++ b/server/sonar-web/src/main/js/components/issue/templates/issue.hbs @@ -4,7 +4,8 @@
- {{message}}  + {{message}}  +
@@ -22,6 +23,14 @@ {{/if}} + {{#notEmpty secondaryLocations}} +
  • + +
  • + {{/notEmpty}} +
  • @@ -143,7 +152,7 @@ {{#if updatable}} + data-confirm-msg="{{t 'issue.comment.delete_confirm_message'}}"> {{/if}} diff --git a/server/sonar-web/src/main/js/components/source-viewer/helpers/code-with-issue-locations-helper.js b/server/sonar-web/src/main/js/components/source-viewer/helpers/code-with-issue-locations-helper.js index b95c6efd18c..fd5d56a3bde 100644 --- a/server/sonar-web/src/main/js/components/source-viewer/helpers/code-with-issue-locations-helper.js +++ b/server/sonar-web/src/main/js/components/source-viewer/helpers/code-with-issue-locations-helper.js @@ -22,7 +22,8 @@ define(function () { * @returns {string} */ function part (str, from, to, acc) { - return str.substr(from - acc, to - from); + // we do not want negative number as the first argument of `substr` + return from >= acc ? str.substr(from - acc, to - from) : str.substr(0, to - from); } @@ -53,9 +54,10 @@ define(function () { * Highlight issue locations in the list of tokens * @param {Array} tokens * @param {Array} issueLocations + * @param {string} className * @returns {Array} */ - function highlightIssueLocations (tokens, issueLocations) { + function highlightIssueLocations (tokens, issueLocations, className) { issueLocations.forEach(function (location) { var nextTokens = [], acc = 0; @@ -68,8 +70,8 @@ define(function () { nextTokens.push({ className: token.className, text: p1 }); } if (p2.length) { - var newClassName = token.className.indexOf('source-line-code-issue') === -1 ? - [token.className, 'source-line-code-issue'].join(' ') : token.className; + var newClassName = token.className.indexOf(className) === -1 ? + [token.className, className].join(' ') : token.className; nextTokens.push({ className: newClassName, text: p2 }); } if (p3.length) { @@ -100,20 +102,26 @@ define(function () { * highlight issues and generate result html * @param {string} code * @param {Array} issueLocations + * @param {string} [optionalClassName] * @returns {string} */ - function doTheStuff (code, issueLocations) { + function doTheStuff (code, issueLocations, optionalClassName) { var _code = code || ' '; var _issueLocations = issueLocations || []; - return generateHTML(highlightIssueLocations(splitByTokens(_code), _issueLocations)); + var _className = optionalClassName ? optionalClassName : 'source-line-code-issue'; + return generateHTML(highlightIssueLocations(splitByTokens(_code), _issueLocations, _className)); } - /** - * Handlebars helper to highlight issue locations in the source code - */ - Handlebars.registerHelper('codeWithIssueLocations', function (code, issueLocations) { - return doTheStuff(code, issueLocations); - }); + if (typeof Handlebars !== 'undefined') { + /** + * Handlebars helper to highlight issue locations in the source code + */ + Handlebars.registerHelper('codeWithIssueLocations', function (code, issueLocations) { + return doTheStuff(code, issueLocations); + }); + } + + return doTheStuff; }); diff --git a/server/sonar-web/src/main/js/components/source-viewer/main.js b/server/sonar-web/src/main/js/components/source-viewer/main.js index 828e9e53ffc..a73b56a315f 100644 --- a/server/sonar-web/src/main/js/components/source-viewer/main.js +++ b/server/sonar-web/src/main/js/components/source-viewer/main.js @@ -39,7 +39,8 @@ define([ SCMPopupView, CoveragePopupView, DuplicationPopupView, - LineActionsPopupView) { + LineActionsPopupView, + highlightLocations) { var $ = jQuery, HIGHLIGHTED_ROW_CLASS = 'source-line-highlighted'; @@ -47,6 +48,7 @@ define([ return Marionette.LayoutView.extend({ className: 'source-viewer', template: Templates['source-viewer'], + flowLocationTemplate: Templates['source-viewer-flow-location'], ISSUES_LIMIT: 3000, LINES_LIMIT: 1000, @@ -84,6 +86,7 @@ define([ } this.issues = new Issues(); this.listenTo(this.issues, 'change:severity', this.onIssuesSeverityChange); + this.listenTo(this.issues, 'locations', this.toggleFlowLocations); this.issueViews = []; this.loadSourceBeforeThrottled = _.throttle(this.loadSourceBefore, 1000); this.loadSourceAfterThrottled = _.throttle(this.loadSourceAfter, 1000); @@ -277,8 +280,8 @@ define([ data: { componentUuids: this.model.id, f: 'component,componentId,project,subProject,rule,status,resolution,author,reporter,assignee,debt,' + - 'line,message,severity,actionPlan,creationDate,updateDate,closeDate,tags,comments,attr,actions,' + - 'transitions,actionPlanName', + 'line,message,severity,actionPlan,creationDate,updateDate,closeDate,tags,comments,attr,actions,' + + 'transitions,actionPlanName', additionalFields: '_all', resolved: false, s: 'FILE_LINE', @@ -732,6 +735,62 @@ define([ hideFilteredTooltip: function (e) { $(e.currentTarget).tooltip('destroy'); + }, + + toggleFlowLocations: function (issue) { + if (this.locationsShowFor === issue) { + this.hideFlowLocations(); + } else { + this.hideFlowLocations(); + this.showFlowLocations(issue); + } + }, + + showFlowLocations: function (issue) { + this.locationsShowFor = issue; + var primaryLocation = { + msg: issue.get('message'), + textRange: issue.get('textRange') + }, + _locations = [primaryLocation].concat(issue.get('secondaryLocations')); + _locations.forEach(this.showFlowLocation, this); + }, + + showFlowLocation: function (location) { + if (location && location.textRange) { + var line = location.textRange.startLine, + row = this.$('.source-line-code[data-line-number="' + line + '"]'), + renderedFlowLocation = this.renderFlowLocation(location); + row.append(renderedFlowLocation); + this.highlightFlowLocationInCode(location); + } + }, + + renderFlowLocation: function (location) { + location.msg = location.msg ? location.msg : ' '; + return this.flowLocationTemplate(location); + }, + + highlightFlowLocationInCode: function (location) { + for (var line = location.textRange.startLine; line <= location.textRange.endLine; line++) { + var row = this.$('.source-line-code[data-line-number="' + line + '"]'); + + // get location for the current line + var from = line === location.textRange.startLine ? location.textRange.startOffset : 0, + to = line === location.textRange.endLine ? location.textRange.endOffset : 999999, + _location = { from: from, to: to }; + + // mark issue location in the source code + var code = row.find('pre').html(), + newCode = highlightLocations(code, [_location], 'source-line-code-secondary-issue'); + row.find('pre').html(newCode); + } + }, + + hideFlowLocations: function () { + this.locationsShowFor = null; + this.$('.source-viewer-flow-location').remove(); + this.$('.source-line-code-secondary-issue').removeClass('source-line-code-secondary-issue'); } }); diff --git a/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-flow-location.hbs b/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-flow-location.hbs new file mode 100644 index 00000000000..e11d2200798 --- /dev/null +++ b/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-flow-location.hbs @@ -0,0 +1 @@ +
    {{limitString msg}}
    diff --git a/server/sonar-web/src/main/less/components/source.less b/server/sonar-web/src/main/less/components/source.less index cbe29981233..24f587f1ea5 100644 --- a/server/sonar-web/src/main/less/components/source.less +++ b/server/sonar-web/src/main/less/components/source.less @@ -21,7 +21,7 @@ @import (reference) "../variables"; @import (reference) "ui"; -@lineHeight: 18px; +@source-line-height: 18px; @lineWithIssuesBackground: #ffeaea; @duplicationColor: #f3ca8e; @@ -95,18 +95,19 @@ } .source-viewer pre { - height: @lineHeight; + height: @source-line-height; padding: 0; } .source-viewer pre, .source-meta { - line-height: @lineHeight; + line-height: @source-line-height; font-family: @monoFontFamily; font-size: 12px; } .source-line-code { + position: relative; padding: 0 10px; .issue-list { @@ -123,6 +124,12 @@ background-position: bottom; } +.source-line-code-secondary-issue { + display: inline-block; + background-color: @red; + color: #fff !important; +} + .source-meta { vertical-align: top; width: 1px; @@ -204,7 +211,7 @@ .source-line-bar { width: 5px; - height: @lineHeight; + height: @source-line-height; } .source-line-with-issues { @@ -444,3 +451,33 @@ .source-viewer-test-covered-lines { text-align: right; } + +.source-viewer-flow-location { + position: absolute; + top: 0; + right: 0; + line-height: @source-line-height - 2px; + margin: 1px 0; + padding: 0 10px; + background-color: @red; + color: #fff; + font-size: 12px; + z-index: @issue-flow-location-z-index; + + &:before { + @arrow-size: (@source-line-height - 2px) / 2; + content: " "; + position: absolute; + top: 0; + right: 100%; + display: block; + .square(0); + border-top: @arrow-size solid transparent; + border-bottom: @arrow-size solid transparent; + border-right: @arrow-size solid @red; + } +} + +.source-viewer-flow-location + .source-viewer-flow-location { + z-index: @issue-flow-location-z-index - 1; +} diff --git a/server/sonar-web/src/main/less/init/icons.less b/server/sonar-web/src/main/less/init/icons.less index ca4a8b2b49f..f0d20455d5e 100644 --- a/server/sonar-web/src/main/less/init/icons.less +++ b/server/sonar-web/src/main/less/init/icons.less @@ -536,6 +536,15 @@ a[class^="icon-"], a[class*=" icon-"] { background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/PjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+PHN2ZyB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiB2aWV3Qm94PSIwIDAgOSAxNiIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWw6c3BhY2U9InByZXNlcnZlIiBzdHlsZT0iZmlsbC1ydWxlOmV2ZW5vZGQ7Y2xpcC1ydWxlOmV2ZW5vZGQ7c3Ryb2tlLWxpbmVqb2luOnJvdW5kO3N0cm9rZS1taXRlcmxpbWl0OjEuNDE0MjE7Ij4gICAgPHBhdGggZD0iTTYuNTcxNDMsNS4xNDI4NkM2LjU3MTQzLDUuMjIwMjQgNi41NDMxNSw1LjI4NzIgNi40ODY2MSw1LjM0Mzc1QzYuNDMwMDYsNS40MDAzIDYuMzYzMSw1LjQyODU3IDYuMjg1NzEsNS40Mjg1N0M2LjIwODMzLDUuNDI4NTcgNi4xNDEzNyw1LjQwMDMgNi4wODQ4Miw1LjM0Mzc1QzYuMDI4MjcsNS4yODcyIDYsNS4yMjAyNCA2LDUuMTQyODZDNiw0Ljg2OTA1IDUuODM5MjksNC42NTc3NCA1LjUxNzg2LDQuNTA4OTNDNS4xOTY0Myw0LjM2MDEyIDQuODgwOTUsNC4yODU3MSA0LjU3MTQzLDQuMjg1NzFDNC40OTQwNSw0LjI4NTcxIDQuNDI3MDgsNC4yNTc0NCA0LjM3MDU0LDQuMjAwODlDNC4zMTM5OSw0LjE0NDM1IDQuMjg1NzEsNC4wNzczOCA0LjI4NTcxLDRDNC4yODU3MSwzLjkyMjYyIDQuMzEzOTksMy44NTU2NSA0LjM3MDU0LDMuNzk5MTFDNC40MjcwOCwzLjc0MjU2IDQuNDk0MDUsMy43MTQyOSA0LjU3MTQzLDMuNzE0MjlDNC44NjkwNSwzLjcxNDI5IDUuMTY1MTgsMy43NjE5IDUuNDU5ODIsMy44NTcxNEM1Ljc1NDQ2LDMuOTUyMzggNi4wMTMzOSw0LjExMzEgNi4yMzY2MSw0LjMzOTI5QzYuNDU5ODIsNC41NjU0OCA2LjU3MTQzLDQuODMzMzMgNi41NzE0Myw1LjE0Mjg2Wk04LDUuMTQyODZDOCw0LjcxNDI5IDcuODk3MzIsNC4zMTU0OCA3LjY5MTk2LDMuOTQ2NDNDNy40ODY2MSwzLjU3NzM4IDcuMjE4NzUsMy4yNzUzIDYuODg4MzksMy4wNDAxOEM2LjU1ODA0LDIuODA1MDYgNi4xOTE5NiwyLjYyMDU0IDUuNzkwMTgsMi40ODY2MUM1LjM4ODM5LDIuMzUyNjggNC45ODIxNCwyLjI4NTcxIDQuNTcxNDMsMi4yODU3MUM0LjE2MDcxLDIuMjg1NzEgMy43NTQ0NiwyLjM1MjY4IDMuMzUyNjgsMi40ODY2MUMyLjk1MDg5LDIuNjIwNTQgMi41ODQ4MiwyLjgwNTA2IDIuMjU0NDYsMy4wNDAxOEMxLjkyNDExLDMuMjc1MyAxLjY1NjI1LDMuNTc3MzggMS40NTA4OSwzLjk0NjQzQzEuMjQ1NTQsNC4zMTU0OCAxLjE0Mjg2LDQuNzE0MjkgMS4xNDI4Niw1LjE0Mjg2QzEuMTQyODYsNS43NDQwNSAxLjM0NTI0LDYuMjc5NzYgMS43NSw2Ljc1QzEuODA5NTIsNi44MTU0OCAxLjkwMDMsNi45MTM2OSAyLjAyMjMyLDcuMDQ0NjRDMi4xNDQzNSw3LjE3NTYgMi4yMzUxMiw3LjI3MzgxIDIuMjk0NjQsNy4zMzkyOUMzLjA1NjU1LDguMjUgMy40NzYxOSw5LjEzNjkgMy41NTM1NywxMEw1LjU4OTI5LDEwQzUuNjY2NjcsOS4xMzY5IDYuMDg2MzEsOC4yNSA2Ljg0ODIxLDcuMzM5MjlDNi45MDc3NCw3LjI3MzgxIDYuOTk4NTEsNy4xNzU2IDcuMTIwNTQsNy4wNDQ2NEM3LjI0MjU2LDYuOTEzNjkgNy4zMzMzMyw2LjgxNTQ4IDcuMzkyODYsNi43NUM3Ljc5NzYyLDYuMjc5NzYgOCw1Ljc0NDA1IDgsNS4xNDI4NlpNOS4xNDI4Niw1LjE0Mjg2QzkuMTQyODYsNi4wNjU0OCA4LjgzNjMxLDYuODYzMSA4LjIyMzIxLDcuNTM1NzFDNy45NTUzNiw3LjgyNzM4IDcuNzMzNjMsOC4wODYzMSA3LjU1ODA0LDguMzEyNUM3LjM4MjQ0LDguNTM4NjkgNy4yMDUzNiw4LjgyMjkyIDcuMDI2NzksOS4xNjUxOEM2Ljg0ODIxLDkuNTA3NDQgNi43NDcwMiw5LjgyNzM4IDYuNzIzMjEsMTAuMTI1QzcuMDAyOTgsMTAuMjkxNyA3LjE0Mjg2LDEwLjUzNTcgNy4xNDI4NiwxMC44NTcxQzcuMTQyODYsMTEuMDc3NCA3LjA2ODQ1LDExLjI2NzkgNi45MTk2NCwxMS40Mjg2QzcuMDY4NDUsMTEuNTg5MyA3LjE0Mjg2LDExLjc3OTggNy4xNDI4NiwxMkM3LjE0Mjg2LDEyLjMwOTUgNy4wMDg5MywxMi41NTA2IDYuNzQxMDcsMTIuNzIzMkM2LjgxODQ1LDEyLjg2MDEgNi44NTcxNCwxMyA2Ljg1NzE0LDEzLjE0MjlDNi44NTcxNCwxMy40MTY3IDYuNzYzMzksMTMuNjI4IDYuNTc1ODksMTMuNzc2OEM2LjM4ODM5LDEzLjkyNTYgNi4xNTc3NCwxNCA1Ljg4MzkzLDE0QzUuNzY0ODgsMTQuMjYxOSA1LjU4NjMxLDE0LjQ3MDIgNS4zNDgyMSwxNC42MjVDNS4xMTAxMiwxNC43Nzk4IDQuODUxMTksMTQuODU3MSA0LjU3MTQzLDE0Ljg1NzFDNC4yOTE2NywxNC44NTcxIDQuMDMyNzQsMTQuNzc5OCAzLjc5NDY0LDE0LjYyNUMzLjU1NjU1LDE0LjQ3MDIgMy4zNzc5OCwxNC4yNjE5IDMuMjU4OTMsMTRDMi45ODUxMiwxNCAyLjc1NDQ2LDEzLjkyNTYgMi41NjY5NiwxMy43NzY4QzIuMzc5NDYsMTMuNjI4IDIuMjg1NzEsMTMuNDE2NyAyLjI4NTcxLDEzLjE0MjlDMi4yODU3MSwxMyAyLjMyNDQsMTIuODYwMSAyLjQwMTc5LDEyLjcyMzJDMi4xMzM5MywxMi41NTA2IDIsMTIuMzA5NSAyLDEyQzIsMTEuNzc5OCAyLjA3NDQsMTEuNTg5MyAyLjIyMzIxLDExLjQyODZDMi4wNzQ0LDExLjI2NzkgMiwxMS4wNzc0IDIsMTAuODU3MUMyLDEwLjUzNTcgMi4xMzk4OCwxMC4yOTE3IDIuNDE5NjQsMTAuMTI1QzIuMzk1ODMsOS44MjczOCAyLjI5NDY0LDkuNTA3NDQgMi4xMTYwNyw5LjE2NTE4QzEuOTM3NSw4LjgyMjkyIDEuNzYwNDIsOC41Mzg2OSAxLjU4NDgyLDguMzEyNUMxLjQwOTIzLDguMDg2MzEgMS4xODc1LDcuODI3MzggMC45MTk2NDMsNy41MzU3MUMwLjMwNjU0OCw2Ljg2MzEgMCw2LjA2NTQ4IDAsNS4xNDI4NkMwLDQuNTUzNTcgMC4xMzI0NCw0LjAwNDQ2IDAuMzk3MzIxLDMuNDk1NTRDMC42NjIyMDIsMi45ODY2MSAxLjAxMDQyLDIuNTYzOTkgMS40NDE5NiwyLjIyNzY4QzEuODczNTEsMS44OTEzNyAyLjM2MTYxLDEuNjI2NDkgMi45MDYyNSwxLjQzMzA0QzMuNDUwODksMS4yMzk1OCA0LjAwNTk1LDEuMTQyODYgNC41NzE0MywxLjE0Mjg2QzUuMTM2OSwxLjE0Mjg2IDUuNjkxOTYsMS4yMzk1OCA2LjIzNjYxLDEuNDMzMDRDNi43ODEyNSwxLjYyNjQ5IDcuMjY5MzUsMS44OTEzNyA3LjcwMDg5LDIuMjI3NjhDOC4xMzI0NCwyLjU2Mzk5IDguNDgwNjUsMi45ODY2MSA4Ljc0NTU0LDMuNDk1NTRDOS4wMTA0Miw0LjAwNDQ2IDkuMTQyODYsNC41NTM1NyA5LjE0Mjg2LDUuMTQyODZaIiBzdHlsZT0iZmlsbDpyZ2IoNDUsMTM1LDE5Mik7ZmlsbC1ydWxlOm5vbnplcm87Ii8+PC9zdmc+); background-repeat: no-repeat; } +.icon-issue-flow { + position: relative; + top: 1px; + display: inline-block; + vertical-align: top; + .size(14px, 14px); + background-image: url(data:image/svg+xml,%3Csvg%20width%3D%2214%22%20height%3D%2214%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20stroke-linejoin%3D%22round%22%20stroke-miterlimit%3D%221.414%22%3E%3Cpath%20d%3D%22M2.977%2012.656c0%20.417-.142.745-.426.985-.283.24-.636.36-1.058.36-.552%200-1-.172-1.344-.516l.446-.687c.255.234.53.35.828.35.15%200%20.282-.036.394-.112.112-.075.168-.186.168-.332%200-.333-.273-.48-.82-.437l-.203-.438c.043-.052.127-.165.255-.34.127-.174.238-.315.332-.422.094-.106.19-.207.29-.3v-.008c-.084%200-.21.002-.38.008-.17.005-.296.007-.38.007v.415H.25V10h2.602v.688l-.743.898c.265.062.476.19.632.383.156.19.235.42.235.686zm.015-4.898V9H.164c-.03-.188-.047-.328-.047-.422%200-.265.06-.508.184-.726.123-.22.27-.396.442-.532.172-.135.344-.26.516-.37.172-.113.32-.226.44-.34.124-.115.185-.232.185-.352%200-.13-.038-.23-.113-.3-.076-.07-.18-.106-.31-.106-.24%200-.45.15-.632.453l-.664-.46c.125-.267.31-.474.56-.622.246-.15.52-.223.823-.223.38%200%20.7.108.96.324.26.216.39.51.39.88%200%20.26-.087.498-.264.714-.177.216-.373.384-.586.504-.214.12-.41.25-.59.394-.18.144-.272.28-.277.41h.992V7.76h.82zM14%2010.25v1.5c0%20.068-.025.126-.074.176-.05.05-.108.074-.176.074h-9.5c-.068%200-.126-.025-.176-.074-.05-.05-.074-.108-.074-.176v-1.5c0-.073.023-.133.07-.18.047-.047.107-.07.18-.07h9.5c.068%200%20.126.025.176.074.05.05.074.108.074.176zM3%203.227V4H.383v-.773h.836c0-.214%200-.532.003-.954l.004-.945v-.094H1.21c-.04.09-.17.23-.39.422l-.554-.593L1.328.07h.828v3.157H3zM14%206.25v1.5c0%20.068-.025.126-.074.176-.05.05-.108.074-.176.074h-9.5c-.068%200-.126-.025-.176-.074C4.024%207.876%204%207.818%204%207.75v-1.5c0-.073.023-.133.07-.18.047-.047.107-.07.18-.07h9.5c.068%200%20.126.025.176.074.05.05.074.108.074.176zm0-4v1.5c0%20.068-.025.126-.074.176-.05.05-.108.074-.176.074h-9.5c-.068%200-.126-.025-.176-.074C4.024%203.876%204%203.818%204%203.75v-1.5c0-.068.025-.126.074-.176.05-.05.108-.074.176-.074h9.5c.068%200%20.126.025.176.074.05.05.074.108.074.176z%22%20fill%3D%22%23236A97%22%20fill-rule%3D%22nonzero%22%2F%3E%3C%2Fsvg%3E); + background-repeat: no-repeat; +} /* diff --git a/server/sonar-web/src/main/less/pages/issues.less b/server/sonar-web/src/main/less/pages/issues.less index 0f512c1ba6c..cfa2cc22bba 100644 --- a/server/sonar-web/src/main/less/pages/issues.less +++ b/server/sonar-web/src/main/less/pages/issues.less @@ -35,6 +35,11 @@ } } + + .issue-meta-locations { + position: absolute; + visibility: hidden; + } } .issues-workspace-list-component { diff --git a/server/sonar-web/src/main/less/variables.less b/server/sonar-web/src/main/less/variables.less index 44a86acb13f..82366ca71be 100644 --- a/server/sonar-web/src/main/less/variables.less +++ b/server/sonar-web/src/main/less/variables.less @@ -216,6 +216,8 @@ @workspace-nav-z-index: 451; @workspace-viewer-z-index: 450; +@issue-flow-location-z-index: 505; + // ui elements @tooltip-z-index: 8000; @tip-z-index: 8000; diff --git a/server/sonar-web/src/test/json/source-viewer-spec/issues-with-precise-location.json b/server/sonar-web/src/test/json/source-viewer-spec/issues-with-precise-location.json index 706d0868793..abf37c14c8c 100644 --- a/server/sonar-web/src/test/json/source-viewer-spec/issues-with-precise-location.json +++ b/server/sonar-web/src/test/json/source-viewer-spec/issues-with-precise-location.json @@ -68,18 +68,18 @@ "componentId": 1875, "project": "com.sonarsource.samples:multiple-issue-locations", "subProject": "com.sonarsource.samples:multiple-issue-locations", - "line": 11, + "line": 9, "textRange": { - "startLine": 11, - "endLine": 11, + "startLine": 9, + "endLine": 9, "startOffset": 6, - "endOffset": 11 + "endOffset": 10 }, "secondaryLocations": [ { "textRange": { - "startLine": 10, - "endLine": 10, + "startLine": 8, + "endLine": 8, "startOffset": 6, "endOffset": 11 } diff --git a/server/sonar-web/test/intern.js b/server/sonar-web/test/intern.js index 42049ab87db..e17f7661681 100644 --- a/server/sonar-web/test/intern.js +++ b/server/sonar-web/test/intern.js @@ -17,7 +17,8 @@ define(['intern'], function (intern) { suites: [ 'test/unit/application.spec', 'test/unit/issue.spec', - 'test/unit/overview/card.spec' + 'test/unit/overview/card.spec', + 'test/unit/code-with-issue-locations-helper.spec' ], functionalSuites: [ diff --git a/server/sonar-web/test/medium/source-viewer.spec.js b/server/sonar-web/test/medium/source-viewer.spec.js index 27a5b96057f..95d33dbab18 100644 --- a/server/sonar-web/test/medium/source-viewer.spec.js +++ b/server/sonar-web/test/medium/source-viewer.spec.js @@ -19,8 +19,8 @@ define(function (require) { .checkElementExist('.source-line-code[data-line-number="3"] .source-line-code-issue') .checkElementInclude('.source-line-code[data-line-number="3"] .source-line-code-issue', '14 So') - .checkElementExist('.source-line-code[data-line-number="11"] .source-line-code-issue') - .checkElementInclude('.source-line-code[data-line-number="11"] .source-line-code-issue', 'arQub') + .checkElementExist('.source-line-code[data-line-number="9"] .source-line-code-issue') + .checkElementInclude('.source-line-code[data-line-number="9"] .source-line-code-issue', 'sion') .checkElementExist('.source-line-code[data-line-number="18"] .source-line-code-issue') .checkElementInclude('.source-line-code[data-line-number="18"] .source-line-code-issue', @@ -28,6 +28,46 @@ define(function (require) { .checkElementExist('.source-line-code[data-line-number="19"] .source-line-code-issue') .checkElementInclude('.source-line-code[data-line-number="19"] .source-line-code-issue', ' */'); }); + + bdd.it('should show secondary issue locations on the same line', function () { + return this.remote + .open() + .mockFromFile('/api/components/app', 'source-viewer-spec/app.json', { data: { uuid: 'uuid' } }) + .mockFromFile('/api/sources/lines', 'source-viewer-spec/lines.json', { data: { uuid: 'uuid' } }) + .mockFromFile('/api/issues/search', + 'source-viewer-spec/issues-with-precise-location.json', + { data: { componentUuids: 'uuid' } }) + .startApp('source-viewer', { file: file }) + .checkElementExist('.source-line-code[data-line-number="3"] .source-line-code-issue') + .checkElementInclude('.source-line-code[data-line-number="3"] .source-line-code-issue', '14 So') + .clickElement('.source-line-with-issues[data-line-number="3"]') + .clickElement('.js-issue-locations') + .checkElementExist('.source-line-code[data-line-number="3"] .source-viewer-flow-location') + .checkElementCount('.source-line-code[data-line-number="3"] .source-line-code-secondary-issue', 2) + .checkElementInclude('.source-line-code[data-line-number="3"] .source-line-code-secondary-issue', ') 200') + .checkElementInclude('.source-line-code[data-line-number="3"] .source-line-code-secondary-issue', '14 So'); + }); + + bdd.it('should show secondary issue locations on the different lines', function () { + return this.remote + .open() + .mockFromFile('/api/components/app', 'source-viewer-spec/app.json', { data: { uuid: 'uuid' } }) + .mockFromFile('/api/sources/lines', 'source-viewer-spec/lines.json', { data: { uuid: 'uuid' } }) + .mockFromFile('/api/issues/search', + 'source-viewer-spec/issues-with-precise-location.json', + { data: { componentUuids: 'uuid' } }) + .startApp('source-viewer', { file: file }) + .checkElementExist('.source-line-code[data-line-number="9"] .source-line-code-issue') + .checkElementInclude('.source-line-code[data-line-number="9"] .source-line-code-issue', 'sion') + .clickElement('.source-line-with-issues[data-line-number="9"]') + .clickElement('.js-issue-locations') + .checkElementExist('.source-line-code[data-line-number="8"] .source-viewer-flow-location') + .checkElementExist('.source-line-code[data-line-number="9"] .source-viewer-flow-location') + .checkElementCount('.source-line-code[data-line-number="8"] .source-line-code-secondary-issue', 1) + .checkElementCount('.source-line-code[data-line-number="9"] .source-line-code-secondary-issue', 1) + .checkElementInclude('.source-line-code[data-line-number="8"] .source-line-code-secondary-issue', 'ense ') + .checkElementInclude('.source-line-code[data-line-number="9"] .source-line-code-secondary-issue', 'sion'); + }); }); }); }); diff --git a/server/sonar-web/test/unit/code-with-issue-locations-helper.spec.js b/server/sonar-web/test/unit/code-with-issue-locations-helper.spec.js new file mode 100644 index 00000000000..a1fa2cb0a1e --- /dev/null +++ b/server/sonar-web/test/unit/code-with-issue-locations-helper.spec.js @@ -0,0 +1,56 @@ +define(function (require) { + var bdd = require('intern!bdd'); + var assert = require('intern/chai!assert'); + + var helper = require('build/js/components/source-viewer/helpers/code-with-issue-locations-helper'); + + bdd.describe('Code With Issue Locations Helper', function () { + bdd.it('should exist', function () { + assert.equal(typeof helper, 'function'); + }); + + bdd.it('should mark one location', function () { + var code = 'if (a + 1) {', + locations = [{ from: 1, to: 5 }], + result = helper(code, locations, 'x'); + assert.equal(result, + 'if (a + 1) {'); + }); + + bdd.it('should mark two locations', function () { + var code = 'abcdefghijklmnopqrst', + locations = [ + { from: 1, to: 6 }, + { from: 11, to: 16 } + ], + result = helper(code, locations, 'x'); + assert.equal(result, + ['a', + 'bcdef', + 'ghijk', + 'lmnop', + 'qrst'].join('')); + }); + + bdd.it('should mark one locations', function () { + var code = ' * Copyright (C) 2008-2014 SonarSource', + locations = [{ from: 15, to: 20 }], + result = helper(code, locations, 'x'); + assert.equal(result, + ' * Copyright (C) 2008-2014 SonarSource'); + }); + + bdd.it('should mark two locations', function () { + var code = ' * Copyright (C) 2008-2014 SonarSource', + locations = [ + { from: 24, to: 29 }, + { from: 15, to: 20 } + ], + result = helper(code, locations, 'x'); + assert.equal(result, + ' * Copyright (C) 2008-2014 SonarSource'); + // * Copyright (C) 2008-204 SonarSource + }); + }); +}); + -- 2.39.5