diff options
16 files changed, 436 insertions, 39 deletions
diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/issues/IssueFilterWidget.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/issues/IssueFilterWidget.java index a8baf1053ac..f02fda9677d 100644 --- a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/issues/IssueFilterWidget.java +++ b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/issues/IssueFilterWidget.java @@ -28,13 +28,15 @@ import static org.sonar.api.web.WidgetScope.GLOBAL; @WidgetScope(GLOBAL) @WidgetProperties({ @WidgetProperty(key = IssueFilterWidget.FILTER_PROPERTY, type = WidgetPropertyType.ISSUE_FILTER, optional = false), - @WidgetProperty(key = IssueFilterWidget.PAGE_SIZE_PROPERTY, type = WidgetPropertyType.INTEGER, defaultValue = "30"), + @WidgetProperty(key = IssueFilterWidget.DISTRIBUTION_AXIS_PROPERTY, type = WidgetPropertyType.SINGLE_SELECT_LIST, defaultValue = "severities", + options = {"severities", "resolutions", "statuses", "rules", "tags", "projectUuids", "assignees", "reporters", + "authors", "languages", "actionPlans", "createdAt"}), @WidgetProperty(key = IssueFilterWidget.DISPLAY_FILTER_DESCRIPTION, type = WidgetPropertyType.BOOLEAN, defaultValue = "false") }) public class IssueFilterWidget extends CoreWidget { public static final String FILTER_PROPERTY = "filter"; - public static final String PAGE_SIZE_PROPERTY = "numberOfLines"; + public static final String DISTRIBUTION_AXIS_PROPERTY = "distributionAxis"; public static final String DISPLAY_FILTER_DESCRIPTION = "displayFilterDescription"; public static final String ID = "issue_filter"; diff --git a/plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/issues/issue_filter.html.erb b/plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/issues/issue_filter.html.erb index 9f0dfa07851..0836a22ad9d 100644 --- a/plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/issues/issue_filter.html.erb +++ b/plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/issues/issue_filter.html.erb @@ -1,25 +1,38 @@ <% + container_id = 'widget-issue-filter-' + widget.id.to_s filter_id = widget_properties['filter'] filter = Internal.issues.findIssueFilterById(filter_id.to_i) - if filter - if Internal.issues.isUserAuthorized(filter) - search_options = {} - search_options['filter'] = filter_id - @widget_title = link_to h(filter.name), {:controller => 'issues', :action => 'filter', :id => filter.id} + distribution_axis = widget_properties['distributionAxis'] %> + +<% if filter %> + <% if Internal.issues.isUserAuthorized(filter) %> + + <% @widget_title = "<a href=\"#{url_for({:controller => 'issues', :action => 'index'})}#id=#{filter.id}\">#{h(filter.name)}</a>" %> <% if widget_properties['displayFilterDescription'] && !filter.description.blank? %> - <div style="padding-bottom: 5px"> - <span class="note"><%= h filter.description -%></span> - </div> + <p class="note spacer-bottom"><%= h filter.description -%></p> <% end %> - <%= render :partial => 'project/widgets/issues/issues_list_widget', - :locals => {:search_options => search_options, :widget_id => widget.id.to_s, :widget_properties => widget_properties} %> - <% - end - else - %> - <p><%= image_tag 'warning.png' %> <%= message 'widget.issue_filter.unknown_filter_warning' -%></p> -<% - end -%> + <div id="<%= container_id -%>"></div> + <script> + require(['widgets/issue-filter'], function (IssueFilter) { + window.requestMessages().done(function () { + new IssueFilter({ + el: '#<%= container_id -%>', + query: '<%= filter.data -%>', + distributionAxis: '<%= distribution_axis -%>' + }); + }); + }); + </script> + + <% else %> + + <p class="message-alert"><%= message 'widget.issue_filter.insufficient_privileges_warning' -%></p> + + <% end %> +<% else %> + + <p class="message-alert"><%= message 'widget.issue_filter.unknown_filter_warning' -%></p> + +<% end %> diff --git a/server/sonar-web/Gruntfile.coffee b/server/sonar-web/Gruntfile.coffee index 29b2f98eb14..a6858a6a19d 100644 --- a/server/sonar-web/Gruntfile.coffee +++ b/server/sonar-web/Gruntfile.coffee @@ -243,6 +243,10 @@ module.exports = (grunt) -> name: 'nav/app' out: '<%= grunt.option("assetsDir") || pkg.assets %>build/js/nav/app.js' + issueFilterWidget: options: + name: 'widgets/issue-filter' + out: '<%= grunt.option("assetsDir") || pkg.assets %>build/js/widgets/issue-filter.js' + handlebars: options: @@ -296,6 +300,9 @@ module.exports = (grunt) -> '<%= grunt.option("assetsDir") || pkg.assets %>js/templates/nav.js': [ '<%= pkg.sources %>hbs/nav/**/*.hbs' ] + '<%= grunt.option("assetsDir") || pkg.assets %>js/templates/widgets.js': [ + '<%= pkg.sources %>hbs/widgets/**/*.hbs' + ] clean: diff --git a/server/sonar-web/src/main/hbs/widgets/_widget-issue-filter-total.hbs b/server/sonar-web/src/main/hbs/widgets/_widget-issue-filter-total.hbs new file mode 100644 index 00000000000..407209bb5ab --- /dev/null +++ b/server/sonar-web/src/main/hbs/widgets/_widget-issue-filter-total.hbs @@ -0,0 +1,11 @@ +<tr> + <td> + <a href="{{link '/issues/search#' query}}"><strong>{{t 'total'}}</strong></a> + </td> + <td class="text-right"><strong>{{total}}</strong></td> + <td class="barchart"> + <div class="barchart" style="width: 100%;"> + <div style="width: 100%;"></div> + </div> + </td> +</tr> diff --git a/server/sonar-web/src/main/hbs/widgets/widget-issue-filter-action-plans.hbs b/server/sonar-web/src/main/hbs/widgets/widget-issue-filter-action-plans.hbs new file mode 100644 index 00000000000..748c57df613 --- /dev/null +++ b/server/sonar-web/src/main/hbs/widgets/widget-issue-filter-action-plans.hbs @@ -0,0 +1,22 @@ +<table class="data zebra"> + {{> '_widget-issue-filter-total'}} + {{#each items}} + <tr> + <td> + {{#eq val ''}} + <a href="{{issueFilterItemLink ../../parsedQuery 'planned' 'false'}}">{{t 'issue.unplanned'}}</a> + {{else}} + <a href="{{issueFilterItemLink ../../parsedQuery 'actionPlans' val}}">{{default label val}}</a> + {{/eq}} + </td> + <td class="text-right nowrap"> + {{numberShort count}} + </td> + <td class="barchart"> + <div class="barchart" style="width: 100%;"> + <div style="width: {{percent count ../total}};"></div> + </div> + </td> + </tr> + {{/each}} +</table> diff --git a/server/sonar-web/src/main/hbs/widgets/widget-issue-filter-assignees.hbs b/server/sonar-web/src/main/hbs/widgets/widget-issue-filter-assignees.hbs new file mode 100644 index 00000000000..030e94370c1 --- /dev/null +++ b/server/sonar-web/src/main/hbs/widgets/widget-issue-filter-assignees.hbs @@ -0,0 +1,22 @@ +<table class="data zebra"> + {{> '_widget-issue-filter-total'}} + {{#each items}} + <tr> + <td> + {{#eq val ''}} + <a href="{{issueFilterItemLink ../../parsedQuery 'assigned' 'false'}}">{{t 'unassigned'}}</a> + {{else}} + <a href="{{issueFilterItemLink ../../parsedQuery 'assignees' val}}">{{default label val}}</a> + {{/eq}} + </td> + <td class="text-right nowrap"> + {{numberShort count}} + </td> + <td class="barchart"> + <div class="barchart" style="width: 100%;"> + <div style="width: {{percent count ../total}};"></div> + </div> + </td> + </tr> + {{/each}} +</table> diff --git a/server/sonar-web/src/main/hbs/widgets/widget-issue-filter-resolutions.hbs b/server/sonar-web/src/main/hbs/widgets/widget-issue-filter-resolutions.hbs new file mode 100644 index 00000000000..2d83a97ba4a --- /dev/null +++ b/server/sonar-web/src/main/hbs/widgets/widget-issue-filter-resolutions.hbs @@ -0,0 +1,22 @@ +<table class="data zebra"> + {{> '_widget-issue-filter-total'}} + {{#each items}} + <tr> + <td> + {{#eq val ''}} + <a href="{{issueFilterItemLink ../../parsedQuery 'resolved' 'false'}}">{{t 'unresolved'}}</a> + {{else}} + <a href="{{issueFilterItemLink ../../parsedQuery 'resolutions' val}}">{{t 'issue.resolution' val}}</a> + {{/eq}} + </td> + <td class="text-right nowrap"> + {{numberShort count}} + </td> + <td class="barchart"> + <div class="barchart" style="width: 100%;"> + <div style="width: {{percent count ../total}};"></div> + </div> + </td> + </tr> + {{/each}} +</table> diff --git a/server/sonar-web/src/main/hbs/widgets/widget-issue-filter-severities.hbs b/server/sonar-web/src/main/hbs/widgets/widget-issue-filter-severities.hbs new file mode 100644 index 00000000000..64cb0477d9f --- /dev/null +++ b/server/sonar-web/src/main/hbs/widgets/widget-issue-filter-severities.hbs @@ -0,0 +1,19 @@ +<table class="data zebra"> + <tr> + <td> + <a href="{{link '/issues/search#' query}}"><strong>{{t 'total'}}</strong></a> + </td> + <td class="text-right"><strong>{{total}}</strong></td> + </tr> + {{#each items}} + <tr> + <td> + {{severityIcon val}} + <a href="{{issueFilterItemLink ../parsedQuery ../property val}}">{{t 'severity' val}}</a> + </td> + <td class="text-right nowrap"> + {{numberShort count}} + </td> + </tr> + {{/each}} +</table> diff --git a/server/sonar-web/src/main/hbs/widgets/widget-issue-filter-statuses.hbs b/server/sonar-web/src/main/hbs/widgets/widget-issue-filter-statuses.hbs new file mode 100644 index 00000000000..97dc5ec5e70 --- /dev/null +++ b/server/sonar-web/src/main/hbs/widgets/widget-issue-filter-statuses.hbs @@ -0,0 +1,27 @@ +<table class="data zebra"> + <tr> + <td><strong>{{t 'total'}}</strong></td> + <td class="text-right"><strong>{{total}}</strong></td> + <td class="barchart"> + <div class="barchart" style="width: 100%;"> + <div style="width: 100%;"></div> + </div> + </td> + </tr> + {{#each items}} + <tr> + <td> + {{statusIcon val}} + <a href="{{issueFilterItemLink ../parsedQuery ../property val}}">{{t 'issue.status' val}}</a> + </td> + <td class="text-right nowrap"> + {{numberShort count}} + </td> + <td class="barchart"> + <div class="barchart" style="width: 100%;"> + <div style="width: {{percent count ../total}};"></div> + </div> + </td> + </tr> + {{/each}} +</table> diff --git a/server/sonar-web/src/main/hbs/widgets/widget-issue-filter.hbs b/server/sonar-web/src/main/hbs/widgets/widget-issue-filter.hbs new file mode 100644 index 00000000000..e635a2fdcde --- /dev/null +++ b/server/sonar-web/src/main/hbs/widgets/widget-issue-filter.hbs @@ -0,0 +1,18 @@ +<table class="data zebra"> + {{> '_widget-issue-filter-total'}} + {{#each items}} + <tr> + <td> + <a href="{{searchLink}}">{{default label val}}</a> + </td> + <td class="text-right nowrap"> + {{numberShort count}} + </td> + <td class="barchart"> + <div class="barchart" style="width: 100%;"> + <div style="width: {{percent count ../total}};"></div> + </div> + </td> + </tr> + {{/each}} +</table> diff --git a/server/sonar-web/src/main/js/widgets/issue-filter.js b/server/sonar-web/src/main/js/widgets/issue-filter.js new file mode 100644 index 00000000000..de810bf8e66 --- /dev/null +++ b/server/sonar-web/src/main/js/widgets/issue-filter.js @@ -0,0 +1,218 @@ +define(['templates/widgets'], function () { + + var $ = jQuery, + defaultComparator = function (item) { + return -item.count; + }, + defaultFilter = function (item) { + var items = this.query[this.property]; + return items == null || + (items != null && items.split(',').indexOf(item.val) !== -1); + }, + defaultLabel = function (item) { + return item.val; + }, + defaultLink = function (item, property, query) { + var criterion = {}; + criterion[property] = item.val; + var r = _.extend({}, query, criterion); + return baseUrl + '/issues/search#' + getQuery(r); + }, + byDistributionConf = { + 'severities': { + template: 'widget-issue-filter-severities', + comparator: function (item) { + var order = ['BLOCKER', 'CRITICAL', 'MAJOR', 'MINOR', 'INFO']; + return order.indexOf(item.val); + } + }, + 'statuses': { + template: 'widget-issue-filter-statuses', + comparator: function (item) { + var order = ['OPEN', 'REOPENED', 'CONFIRMED', 'RESOLVED', 'CLOSED']; + return order.indexOf(item.val); + } + }, + 'resolutions': { + template: 'widget-issue-filter-resolutions', + comparator: function (item) { + var order = ['', 'FALSE-POSITIVE', 'WONTFIX', 'FIXED', 'REMOVED']; + return order.indexOf(item.val); + } + }, + 'rules': { + label: function (item, r) { + if (_.isArray(r.rules)) { + var rule = _.findWhere(r.rules, { key: item.val }); + if (rule != null) { + return rule.name; + } + } + } + }, + 'projectUuids': { + label: function (item, r) { + if (_.isArray(r.projects)) { + var project = _.findWhere(r.projects, { uuid: item.val }); + if (project != null) { + return project.name; + } + } + } + }, + 'assignees': { + template: 'widget-issue-filter-assignees', + label: function (item, r) { + if (_.isArray(r.users)) { + var user = _.findWhere(r.users, { login: item.val }); + if (user != null) { + return user.name; + } + } + } + }, + 'languages': { + label: function (item, r) { + if (_.isArray(r.languages)) { + var lang = _.findWhere(r.languages, { key: item.val }); + if (lang != null) { + return lang.name; + } + } + } + }, + 'actionPlans': { + template: 'widget-issue-filter-action-plans', + label: function (item, r) { + if (_.isArray(r.actionPlans)) { + var actionPlan = _.findWhere(r.actionPlans, { key: item.val }); + if (actionPlan != null) { + return actionPlan.name; + } + } + } + }, + 'createdAt': { + comparator: function (item) { + return moment(item.val).toDate(); + }, + label: function (item, r, items, index, query) { + var beginning = moment(item.val), + endDate = query.createdBefore != null ? moment(query.createdBefore) : moment(), + ending = index < items.length - 1 ? moment(items[index + 1].val).subtract(1, 'days') : endDate, + isSameDay = ending.diff(beginning, 'days') <= 1; + return beginning.format('LL') + (isSameDay ? '' : (' – ' + ending.format('LL'))); + }, + link: function (item, property, query, index, items) { + var createdAfter = moment(item.val), + endDate = query.createdBefore != null ? moment(query.createdBefore) : moment(), + createdBefore = index < items.length - 1 ? moment(items[index + 1].val).subtract(1, 'days') : endDate, + isSameDay = createdBefore.diff(createdAfter, 'days') <= 1; + if (isSameDay) { + createdBefore.add(1, 'days'); + } + var r = _.extend({}, query, { + createdAfter: createdAfter.format('YYYY-MM-DD'), + createdBefore: createdBefore.format('YYYY-MM-DD') + }); + return baseUrl + '/issues/search#' + getQuery(r); + } + } + }; + + function getQuery (query, separator) { + separator = separator || '|'; + var route = []; + _.forEach(query, function (value, property) { + route.push('' + property + '=' + encodeURIComponent(value)); + }); + return route.join(separator); + } + + Handlebars.registerHelper('issueFilterItemLink', function (query, property, value) { + var criterion = {}; + criterion[property] = value; + var r = _.extend({}, query, criterion); + return baseUrl + '/issues/search#' + getQuery(r); + }); + + return Marionette.ItemView.extend({ + + getTemplate: function () { + var template = this.conf != null && this.conf.template != null ? this.conf.template : 'widget-issue-filter'; + return Templates[template]; + }, + + initialize: function () { + this.model = new Backbone.Model({ + query: this.options.query, + parsedQuery: this.getParsedQuery(), + property: this.options.distributionAxis + }); + this.listenTo(this.model, 'change', this.render); + this.conf = byDistributionConf[this.options.distributionAxis]; + this.query = this.getParsedQuery(); + this.requestIssues(); + }, + + getParsedQuery: function () { + var queryString = this.options.query || '', + query = {}; + queryString.split('|').forEach(function (criterionString) { + var criterion = criterionString.split('='); + if (criterion.length === 2) { + query[criterion[0]] = criterion[1]; + } + }); + return query; + }, + + sortItems: function (items) { + var comparator = this.conf != null && this.conf.comparator != null ? this.conf.comparator : defaultComparator; + return _.sortBy(items, comparator); + }, + + filterItems: function (items) { + var filter = this.conf != null && this.conf.filter != null ? this.conf.filter : defaultFilter; + return _.filter(items, filter, { query: this.query, property: this.options.distributionAxis }); + }, + + withLink: function (items) { + var link = this.conf != null && this.conf.link != null ? this.conf.link : defaultLink, + property = this.options.distributionAxis, + query = this.model.get('parsedQuery'); + return items.map(function (item, index) { + return _.extend(item, { searchLink: link(item, property, query, index, items) }); + }); + }, + + withLabels: function (items) { + var label = this.conf != null && this.conf.label != null ? this.conf.label : defaultLabel, + r = this.model.get('rawResponse'), + query = this.model.get('parsedQuery'); + return items.map(function (item, index) { + return _.extend(item, { label: label(item, r, items, index, query) }); + }); + }, + + requestIssues: function () { + var that = this, + url = baseUrl + '/api/issues/search', + options = _.extend({}, this.query, { + ps: 1, + facets: this.options.distributionAxis + }); + return $.get(url, options).done(function (r) { + if (_.isArray(r.facets) && r.facets.length === 1) { + // save response object, but do not trigger repaint + that.model.set({ rawResponse: r }, { silent: true }); + that.model.set({ + items: that.sortItems(that.withLabels(that.withLink(that.filterItems(r.facets[0].values)))), + total: r.total + }); + } + }); + } + }); + +}); diff --git a/server/sonar-web/src/main/less/init/misc.less b/server/sonar-web/src/main/less/init/misc.less index f8b31448f3d..3dc7e5df1ec 100644 --- a/server/sonar-web/src/main/less/init/misc.less +++ b/server/sonar-web/src/main/less/init/misc.less @@ -103,3 +103,19 @@ td.spacer-top { .bordered-top { border-top: 1px solid @barBorderColor; } + +.zero-font-size { + font-size: 0 !important; +} + +.width-100 { + width: 100%; +} + +.width-80 { + width: 80%; +} + +.width-60 { + width: 60%; +} diff --git a/server/sonar-web/src/main/less/init/tables.less b/server/sonar-web/src/main/less/init/tables.less index afca8360a88..aca9725e965 100644 --- a/server/sonar-web/src/main/less/init/tables.less +++ b/server/sonar-web/src/main/less/init/tables.less @@ -53,8 +53,9 @@ table.data > tfoot > tr > td { } table.data > tbody > tr > td { - padding: 5px; + padding: 4px 5px; vertical-align: text-top; + line-height: 20px; } table.data td.small, table.data th.small { diff --git a/server/sonar-web/src/main/less/pages/dashboard.less b/server/sonar-web/src/main/less/pages/dashboard.less index 8dc8cfdffe6..84fd6334170 100644 --- a/server/sonar-web/src/main/less/pages/dashboard.less +++ b/server/sonar-web/src/main/less/pages/dashboard.less @@ -325,22 +325,6 @@ .widget-span-11 { width: 91.666666666667%; } .widget-span-12 { width: 100%; } -@media (max-width: 1279px) { - .widget-span-1 { width: 50%; } - .widget-span-2 { width: 50%; } - .widget-span-3 { width: 50%; } - .widget-span-3-5 { width: 50%; } - .widget-span-4 { width: 50%; } - .widget-span-5 { width: 50%; } - .widget-span-6 { width: 50%; } - .widget-span-7 { width: 100%; } - .widget-span-8 { width: 100%; } - .widget-span-9 { width: 100%; } - .widget-span-10 { width: 100%; } - .widget-span-11 { width: 100%; } - .widget-span-12 { width: 100%; } -} - .widget-label { display: block; font-size: @baseFontSize; diff --git a/server/sonar-web/src/main/less/style.less b/server/sonar-web/src/main/less/style.less index 5278454f452..806d57bfc2c 100644 --- a/server/sonar-web/src/main/less/style.less +++ b/server/sonar-web/src/main/less/style.less @@ -707,7 +707,8 @@ div.barchart { } div.barchart > div { - background-color: @darkBlue; + min-width: 1px; + background-color: #c4d6e1; height: 0.9em; } diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 6709db02e44..72e60258277 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -157,6 +157,7 @@ template=Template title=Title to=To to.downcase=to +total=Total treemap=Treemap true=True type=Type @@ -1358,9 +1359,22 @@ widget.action_plans.x_unresolved_issues={0} unresolved issues widget.issue_filter.name=Issue Filter widget.issue_filter.description=Displays the result of a pre-configured issue filter. widget.issue_filter.property.filter.name=Filter -widget.issue_filter.property.numberOfLines.name=Page size +widget.issue_filter.property.distributionAxis.name=Distribution Axis widget.issue_filter.property.displayFilterDescription.name=Display Filter Description widget.issue_filter.unknown_filter_warning=This widget is configured to display an issue filter that doesn't exist anymore. +widget.issue_filter.insufficient_privileges_warning=Widget cannot be displayed: insufficient privileges. +widget.issue_filter.property.distributionAxis.option.severities.name=By Severity +widget.issue_filter.property.distributionAxis.option.projectUuids.name=By Project +widget.issue_filter.property.distributionAxis.option.statuses.name=By Status +widget.issue_filter.property.distributionAxis.option.createdAt.name=New Issues +widget.issue_filter.property.distributionAxis.option.actionPlans.name=By Action Plan +widget.issue_filter.property.distributionAxis.option.assignees.name=By Assignee +widget.issue_filter.property.distributionAxis.option.tags.name=By Tag +widget.issue_filter.property.distributionAxis.option.rules.name=By Rule +widget.issue_filter.property.distributionAxis.option.resolutions.name=By Resolution +widget.issue_filter.property.distributionAxis.option.languages.name=By Language +widget.issue_filter.property.distributionAxis.option.reporters.name=By Reporter +widget.issue_filter.property.distributionAxis.option.authors.name=By Author widget.issue_tag_cloud.name=Project Issue Tag Cloud widget.issue_tag_cloud.title=Issue Tag Cloud |