]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-4927 Add Quality Gate widget
authorJean-Baptiste Lievremont <jean-baptiste.lievremont@sonarsource.com>
Fri, 25 Apr 2014 13:45:08 +0000 (15:45 +0200)
committerJean-Baptiste Lievremont <jean-baptiste.lievremont@sonarsource.com>
Fri, 25 Apr 2014 13:45:16 +0000 (15:45 +0200)
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/QualityGateWidget.java [new file with mode: 0644]
plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/quality_gate.html.erb [new file with mode: 0644]
sonar-core/src/main/resources/org/sonar/l10n/core.properties
sonar-plugin-api/src/main/java/org/sonar/api/measures/CoreMetrics.java
sonar-server/src/main/webapp/WEB-INF/app/helpers/application_helper.rb
sonar-server/src/main/webapp/WEB-INF/app/models/metric.rb

index b8770197f47d2ae6f6d7bb173176f6d47ab9c25c..6268c9b7109188debdea9c5c9647604cd526fa11 100644 (file)
 package org.sonar.plugins.core;
 
 import com.google.common.collect.ImmutableList;
-import org.sonar.api.CoreProperties;
-import org.sonar.api.Properties;
-import org.sonar.api.Property;
-import org.sonar.api.PropertyType;
-import org.sonar.api.SonarPlugin;
+import org.sonar.api.*;
 import org.sonar.api.checks.NoSonarFilter;
 import org.sonar.core.timemachine.Periods;
 import org.sonar.plugins.core.batch.IndexProjectPostJob;
@@ -32,71 +28,19 @@ import org.sonar.plugins.core.charts.DistributionAreaChart;
 import org.sonar.plugins.core.charts.DistributionBarChart;
 import org.sonar.plugins.core.charts.XradarChart;
 import org.sonar.plugins.core.colorizers.JavaColorizerFormat;
-import org.sonar.plugins.core.dashboards.GlobalDefaultDashboard;
-import org.sonar.plugins.core.dashboards.ProjectDefaultDashboard;
-import org.sonar.plugins.core.dashboards.ProjectHotspotDashboard;
-import org.sonar.plugins.core.dashboards.ProjectIssuesDashboard;
-import org.sonar.plugins.core.dashboards.ProjectTimeMachineDashboard;
-import org.sonar.plugins.core.issue.CountFalsePositivesDecorator;
-import org.sonar.plugins.core.issue.CountUnresolvedIssuesDecorator;
-import org.sonar.plugins.core.issue.InitialOpenIssuesSensor;
-import org.sonar.plugins.core.issue.InitialOpenIssuesStack;
-import org.sonar.plugins.core.issue.IssueHandlers;
-import org.sonar.plugins.core.issue.IssueTracking;
-import org.sonar.plugins.core.issue.IssueTrackingDecorator;
-import org.sonar.plugins.core.issue.IssuesDensityDecorator;
-import org.sonar.plugins.core.issue.WeightedIssuesDecorator;
-import org.sonar.plugins.core.issue.notification.ChangesOnMyIssueNotificationDispatcher;
-import org.sonar.plugins.core.issue.notification.IssueChangesEmailTemplate;
-import org.sonar.plugins.core.issue.notification.NewFalsePositiveNotificationDispatcher;
-import org.sonar.plugins.core.issue.notification.NewIssuesEmailTemplate;
-import org.sonar.plugins.core.issue.notification.NewIssuesNotificationDispatcher;
-import org.sonar.plugins.core.issue.notification.SendIssueNotificationsPostJob;
+import org.sonar.plugins.core.dashboards.*;
+import org.sonar.plugins.core.issue.*;
+import org.sonar.plugins.core.issue.notification.*;
 import org.sonar.plugins.core.measurefilters.MyFavouritesFilter;
 import org.sonar.plugins.core.measurefilters.ProjectFilter;
 import org.sonar.plugins.core.notifications.alerts.NewAlerts;
 import org.sonar.plugins.core.security.ApplyProjectRolesDecorator;
