]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-2701 New "Time Machine" widget
authorFabrice Bellingard <bellingard@gmail.com>
Thu, 25 Aug 2011 14:29:16 +0000 (16:29 +0200)
committerFabrice Bellingard <bellingard@gmail.com>
Thu, 25 Aug 2011 14:29:16 +0000 (16:29 +0200)
Allows to display measures of required metrics by past version

plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/TimeMachineWidget.java [new file with mode: 0644]
plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/time_machine.html.erb [new file with mode: 0644]
plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/timeline.html.erb
plugins/sonar-l10n-en-plugin/src/main/resources/org/sonar/l10n/core.properties
sonar-server/src/main/webapp/WEB-INF/app/models/snapshot.rb
sonar-server/src/main/webapp/stylesheets/style.css

index 4a30f0daac8e7f1d2211ab3be255d4e4702789f3..fd6b705e6bc348b6b3065bc5016395d7ce34c843 100644 (file)
@@ -215,6 +215,7 @@ public class CorePlugin extends SonarPlugin {
     extensions.add(EventsWidget.class);
     extensions.add(CustomMeasuresWidget.class);
     extensions.add(TimelineWidget.class);
+    extensions.add(TimeMachineWidget.class);
 
     // chart
     extensions.add(XradarChart.class);
diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/TimeMachineWidget.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/TimeMachineWidget.java
new file mode 100644 (file)
index 0000000..5d8af7e
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2011 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * Sonar is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+ */
+package org.sonar.plugins.core.widgets;
+
+import org.sonar.api.web.AbstractRubyTemplate;
+import org.sonar.api.web.RubyRailsWidget;
+import org.sonar.api.web.WidgetProperties;
+import org.sonar.api.web.WidgetProperty;
+import org.sonar.api.web.WidgetPropertyType;
+
+@WidgetProperties(
+    {
+        @WidgetProperty(key = "numberOfVersions", type = WidgetPropertyType.INTEGER, defaultValue = "4"),
+        @WidgetProperty(key = "displaySparkLine", type = WidgetPropertyType.BOOLEAN),
+        @WidgetProperty(key = "metric1", type = WidgetPropertyType.METRIC, defaultValue = "ncloc"),
+        @WidgetProperty(key = "metric2", type = WidgetPropertyType.METRIC),
+        @WidgetProperty(key = "metric3", type = WidgetPropertyType.METRIC),
+        @WidgetProperty(key = "metric4", type = WidgetPropertyType.METRIC),
+        @WidgetProperty(key = "metric5", type = WidgetPropertyType.METRIC),
+        @WidgetProperty(key = "metric6", type = WidgetPropertyType.METRIC),
+        @WidgetProperty(key = "metric7", type = WidgetPropertyType.METRIC),
+        @WidgetProperty(key = "metric8", type = WidgetPropertyType.METRIC),
+        @WidgetProperty(key = "metric9", type = WidgetPropertyType.METRIC),
+        @WidgetProperty(key = "metric10", type = WidgetPropertyType.METRIC)
+    }
+)
+public class TimeMachineWidget extends AbstractRubyTemplate implements RubyRailsWidget {
+  public String getId() {
+    return "time_machine";
+  }
+
+  public String getTitle() {
+    return "Time Machine";
+  }
+
+  @Override
+  protected String getTemplatePath() {
+    return "/org/sonar/plugins/core/widgets/time_machine.html.erb";
+    //return "/Users/fbellingard/Documents/Sonar/repos/sonar/plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/time_machine.html.erb";
+  }
+}
\ No newline at end of file
diff --git a/plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/time_machine.html.erb b/plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/time_machine.html.erb
new file mode 100644 (file)
index 0000000..e1d0aee
--- /dev/null
@@ -0,0 +1,114 @@
+<%
+  # Retrieve widget settings
+  metric_ids = []
+  (1..10).each do |index|
+    metric=widget_properties["metric#{index}"]
+    if metric
+      metric_ids << metric.id
+    end
+  end
+  if metric_ids.empty?
+    # No metric has been selected, it's the first time the widget is displayed: 'ncloc' is the default metric
+    ncloc = Metric.find(:first, :conditions => "name = 'ncloc'")
+    metric_ids << ncloc.id
+  end
+  numberOfVersions = widget_properties["numberOfVersions"].to_i == 0 ? 4 : widget_properties["numberOfVersions"].to_i
+  displaySparkLine = widget_properties["displaySparkLine"]
+  
+  # Retrieve the measures for each metric on each snapshot
+  options = {}
+  from_date = dashboard_configuration.from_datetime
+  if from_date
+    options[:from] = from_date
+  end
+  snapshots=Snapshot.for_timemachine_widget(@resource, numberOfVersions, options)
+  sids = snapshots.collect{|s| s.id}.uniq
+  measures=ProjectMeasure.find(:all, :conditions => {:snapshot_id => sids, :metric_id => metric_ids})
+
+  # And prepare the rows to display
+  snapshot_by_id={}
+  snapshots.each do |s|
+    snapshot_by_id[s.id]=s
+  end
+  rows_by_metric_id={}
+  rows=[]
+  measures.each do |measure|
+    next unless measure.metric
+
+    if measure.metric.timemachine? && (measure.value || measure.text_value)
+      row=rows_by_metric_id[measure.metric_id]
+      unless row
+        row=Sonar::TimemachineRow.new(measure.metric)
+        rows<<row
+        rows_by_metric_id[measure.metric_id]=row
+      end
+
+      #optimization : avoid eager loading of snapshots
+      measure.snapshot=snapshot_by_id[measure.snapshot_id]
+      row.add_measure(measure)
+    end
+  end
+  
+  rows.sort!
+%>
+
+<div class="widget-matrix">
+
+<table class="widget-matrix">
+
+  <thead>
+    <tr>
+      <th valign="top" nowrap="nowrap" style="background-color: #FFFFFF; border: 0px;"> 
+      </th>
+      <% snapshots.each do |snapshot| %>
+        <th nowrap="nowrap" align="right" valign="top">
+            <%= l snapshot.created_at.to_date %>
+            <br/>
+            <% snapshot.user_events.each do |event| %>
+                <b><%= event.name %></b>
+                <br/>
+            <% end %>
+          </th>
+      <% end %>
+      <th> </th>
+  </tr>
+  </thead>
+
+  <tbody>
+    <%
+      previous_domain=''
+      rows.select{|row| row.metric.val_type != Metric::VALUE_TYPE_DISTRIB}.each do |row|
+        if previous_domain != row.domain 
+    %>
+    <tr>
+      <td class="title" colspan="<%= snapshots.size + 2 -%>"><%= row.domain %></td>
+    </tr>
+    <% 
+        end
+        previous_domain = row.domain 
+    %>
+    <tr class="<%= cycle("even", "odd", :name => row.domain) -%>">
+      <td width="1%" nowrap="nowrap" valign="top">
+        <%= row.metric.short_name %>
+      </td>
+    <% 
+      snapshots.each do |snapshot|
+        measure=row.measure(snapshot)
+    %>
+      <td align="right" width="1%" nowrap="nowrap"><%= format_measure(measure, :skip_span_id => true) %></td>
+    <% end %>
+      <td>
+    <%
+      sparkline_url=row.sparkline_url
+      if displaySparkLine && sparkline_url
+    %>
+        <%= image_tag(sparkline_url) %>
+    <% end %>
+      </td>
+    </tr>
+    <% end %>
+  </tbody>
+
+</table>
+
+</div>
\ No newline at end of file
index ad44fbac17aa27c96fdf3d880636a084de44472d..806d77f7a7c29e743ab7cf212d1fc5e67de62647 100644 (file)
@@ -1,5 +1,6 @@
 <%= javascript_include_tag 'protovis-sonar' %>
 <%
+  # Retrieve widget settings
   metric_data_map = {}
   metric_name_map = {}
   (1..3).each do |index|
   end
   if metric_data_map.empty?
     # No metric has been selected, it's the first time the widget is displayed: 'ncloc' is the default metric
-    ncloc = Metric.find(:all, :conditions => "name = 'ncloc'").first
+    ncloc = Metric.find(:first, :conditions => "name = 'ncloc'")
     metric_data_map[ncloc.id] = []
     metric_name_map[ncloc.id] = message('metric.ncloc.name')
   end
+  chartHeight = widget_properties["chartHeight"].to_i == 0 ? "null" : widget_properties["chartHeight"]
   
   # Retrieve metric trend information
   options = {}
   js_translations = "{"
   js_translations += "\"date\":\"" + message("date") + "\""
   js_translations += "}"
-  
-  # Check if the widget height was specified
-  chartHeight = widget_properties["chartHeight"].to_i == 0 ? "null" : widget_properties["chartHeight"]
+    
 %>
 
 <% if widget_properties["chartTitle"] %>
index c0754fd8e2ff3a9504cc84945aba6aa1d60efa61..97560a509c034b70c528b521647b65f1e1586eda 100644 (file)
@@ -514,6 +514,9 @@ widget.timeline.name=Timeline
 widget.timeline.description=Displays up to 3 metrics on a history chart.
 widget.timeline.timeline_not_displayed=The timeline won't be displayed as currently only 1 snapshot is available.
 
+widget.time_machine.name=Time Machine
+widget.time_machine.description=Displays up to 10 metrics in a table, showing their value for a specified number of past snapshots.
+
 widget.ckjm.name=Chidamber & Kemerer
 widget.ckjm.description=Reports on LCOM4 and RFC average and distribution.
 widget.ckjm.lcom4=LCOM4
index 5a6fd0ea838e5fb65c6ce1569c498eca1f4ddef3..bc1d8d5e9ce33a9b2445303cec222b1290cbc10e 100644 (file)
@@ -67,6 +67,26 @@ class Snapshot < ActiveRecord::Base
 
     snapshots.compact.uniq
   end
+  
+  def self.for_timemachine_widget(resource, number_of_versions, options={})
+    conditions = ["events.category=? AND events.resource_id=?", "Version", resource.id]
+    if (options[:from])
+      conditions[0] += " AND events.event_date>=?"
+      conditions << options[:from]
+    end
+    
+    events = Event.find(:all, :conditions => conditions, :order => 'events.event_date ASC')
+    events_to_display = events
+    if number_of_versions < events.size
+      events_to_display = [events.first] + events[events.size-number_of_versions+1 .. events.size-1]
+    end
+    sids = []
+    events_to_display.each() do |event|
+      sids << event.snapshot_id
+    end
+    
+    snapshots=Snapshot.find(:all, :conditions => ["snapshots.id IN (?)", sids], :include => 'events', :order => 'snapshots.created_at ASC')
+  end
 
   def last?
     islast
index 39f8811455814836518067be44a908be997ef811..00f2bfb027fd7cfba8475f490aa4fab13ab1b0bf 100644 (file)
@@ -1792,6 +1792,37 @@ table.matrix tbody td.title {
   padding: 5px 0 0 5px;
 }
 
+div.widget-matrix {
+ overflow:auto;
+}
+
+table.widget-matrix {
+  font-size: 12px;
+}
+
+table.widget-matrix thead {
+  background-color: #CAE3F2;
+}
+
+table.widget-matrix thead th {
+  text-align: right;
+  border: 1px solid #4b9fd5;
+  padding: 2px;
+  font-size: 11px;
+}
+
+table.widget-matrix tbody td.title {
+  border: none;
+  font-weight: bold;
+  padding: 5px 0 0 0px;
+}
+
+table.widget-matrix tbody td {
+  border: 1px solid #ddd;
+  margin: 0;
+  padding: 1px 0 1px 10px;
+}
+
 
 a.nolink, .dashbox a, .dashbox a:visited {
   text-decoration: none;