]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-5886 improve the creation date facet on the issues page
authorStas Vilchik <vilchiks@gmail.com>
Wed, 28 Jan 2015 09:08:19 +0000 (10:08 +0100)
committerStas Vilchik <vilchiks@gmail.com>
Wed, 4 Feb 2015 16:16:18 +0000 (17:16 +0100)
plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/issues/issues.html.erb
server/sonar-web/Gruntfile.coffee
server/sonar-web/src/main/coffee/issues/facets-view.coffee
server/sonar-web/src/main/coffee/issues/facets/creation-date-facet.coffee
server/sonar-web/src/main/coffee/issues/models/state.coffee
server/sonar-web/src/main/hbs/issues/facets/issues-creation-date-facet.hbs
server/sonar-web/src/main/js/common/handlebars-extensions.js
server/sonar-web/src/main/js/graphics/barchart.js [new file with mode: 0644]
server/sonar-web/src/main/js/graphics/timeline.js [new file with mode: 0644]
server/sonar-web/src/main/less/components/search-navigator.less
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index bf9a47204d7d00ecac4b0f61e6a851602004217b..a2e540c851177d073311be44866be1f07bc16896 100644 (file)
@@ -17,7 +17,7 @@
    new_technical_debt = @snapshot.measure('new_technical_debt')
 
    if @dashboard_configuration.selected_period?
-     period_date = @snapshot.period_datetime(@dashboard_configuration.period_index).strftime('%Y-%m-%d')
+     period_date = @snapshot.period_datetime(@dashboard_configuration.period_index).strftime('%FT%T%z')
    end
 %>
 
index d7afde755b17ba50e5e5fbfb124def41e22d1495..739811baabc1d36782ac8ebe3910aa021d5d4df3 100644 (file)
@@ -104,6 +104,8 @@ module.exports = (grunt) ->
             '<%= grunt.option("assetsDir") || pkg.assets %>js/widgets/tag-cloud.js'
             '<%= grunt.option("assetsDir") || pkg.assets %>js/widgets/treemap.js'
             '<%= grunt.option("assetsDir") || pkg.assets %>js/graphics/pie-chart.js'
+            '<%= grunt.option("assetsDir") || pkg.assets %>js/graphics/timeline.js'
+            '<%= grunt.option("assetsDir") || pkg.assets %>js/graphics/barchart.js'
             '<%= grunt.option("assetsDir") || pkg.assets %>js/top-search.js'
             '<%= grunt.option("assetsDir") || pkg.assets %>js/sortable.js'
             '<%= grunt.option("assetsDir") || pkg.assets %>js/common/inputs.js'
@@ -148,6 +150,8 @@ module.exports = (grunt) ->
             '<%= grunt.option("assetsDir") || pkg.assets %>js/widgets/tag-cloud.js'
             '<%= grunt.option("assetsDir") || pkg.assets %>js/widgets/treemap.js'
             '<%= grunt.option("assetsDir") || pkg.assets %>js/graphics/pie-chart.js'
+            '<%= grunt.option("assetsDir") || pkg.assets %>js/graphics/timeline.js'
+            '<%= grunt.option("assetsDir") || pkg.assets %>js/graphics/barchart.js'
             '<%= grunt.option("assetsDir") || pkg.assets %>js/top-search.js'
             '<%= grunt.option("assetsDir") || pkg.assets %>js/sortable.js'
             '<%= grunt.option("assetsDir") || pkg.assets %>js/common/inputs.js'