-import org.sonar.plugins.core.sensors.BranchCoverageDecorator;
-import org.sonar.plugins.core.sensors.CommentDensityDecorator;
-import org.sonar.plugins.core.sensors.CoverageDecorator;
-import org.sonar.plugins.core.sensors.CoverageMeasurementFilter;
-import org.sonar.plugins.core.sensors.DirectoriesDecorator;
-import org.sonar.plugins.core.sensors.FileHashSensor;
-import org.sonar.plugins.core.sensors.FilesDecorator;
-import org.sonar.plugins.core.sensors.ItBranchCoverageDecorator;
-import org.sonar.plugins.core.sensors.ItCoverageDecorator;
-import org.sonar.plugins.core.sensors.ItLineCoverageDecorator;
-import org.sonar.plugins.core.sensors.LineCoverageDecorator;
-import org.sonar.plugins.core.sensors.ManualMeasureDecorator;
-import org.sonar.plugins.core.sensors.OverallBranchCoverageDecorator;
-import org.sonar.plugins.core.sensors.OverallCoverageDecorator;
-import org.sonar.plugins.core.sensors.OverallLineCoverageDecorator;
-import org.sonar.plugins.core.sensors.ProfileEventsSensor;
-import org.sonar.plugins.core.sensors.ProjectLinksSensor;
-import org.sonar.plugins.core.sensors.UnitTestDecorator;
-import org.sonar.plugins.core.sensors.VersionEventsSensor;
-import org.sonar.plugins.core.timemachine.NewCoverageAggregator;
-import org.sonar.plugins.core.timemachine.NewCoverageFileAnalyzer;
-import org.sonar.plugins.core.timemachine.NewItCoverageFileAnalyzer;
-import org.sonar.plugins.core.timemachine.NewOverallCoverageFileAnalyzer;
-import org.sonar.plugins.core.timemachine.TendencyDecorator;
-import org.sonar.plugins.core.timemachine.TimeMachineConfigurationPersister;
-import org.sonar.plugins.core.timemachine.VariationDecorator;
+import org.sonar.plugins.core.sensors.*;
+import org.sonar.plugins.core.timemachine.*;
 import org.sonar.plugins.core.web.TestsViewer;
 import org.sonar.plugins.core.widgets.*;
-import org.sonar.plugins.core.widgets.issues.ActionPlansWidget;
-import org.sonar.plugins.core.widgets.issues.FalsePositiveIssuesWidget;
-import org.sonar.plugins.core.widgets.issues.IssueFilterWidget;
-import org.sonar.plugins.core.widgets.issues.IssuesWidget;
-import org.sonar.plugins.core.widgets.issues.MyUnresolvedIssuesWidget;
-import org.sonar.plugins.core.widgets.issues.UnresolvedIssuesPerAssigneeWidget;
-import org.sonar.plugins.core.widgets.issues.UnresolvedIssuesStatusesWidget;
-import org.sonar.plugins.core.widgets.measures.MeasureFilterAsBubbleChartWidget;
-import org.sonar.plugins.core.widgets.measures.MeasureFilterAsCloudWidget;
-import org.sonar.plugins.core.widgets.measures.MeasureFilterAsHistogramWidget;
-import org.sonar.plugins.core.widgets.measures.MeasureFilterAsPieChartWidget;
-import org.sonar.plugins.core.widgets.measures.MeasureFilterListWidget;
-import org.sonar.plugins.core.widgets.measures.MeasureFilterTreemapWidget;
+import org.sonar.plugins.core.widgets.issues.*;
+import org.sonar.plugins.core.widgets.measures.*;
 
 import java.util.List;
 
