]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-5726 add global issue filter widget
authorStas Vilchik <vilchiks@gmail.com>
Tue, 3 Mar 2015 15:23:05 +0000 (16:23 +0100)
committerStas Vilchik <vilchiks@gmail.com>
Wed, 4 Mar 2015 12:09:21 +0000 (13:09 +0100)
16 files changed:
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/issues/IssueFilterWidget.java
plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/issues/issue_filter.html.erb
server/sonar-web/Gruntfile.coffee
server/sonar-web/src/main/hbs/widgets/_widget-issue-filter-total.hbs [new file with mode: 0644]
server/sonar-web/src/main/hbs/widgets/widget-issue-filter-action-plans.hbs [new file with mode: 0644]
server/sonar-web/src/main/hbs/widgets/widget-issue-filter-assignees.hbs [new file with mode: 0644]
server/sonar-web/src/main/hbs/widgets/widget-issue-filter-resolutions.hbs [new file with mode: 0644]
server/sonar-web/src/main/hbs/widgets/widget-issue-filter-severities.hbs [new file with mode: 0644]
server/sonar-web/src/main/hbs/widgets/widget-issue-filter-statuses.hbs [new file with mode: 0644]
server/sonar-web/src/main/hbs/widgets/widget-issue-filter.hbs [new file with mode: 0644]
server/sonar-web/src/main/js/widgets/issue-filter.js [new file with mode: 0644]
server/sonar-web/src/main/less/init/misc.less
server/sonar-web/src/main/less/init/tables.less
server/sonar-web/src/main/less/pages/dashboard.less
server/sonar-web/src/main/less/style.less
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index a8baf1053ac7fffcf4b10683de6a5c07d74d130f..f02fda9677d71b6040e096b6744acfe95311d815 100644 (file)
@@ -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";
 
index 9f0dfa07851d121595447947add7a3a823e1eaf7..0836a22ad9d7c76587913059ecbaf5725e5c0de7 100644 (file)
@@ -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 %>
index 29b2f98eb1458613bb39198edb437fe67b3da655..a6858a6a19dbe6e107a4f688c590b8fe64f35bca 100644 (file)
@@ -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 (file)
index 0000000..407209b
--- /dev/null
@@ -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 (file)
index 0000000..748c57d
--- /dev/null
@@ -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 (file)
index 0000000..030e943
--- /dev/null
@@ -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 (file)
index 0000000..2d83a97
--- /dev/null
@@ -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 (file)
index 0000000..64cb047
--- /dev/null
@@ -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 (file)
index 0000000..97dc5ec
--- /dev/null
@@ -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 (file)
index 0000000..e635a2f
--- /dev/null
@@ -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 (file)
index 0000000..de810bf
--- /dev/null
@@ -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
+          });
+        }
+      });
+    }
+  });
+
+});
index f8b31448f3d1e6256bce8a42d9e9a73d40ce55c5..3dc7e5df1ecec3b610de30c677467f45e585dc80 100644 (file)
@@ -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%;
+}
index afca8360a8877e149a31d04b8822b76c4560f311..aca9725e9657e74732196926b9fb022a8ca98703 100644 (file)
@@ -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 {
index 8dc8cfdffe6d3b27a0188a9ee4bcecb58965b64c..84fd6334170651941bef51c5d83b5400b0aea21b 100644 (file)
 .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;
index 5278454f45227b341c442732088bed4abb222f7b..806d57bfc2c8be136731aab3aba68fedb017bcfc 100644 (file)
@@ -707,7 +707,8 @@ div.barchart {
 }
 
 div.barchart > div {
-  background-color: @darkBlue;
+  min-width: 1px;
+  background-color: #c4d6e1;
   height: 0.9em;
 }
 
index 6709db02e445a5935a12ec09f1dd84dd5e694b7a..72e60258277123a57411ab0f7f32db8b6fa7faba 100644 (file)
@@ -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