index 564e8d1f47702002b15ff39ee1baef56beff12af..a116b380b9008a107832d58a7e6070315add65ee 100644 (file)
@@ -46,7 +46,7 @@ define [
         when 'statuses' then StatusFacet
         when 'assignees' then AssigneeFacet
         when 'resolutions' then ResolutionFacet
-        when 'creationDate' then CreationDateFacet
+        when 'createdAt' then CreationDateFacet
         when 'projectUuids' then ProjectFacet
         when 'moduleUuids' then ModuleFacet
         when 'rules' then RuleFacet
index 45672f29202abf25db3bd16a960511622e82281b..98249b2da886304a1122c1337c8cdcd72ce83912 100644 (file)
@@ -15,6 +15,8 @@ define [
     events: ->
       _.extend super,
         'change input': 'applyFacet'
+        'click .js-select-period-start': 'selectPeriodStart'
+        'click .js-select-period-end': 'selectPeriodEnd'
 
 
     onRender: ->
@@ -31,6 +33,21 @@ define [
         value = query[prop]
         @$("input[name=#{prop}]").val value if value?
 
+      @$('.js-barchart').barchart @model.getValues()
+
+      @$('select').select2
+        width: '100%'
+        minimumResultsForSearch: 999
+
+
+    selectPeriodStart: ->
+      @$('.js-period-start').datepicker 'show'
+
+
+    selectPeriodEnd: ->
+      @$('.js-period-end').datepicker 'show'
+
+
 
     applyFacet: ->
       obj = {}
@@ -47,4 +64,6 @@ define [
 
     serializeData: ->
       _.extend super,
+        periodStart: @options.app.state.get('query').createdAfter
+        periodEnd: @options.app.state.get('query').createdBefore
         createdAt: @options.app.state.get('query').createdAt
index 337b585c2e75af27f57c47dceb05c9798eab9f8f..b16dea09c72cab7a2418a1321e9e2b8ef7cff883 100644 (file)
@@ -7,20 +7,20 @@ define [
       page: 1
       maxResultsReached: false
       query: {}
-      facets: ['severities', 'resolutions', 'rules', 'tags', 'projectUuids']
+      facets: ['severities', 'resolutions', 'createdAt', 'rules', 'tags', 'projectUuids']
       isContext: false
 
-      allFacets: ['issues', 'severities', 'resolutions', 'rules', 'tags', 'statuses', 'projectUuids', 'moduleUuids',
-                  'directories', 'fileUuids', 'assignees', 'reporters', 'authors', 'languages', 'actionPlans',
-                  'creationDate'],
+      allFacets: ['issues', 'severities', 'resolutions', 'createdAt', 'rules', 'tags', 'statuses', 'projectUuids',
+                  'moduleUuids', 'directories', 'fileUuids', 'assignees', 'reporters', 'authors', 'languages',
+                  'actionPlans'],
       facetsFromServer: ['severities', 'statuses', 'resolutions', 'actionPlans', 'projectUuids', 'directories', 'rules',
-                         'moduleUuids', 'tags', 'assignees', 'reporters', 'authors', 'fileUuids', 'languages'],
+                         'moduleUuids', 'tags', 'assignees', 'reporters', 'authors', 'fileUuids', 'languages',
+                         'createdAt'],
       transform: {
         'resolved': 'resolutions'
         'assigned': 'assignees'
         'planned': 'actionPlans'
-        'createdAt': 'creationDate'
-        'createdBefore': 'creationDate'
-        'createdAfter': 'creationDate'
+        'createdBefore': 'createdAt'
+        'createdAfter': 'createdAt'
       }
 
index c5b3e5dd47e7d6b055535c1d295ba730609b9d11..5aea5db910907e10abda1ce65ddbb26cb65b69e7 100644 (file)
@@ -6,9 +6,19 @@
     {{dt createdAt}} ({{fromNow createdAt}})
   </div>
 {{else}}
-  <div class="search-navigator-facet-container justify">
-    <input type="text" class="search-navigator-facet-input" name="createdAfter" placeholder="From">
-    to
-    <input type="text" class="search-navigator-facet-input" name="createdBefore" placeholder="To">
+  <div class="search-navigator-facet-container">
+    <div class="js-barchart" data-height="75"></div>
+    <div class="search-navigator-date-facet-selection">
+      <a class="js-select-period-start search-navigator-date-facet-selection-dropdown-left">
+        {{#if periodStart}}{{d periodStart}}{{else}}Past{{/if}}&nbsp;<i class="icon-dropdown"></i>
+      </a>
+      <a class="js-select-period-end search-navigator-date-facet-selection-dropdown-right">
+        {{#if periodEnd}}{{d periodEnd}}{{else}}Now{{/if}}&nbsp;<i class="icon-dropdown"></i>
+      </a>
+      <input class="js-period-start search-navigator-date-facet-selection-input-left"
+             type="text" value="{{#if periodStart}}{{ds periodStart}}{{/if}}" name="createdAfter">
+      <input class="js-period-end search-navigator-date-facet-selection-input-right"
+             type="text" value="{{#if periodEnd}}{{ds periodEnd}}{{/if}}" name="createdBefore">
+    </div>
   </div>
 {{/if}}
index 679f23af31ec79e848ead27f35c9ed049e193ba3..e72ed9b0ca81e5721c4bfd138addd90c2612cd7c 100644 (file)
     return moment(date).format('LLL');
   });
 
+  Handlebars.registerHelper('ds', function(date) {
+    return moment(date).format('YYYY-MM-DD');
+  });
+
   Handlebars.registerHelper('fromNow', function(date) {
     return moment(date).fromNow();
   });
diff --git a/server/sonar-web/src/main/js/graphics/barchart.js b/server/sonar-web/src/main/js/graphics/barchart.js
new file mode 100644 (file)
index 0000000..1331a72
--- /dev/null
@@ -0,0 +1,105 @@
+(function ($) {
+
+  function trans (left, top) {
+    return 'translate(' + left + ', ' + top + ')';
+  }
+
+  var defaults = {
+    height: 140,
+    color: '#1f77b4',
+    interpolate: 'basis'
+  };
+
+  /*
+   * data = [
+   *   { val: '2015-01-30', count: 30 },
+   *   ...
+   * ]
+   */
+
+  $.fn.barchart = function (data) {
+    $(this).each(function () {
+      var options = _.defaults($(this).data(), defaults);
+      _.extend(options, { width: $(this).width() });
+
+      var container = d3.select(this),
+          svg = container.append('svg')
+              .attr('width', options.width + 2)
+              .attr('height', options.height + 2)
+              .classed('sonar-d3', true),
+
+          plot = svg.append('g')
+              .classed('plot', true),
+
+          xScale = d3.scale.ordinal()
+              .domain(data.map(function (d, i) {
+                return i;
+              })),
+
+          yScaleMax = d3.max(data, function (d) {
+            return d.count;
+          }),
+          yScale = d3.scale.linear()
+              .domain([0, yScaleMax]);
+
+      _.extend(options, {
+        marginLeft: 1,
+        marginRight: 1,
+        marginTop: 18,
+        marginBottom: 1
+      });
+
+      _.extend(options, {
+        availableWidth: options.width - options.marginLeft - options.marginRight,
+        availableHeight: options.height - options.marginTop - options.marginBottom
+      });
+
+      plot.attr('transform', trans(options.marginLeft, options.marginTop));
+      xScale.rangeRoundBands([0, options.availableWidth], 0.05, 0);
+      yScale.range([3, options.availableHeight]);
+
+      var barWidth = xScale.rangeBand(),
+          bars = plot.selectAll('g').data(data);
+
+      if (barWidth > 0) {
+        var barsEnter = bars.enter()
+            .append('g')
+            .attr('transform', function (d, i) {
+              return trans(xScale(i), Math.ceil(options.availableHeight - yScale(d.count)));
+            });
+
+        barsEnter.append('rect')
+            .style('fill', options.color)
+            .attr('width', barWidth)
+            .attr('height', function (d) {
+              return Math.floor(yScale(d.count));
+            })
+            .attr('title', function (d, i) {
+              var beginning = moment(d.val),
+                  ending = i < data.length - 1 ? moment(data[i].val).subtract(1, 'days') : moment();
+              return d.count + ' | ' + beginning.format('LL') + ' - ' + ending.format('LL');
+            })
+            .attr('data-placement', 'right')
+            .attr('data-toggle', 'tooltip');
+
+        var maxValue = d3.max(data, function (d) {
+              return d.count;
+            }),
+            isValueShown = false;
+
+        barsEnter.append('text')
+            .classed('subtitle', true)
+            .attr('transform', trans(barWidth / 2, -4))
+            .style('text-anchor', 'middle')
+            .text(function (d) {
+              var text = !isValueShown && d.count === maxValue ? d.count : '';
+              isValueShown = d.count === maxValue;
+              return text;
+            });
+
+        $(this).find('[data-toggle=tooltip]').tooltip({ container: 'body' });
+      }
+    });
+  };
+
+})(window.jQuery);
diff --git a/server/sonar-web/src/main/js/graphics/timeline.js b/server/sonar-web/src/main/js/graphics/timeline.js
new file mode 100644 (file)
index 0000000..91ec977
--- /dev/null
@@ -0,0 +1,98 @@
+(function ($) {
+
+  function trans (left, top) {
+    return 'translate(' + left + ', ' + top + ')';
+  }
+
+  var defaults = {
+    height: 140,
+    color: '#1f77b4',
+    interpolate: 'basis'
+  };
+
+  /*
+   * data = [
+   *   { val: '2015-01-30', count: 30 },
+   *   ...
+   * ]
+   */
+
+  $.fn.timeline = function (data) {
+    $(this).each(function () {
+      var options = _.defaults($(this).data(), defaults);
+      _.extend(options, { width: $(this).width() });
+
+      var container = d3.select(this),
+          svg = container.append('svg')
+              .attr('width', options.width + 2)
+              .attr('height', options.height + 2)
+              .classed('sonar-d3', true),
+
+          plot = svg.append('g')
+              .classed('plot', true),
+
+          xScale = d3.time.scale()
+              .domain(d3.extent(data, function (d) {
+                return new Date(d.val);
+              })),
+
+          yScale = d3.scale.linear()
+              .domain([
+                0, d3.max(data, function (d) {
+                  return d.count;
+                })
+              ]),
+
+          line = d3.svg.line()
+              .x(function (d) {
+                return xScale(new Date(d.val));
+              })
+              .y(function (d) {
+                return yScale(d.count);
+              })
+              .interpolate(options.interpolate),
+
+          minDate = xScale.domain()[0],
+          minDateTick = svg.append('text')
+              .classed('subtitle', true)
+              .text(moment(minDate).format('LL')),
+
+          maxDate = xScale.domain()[1],
+          maxDateTick = svg.append('text')
+              .classed('subtitle', true)
+              .text(moment(maxDate).format('LL'))
+              .style('text-anchor', 'end');
+
+      _.extend(options, {
+        marginLeft: 1,
+        marginRight: 1,
+        marginTop: 1,
+        marginBottom: 1 + maxDateTick.node().getBBox().height
+      });
+
+      _.extend(options, {
+        availableWidth: options.width - options.marginLeft - options.marginRight,
+        availableHeight: options.height - options.marginTop - options.marginBottom
+      });
+
+      plot.attr('transform', trans(options.marginLeft, options.marginTop));
+      xScale.range([0, options.availableWidth]);
+      yScale.range([0, options.availableHeight]);
+
+      minDateTick
+          .attr('x', options.marginLeft)
+          .attr('y', options.height);
+      maxDateTick
+          .attr('x', options.width - options.marginRight)
+          .attr('y', options.height);
+
+      plot.append('path')
+          .datum(data)
+          .attr('d', line)
+          .attr('class', 'line')
+          .style('stroke', options.color);
+
+    });
+  };
+
+})(window.jQuery);
index 3ed338d48c75085b1b37a329ba8714a989aa2f6d..6e181377bfa0764d3b4f6394578ae835570a4358 100644 (file)
   padding: 0 10px 16px;
 }
 
+.search-navigator-facet-container-center {
+  text-align: center;
+}
+
 .search-navigator-facet-query {
   padding: 7px 10px;
 
   width: @sideWidth * 0.4;
 }
 
+.search-navigator-facet-histogram {
+  font-size: 0;
+
+  > li {
+    display: inline-block;
+    vertical-align: bottom;
+    width: 24px;
+    font-size: @smallFontSize;
+  }
+
+  > li > a {
+    display: block;
+    padding-bottom: 3px;
+    .link-no-underline;
+    .trans(none);
+
+    &:hover, &:focus,
+    &:hover .search-navigator-facet-histogram-bar-inner, &:focus .search-navigator-facet-histogram-bar-inner {
+      background-color: @barBorderColor;
+    }
+
+    &:hover .search-navigator-facet-histogram-bar, &:focus .search-navigator-facet-histogram-bar {
+      background-color: @blue;
+    }
+  }
+}
+
+.search-navigator-facet-histogram-large {
+  > li {
+    width: 48px;
+  }
+}
+
+.search-navigator-facet-histogram-bar {
+  display: block;
+  height: 60px;
+  background-color: @darkBlue;
+}
+
+.search-navigator-facet-histogram-bar-inner {
+  display: block;
+  max-height: 59px;
+  background-color: @barBackgroundColor;
+}
+
+.search-navigator-facet-histogram-label {
+  display: block;
+  text-align: center;
+}
+
+.search-navigator-date-facet-selection {
+  .clearfix;
+  position: relative;
+  font-size: @smallFontSize;
+}
+
+.search-navigator-date-facet-selection-dropdown-left {
+  float: left;
+  .link-no-underline;
+}
+
+.search-navigator-date-facet-selection-dropdown-right {
+  float: right;
+  .link-no-underline;
+}
+
+.search-navigator-date-facet-selection-input-left {
+  position: absolute;
+  left: 0;
+  width: 100px;
+  visibility: hidden;
+}
+
+.search-navigator-date-facet-selection-input-right {
+  position: absolute;
+  right: 0;
+  width: 100px;
+  visibility: hidden;
+}
+
+
 .search-navigator-filters {
   position: relative;
   .clearfix;
index a4d4d29b17392ebad970cc83b1ee30be44be3b3f..a4bc5ab4abd9acee59b7fa5b587e7613124439f6 100644 (file)
@@ -818,7 +818,7 @@ issues.facet.tags=Tag
 issues.facet.rules=Rule
 issues.facet.resolutions=Resolution
 issues.facet.languages=Language
-issues.facet.creationDate=Creation Date
+issues.facet.createdAt=Creation Date
 issues.facet.reporters=Reporter
 issues.facet.authors=Author
 issues.facet.issues=Issue Key