From: Jean-Baptiste Lievremont Date: Fri, 25 Apr 2014 13:45:08 +0000 (+0200) Subject: SONAR-4927 Add Quality Gate widget X-Git-Tag: 4.4-RC1~1381^2~7 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=433fc34a5a8513fe80dc6cd8ad758afde5bfd0a7;p=sonarqube.git SONAR-4927 Add Quality Gate widget --- diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java index b8770197f47..6268c9b7109 100644 --- a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java +++ b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java @@ -20,11 +20,7 @@ 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 index 00000000000..dd7e1d0cf85 --- /dev/null +++ b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/QualityGateWidget.java @@ -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 index 00000000000..af8143e1a58 --- /dev/null +++ b/plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/quality_gate.html.erb @@ -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 = "#{message('widget.alerts.no_alert')}." + elsif m.alert_status==Metric::TYPE_LEVEL_WARN + label = "#{message('widget.alerts.warnings')}" + else + label = "#{message('widget.alerts.errors')}" + end +-%>
+
<%= format_measure(m) -%> <%= label -%>
+ + + + + + <% 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'] + -%> + + + + + + <% end + end -%> + +
<%= format_measure(detail_measure) -%><%= link_to "#{condition_metric.short_name} #{period_label(@snapshot, period) unless period.blank?}", drilldown_url, {:class => 'nolink'} -%><%= link_to format_measure(actual_measure), drilldown_url, {:class => 'nolink'} -%> + <% 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 -%> +
+
+<% end %> 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 6353deb8990..ffbf75fbfe3 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -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 diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/measures/CoreMetrics.java b/sonar-plugin-api/src/main/java/org/sonar/api/measures/CoreMetrics.java index 7ff4bb07bec..f1c7f33a454 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/measures/CoreMetrics.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/measures/CoreMetrics.java @@ -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(); diff --git a/sonar-server/src/main/webapp/WEB-INF/app/helpers/application_helper.rb b/sonar-server/src/main/webapp/WEB-INF/app/helpers/application_helper.rb index 1a9ffe1c9ae..08227825e4b 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/helpers/application_helper.rb +++ b/sonar-server/src/main/webapp/WEB-INF/app/helpers/application_helper.rb @@ -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="" unless m.data.blank? + html="" unless m.alert_status.blank? else html=m.formatted_value end diff --git a/sonar-server/src/main/webapp/WEB-INF/app/models/metric.rb b/sonar-server/src/main/webapp/WEB-INF/app/models/metric.rb index 712cc1dc90a..a58c1498217 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/models/metric.rb +++ b/sonar-server/src/main/webapp/WEB-INF/app/models/metric.rb @@ -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