@@ -269,6 +213,7 @@ public final class CorePlugin extends SonarPlugin {
 
       // widgets
       AlertsWidget.class,
+      QualityGateWidget.class,
       CoverageWidget.class,
       ItCoverageWidget.class,
       DescriptionWidget.class,
diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/QualityGateWidget.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/QualityGateWidget.java
new file mode 100644 (file)
index 0000000..dd7e1d0
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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 this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.plugins.core.widgets;
+
+import org.sonar.api.web.*;
+
+@WidgetProperties({
+  @WidgetProperty(key = "show_ok", type = WidgetPropertyType.BOOLEAN, defaultValue = "false"),
+})
+@WidgetLayout(WidgetLayoutType.NONE)
+public class QualityGateWidget extends CoreWidget {
+
+  public QualityGateWidget() {
+    super("quality_gate", "Quality Gate Details", "/org/sonar/plugins/core/widgets/quality_gate.html.erb");
+  }
+}
diff --git a/plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/quality_gate.html.erb b/plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/quality_gate.html.erb
new file mode 100644 (file)
index 0000000..af8143e
--- /dev/null
@@ -0,0 +1,63 @@
+<% m=measure(Metric::QUALITY_GATE_DETAILS)
+   if m && !m.measure_data.blank?
+
+     details = JSON.parse m.measure_data.data
+     m.alert_status = details['level']
+
+     warn_message = message('measure_filter.criteria.alert.warn')
+     error_message = message('measure_filter.criteria.alert.error')
+
+     css_class = "color_#{m.alert_status}"
+     if m.alert_status==Metric::TYPE_LEVEL_OK
+       label = "<b>#{message('widget.alerts.no_alert')}.</b>"
+     elsif m.alert_status==Metric::TYPE_LEVEL_WARN
+       label = "<b>#{message('widget.alerts.warnings')}</b>"
+     else
+       label = "<b>#{message('widget.alerts.errors')}</b>"
+     end
+-%><div class="widget <%= css_class -%>" id="quality_gate_widget_<%= widget.id -%>">
+  <div><%= format_measure(m) -%> <%= label -%></div>
+  <table class="data" style="color: black; margin-top: 10px">
+    <thead>
+      <tr></tr>
+    </thead>
+    <tbody>
+    <% details['conditions'].sort_by {|condition| [ -condition['level'].length, metric(condition['metric']).short_name] }.each do |condition|
+
+      level = condition['level']
+      condition_metric = metric(condition['metric'])
+      operator = message("quality_gates.operator.#{condition['op']}.short")
+      period = condition['period']
+      warning_value = condition['warning']
+      error_value = condition['error']
+      actual_value = condition['actual']
+
+      detail_measure = ProjectMeasure.new :metric => m.metric, :alert_status => level
+
+      drilldown_url = period.blank? ? url_for_drilldown(condition_metric) : url_for_drilldown(condition_metric, :period => period)
+
+      actual_measure = ProjectMeasure.new :metric => condition_metric, :value => actual_value
+      warning_measure = ProjectMeasure.new :metric => condition_metric, :value => warning_value
+      error_measure = ProjectMeasure.new :metric => condition_metric, :value => error_value
+
+
+      unless level == 'OK' && !widget_properties['show_ok']
+      -%>
+      <tr>
+        <td><%= format_measure(detail_measure) -%></td>
+        <td><%= link_to "#{condition_metric.short_name} #{period_label(@snapshot, period) unless period.blank?}", drilldown_url, {:class => 'nolink'} -%></td>
+        <td align="right"><%= link_to format_measure(actual_measure), drilldown_url, {:class => 'nolink'} -%></td>
+        <td>
+        <% if level == 'WARN' -%><%= operator -%> <%= format_measure(warning_measure) -%><% end -%>
+        <% if level == 'ERROR' -%><%= operator -%> <%= format_measure(error_measure) -%><% end -%>
+        <% if level == 'OK' -%>
+          <% unless warning_value.blank? -%><%= warn_message -%> <%= operator -%> <%= format_measure(warning_measure) -%> <%= '|' unless error_value.blank? -%><% end %>
+          <% unless error_value.blank? -%><%= error_message -%> <%= operator -%> <%= format_measure(error_measure) -%><% end %>
+        <% end -%>
+      </tr>
+    <%  end
+      end -%>
+    </tbody>
+  </table>
+</div>
+<% end %>
index 6353deb89908aa79d9372fc81c5d4bd2881fd962..ffbf75fbfe3092da667971788250758f5cb2fafb 100644 (file)
@@ -980,6 +980,10 @@ widget.alerts.no_alert=The project has passed the quality gate
 widget.alerts.warnings=The project has warnings on the following quality gate conditions:\u00a0
 widget.alerts.errors=The project failed the quality gate on the following conditions:\u00a0
 
+widget.quality_gate.name=Quality Gate Details
+widget.quality_gate.description=Displays a detailed view of the project's quality gate status.
+widget.quality_gate.property.show_ok.name=Show passed conditions
+
 widget.code_coverage.name=Unit Tests Coverage
 widget.code_coverage.description=Reports on units tests and code coverage by unit tests.
 widget.code_coverage.line_coverage.suffix=\ line coverage
@@ -1651,6 +1655,8 @@ quality_gates.add_condition=Add Condition
 quality_gates.no_conditions=No Conditions
 quality_gates.introduction=Only project measures are checked against thresholds. Sub-projects, directories and files are ignored.
 quality_gates.health_icons=Project health icons represent:
+quality_gates.metric=Metric
+quality_gates.threshold=Threshold
 quality_gates.projects_for_default=Every project not specifically associated to a quality gate will be associated to this one by default.
 quality_gates.projects_for_default.edit=You must not select specific projects for the default quality gate.
 quality_gates.projects.with=With
@@ -1663,6 +1669,10 @@ quality_gates.operator.LT=is less than
 quality_gates.operator.GT=is greater than
 quality_gates.operator.EQ=equals
 quality_gates.operator.NE=is not
+quality_gates.operator.LT.short=<
+quality_gates.operator.GT.short=>
+quality_gates.operator.EQ.short==
+quality_gates.operator.NE.short=\u2260
 quality_gates.delete.confirm.message=Are you sure you want to delete the "{0}" quality gate?
 quality_gates.delete.confirm.default=Are you sure you want to delete the "{0}" quality gate, which is the default quality gate?
 quality_gates.delete_condition=Delete Condition
index 7ff4bb07bec34eab1af4264596b732faa62c2e33..f1c7f33a454eeedfbbef1866dc74dfb1f5f1ef1b 100644 (file)
@@ -2202,8 +2202,10 @@ public final class CoreMetrics {
     .create();
 
   public static final String QUALITY_GATE_DETAILS_KEY = "quality_gate_details";
-  public static final Metric QUALITY_GATE_DETAILS = new Metric.Builder(QUALITY_GATE_DETAILS_KEY, "Quality Gate Details", Metric.ValueType.DATA)
+  public static final Metric QUALITY_GATE_DETAILS = new Metric.Builder(QUALITY_GATE_DETAILS_KEY, "Quality Gate Details", Metric.ValueType.LEVEL)
     .setDescription("The project detailed status with regard to its quality gate.")
+    .setDirection(Metric.DIRECTION_BETTER)
+    .setQualitative(true)
     .setDomain(DOMAIN_GENERAL)
     .create();
 
index 1a9ffe1c9aec7a5b22d2f03d6dcf2c3e36b03486..08227825e4b5bf2bbd64cb51ea8d0d4db5cbbd9f 100644 (file)
@@ -260,7 +260,7 @@ module ApplicationHelper
       if options[:period]
         html=m.format_numeric_value(m.variation(options[:period].to_i))
       elsif m.metric.val_type==Metric::VALUE_TYPE_LEVEL
-        html="<i class=\"icon-alert-#{m.data.downcase}\"></i>" unless m.data.blank?
+        html="<i class=\"icon-alert-#{m.alert_status.downcase}\"></i>" unless m.alert_status.blank?
       else
         html=m.formatted_value
       end
index 712cc1dc90afa60282c25f385787be269423eac5..a58c1498217c0ad00a55d9d19d18359f4762adb1 100644 (file)
@@ -75,12 +75,12 @@ class Metric < ActiveRecord::Base
   # Localized domain name
   def self.domain_for(domain_key)
     return nil if domain_key.nil?
-    
+
     localeMap = Metric.i18n_domain_cache[domain_key]
     locale = I18n.locale
-    
+
     return localeMap[locale] if localeMap && localeMap.has_key?(locale)
-    
+
     i18n_key = 'metric_domain.' + domain_key
     result = Api::Utils.message(i18n_key, :default => domain_key)
     localeMap[locale] = result if localeMap
@@ -91,7 +91,7 @@ class Metric < ActiveRecord::Base
     m=by_key(metric_key)
     m && m.short_name
   end
+
   def key
     name
   end
@@ -101,29 +101,29 @@ class Metric < ActiveRecord::Base
     return default_string unless translate
     Metric.domain_for(default_string)
   end
+
   def domain=(value)
     write_attribute(:domain, value)
   end
+
   def short_name(translate=true)
     default_string = read_attribute(:short_name)
     return default_string unless translate
-    
+
     metric_key = read_attribute(:name)
     return nil if metric_key.nil?
-    
+
     localeMap = Metric.i18n_short_name_cache[metric_key]
     locale = I18n.locale
-    
+
     return localeMap[locale] if localeMap && localeMap.has_key?(locale)
-    
+
     i18n_key = 'metric.' + metric_key + '.name'
     result = Api::Utils.message(i18n_key, :default => default_string)
     localeMap[locale] = result if localeMap
     result
   end
-  
+
   def short_name=(value)
     write_attribute(:short_name, value)
   end
@@ -133,15 +133,15 @@ class Metric < ActiveRecord::Base
     label = Api::Utils.message("metric.#{key}.name", :default => short_name) if label==''
     label
   end
+
   def description(translate=true)
     default_string = read_attribute(:description) || ''
     return default_string unless translate
 
     metric_name = read_attribute(:name)
-      
+
     return nil if metric_name.nil?
-    
+
     i18n_key = 'metric.' + metric_name + '.description'
     result = Api::Utils.message(i18n_key, :default => default_string)
     result
@@ -150,7 +150,7 @@ class Metric < ActiveRecord::Base
   def description=(value)
     write_attribute(:description, value)
   end
+
   def user_managed?
     user_managed==true
   end
@@ -170,7 +170,7 @@ class Metric < ActiveRecord::Base
   def quantitative?
     !qualitative?
   end
-  
+
   def qualitative?
     qualitative
   end
@@ -260,7 +260,7 @@ class Metric < ActiveRecord::Base
     ManualMeasure.delete_all(["metric_id = ?", id])
     self.deactivate(id)
   end
-  
+
   def self.deactivate(id)
     metric = by_id(id)
     metric.enabled = false
@@ -328,7 +328,7 @@ class Metric < ActiveRecord::Base
   DIRECTORIES = 'directories'
   ACCESSORS = 'accessors'
   PUBLIC_API = 'public_api'
-  
+
   COMPLEXITY = 'complexity'
   STATEMENTS = 'statements'
   AVG_CMPX_BY_CLASS = 'class_complexity'
@@ -353,7 +353,7 @@ class Metric < ActiveRecord::Base
   BRANCH_COVERAGE = 'branch_coverage'
   UNCOVERED_LINES='uncovered_lines'
   UNCOVERED_CONDITIONS='uncovered_conditions'
-  
+
   VIOLATIONS = 'violations'
   VIOLATIONS_DENSITY = 'violations_density'
   WEIGHTED_VIOLATIONS = 'weighted_violations'
@@ -376,6 +376,7 @@ class Metric < ActiveRecord::Base
   COMMENTED_OUT_CODE_LINES = 'commented_out_code_lines'
 
   ALERT_STATUS = 'alert_status'
+  QUALITY_GATE_DETAILS = 'quality_gate_details'
   PROFILE='profile'
 
   private
@@ -403,7 +404,7 @@ class Metric < ActiveRecord::Base
     end
     c
   end
-  
+
   def self.i18n_short_name_cache
     c = Caches.cache(I18N_SHORT_NAME_CACHE_KEY)
     if c.size==0