diff --git a/core/src/main/java/hudson/PluginManager.java b/core/src/main/java/hudson/PluginManager.java index e45a438098..b01c865cd4 100644 --- a/core/src/main/java/hudson/PluginManager.java +++ b/core/src/main/java/hudson/PluginManager.java @@ -1557,6 +1557,10 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas releaseTimestamp.put("displayValue", Messages.PluginManager_ago(Functions.getTimeSpanString(plugin.releaseTimestamp))); jsonObject.put("releaseTimestamp", releaseTimestamp); } + if (plugin.healthScore != null) { + jsonObject.put("healthScore", plugin.healthScore); + jsonObject.put("healthScoreClass", plugin.healthScoreClass); + } return jsonObject; }) .collect(toList()); diff --git a/core/src/main/java/hudson/PluginWrapper.java b/core/src/main/java/hudson/PluginWrapper.java index c8cd9f5240..82d13afc18 100644 --- a/core/src/main/java/hudson/PluginWrapper.java +++ b/core/src/main/java/hudson/PluginWrapper.java @@ -222,6 +222,8 @@ public class PluginWrapper implements Comparable, ModelObject { */ private static Set CORE_ONLY_DEPENDANT = Set.of("jenkins-core"); + private Integer healthScore; + /** * Set the list of components that depend on this plugin. * @param dependents The list of components that depend on this plugin. @@ -423,6 +425,32 @@ public class PluginWrapper implements Comparable, ModelObject { } + @Restricted(NoExternalUse.class) // Jelly use only + public Integer getHealthScore() { + if (this.healthScore == null) { + this.healthScore = getInfoFromAllSites().stream() + .filter(Objects::nonNull) + .filter(p -> p.healthScore != null) + .findFirst() + .map(plugin -> plugin.healthScore) + .orElse(null); + } + return this.healthScore; + } + + @Restricted(NoExternalUse.class) + public static String getHealthScoreClassForScore(int score) { + if (score > 80) return "top"; + if (score > 60) return "middle"; + return "bottom"; + } + + @Restricted(NoExternalUse.class) // Jelly use only + public String getHealthScoreClass() { + if (this.healthScore == null) return null; + return getHealthScoreClassForScore(this.healthScore); + } + @ExportedBean public static final class Dependency { @Exported diff --git a/core/src/main/java/hudson/model/UpdateCenter.java b/core/src/main/java/hudson/model/UpdateCenter.java index c4e7880325..cdfaa69156 100644 --- a/core/src/main/java/hudson/model/UpdateCenter.java +++ b/core/src/main/java/hudson/model/UpdateCenter.java @@ -765,6 +765,20 @@ public class UpdateCenter extends AbstractModelObject implements Loadable, Savea return result; } + @Restricted(NoExternalUse.class) + public boolean isHealthScoresAvailable() { + for (UpdateSite site : sites) { + final Data data = site.getData(); + if (data == null) { + continue; + } + if (data.healthScoresAvailable) { + return true; + } + } + return false; + } + private boolean checkMinVersion(@CheckForNull Plugin p, @CheckForNull VersionNumber minVersion) { return p != null && (minVersion == null || !minVersion.isNewerThan(new VersionNumber(p.version))); diff --git a/core/src/main/java/hudson/model/UpdateSite.java b/core/src/main/java/hudson/model/UpdateSite.java index e75cfc8cbe..b02be5286d 100644 --- a/core/src/main/java/hudson/model/UpdateSite.java +++ b/core/src/main/java/hudson/model/UpdateSite.java @@ -611,6 +611,9 @@ public class UpdateSite { */ public final String connectionCheckUrl; + @Restricted(NoExternalUse.class) + public final boolean healthScoresAvailable; + Data(JSONObject o) { this.sourceId = Util.intern((String) o.get("id")); JSONObject c = o.optJSONObject("core"); @@ -649,6 +652,8 @@ public class UpdateSite { } } + boolean healthScoresAvailable = false; + for (Map.Entry e : (Set>) o.getJSONObject("plugins").entrySet()) { Plugin p = new Plugin(sourceId, e.getValue()); // JENKINS-33308 - include implied dependencies for older plugins that may need them @@ -662,6 +667,10 @@ public class UpdateSite { } plugins.put(Util.intern(e.getKey()), p); + if (p.healthScore != null) { + healthScoresAvailable = true; + } + // compatibility with update sites that have no separate 'deprecated' top-level entry. // Also do this even if there are deprecations to potentially allow limiting the top-level entry to overridden URLs. if (p.hasCategory("deprecated")) { @@ -671,6 +680,8 @@ public class UpdateSite { } } + this.healthScoresAvailable = healthScoresAvailable; + connectionCheckUrl = (String) o.get("connectionCheckUrl"); } @@ -1256,6 +1267,12 @@ public class UpdateSite { @Restricted(NoExternalUse.class) public IssueTracker[] issueTrackers; + @Restricted(NoExternalUse.class) + public final Integer healthScore; + + @Restricted(NoExternalUse.class) + public final String healthScoreClass; + @DataBoundConstructor public Plugin(String sourceId, JSONObject o) { super(sourceId, o, UpdateSite.this.url); @@ -1292,6 +1309,12 @@ public class UpdateSite { int optionalDepCount = (int) ja.stream().filter(IS_DEP_PREDICATE.and(IS_NOT_OPTIONAL.negate())).count(); dependencies = getPresizedMutableMap(depCount); optionalDependencies = getPresizedMutableMap(optionalDepCount); + this.healthScore = o.has("health") ? o.getInt("health") : null; + if (healthScore != null) { + this.healthScoreClass = PluginWrapper.getHealthScoreClassForScore(healthScore); + } else { + this.healthScoreClass = null; + } for (Object jo : o.getJSONArray("dependencies")) { JSONObject depObj = (JSONObject) jo; diff --git a/core/src/main/resources/hudson/PluginManager/available.jelly b/core/src/main/resources/hudson/PluginManager/available.jelly index 73e095bd01..ef71ceb256 100644 --- a/core/src/main/resources/hudson/PluginManager/available.jelly +++ b/core/src/main/resources/hudson/PluginManager/available.jelly @@ -72,7 +72,8 @@ THE SOFTWARE.
+ data-hasAdmin="${h.hasPermission(app.ADMINISTER)}" + data-health="${app.updateCenter.healthScoresAvailable}"> @@ -82,6 +83,14 @@ THE SOFTWARE. + + + diff --git a/core/src/main/resources/hudson/PluginManager/available.properties b/core/src/main/resources/hudson/PluginManager/available.properties new file mode 100644 index 0000000000..49683e4d14 --- /dev/null +++ b/core/src/main/resources/hudson/PluginManager/available.properties @@ -0,0 +1,3 @@ +healthTooltip=\ + A score evaluating the health of a plugin, using metrics such as actively maintained, recommended repository configuration applied and documentation provided. \ + Learn more diff --git a/core/src/main/resources/hudson/PluginManager/available_fr.properties b/core/src/main/resources/hudson/PluginManager/available_fr.properties index 394582ca5a..35e1413a60 100644 --- a/core/src/main/resources/hudson/PluginManager/available_fr.properties +++ b/core/src/main/resources/hudson/PluginManager/available_fr.properties @@ -26,3 +26,4 @@ Install\ after\ restart=Installer après le redémarrage Install=Installer Name=Nom Released=Publié +Health=Santé diff --git a/core/src/main/resources/hudson/PluginManager/installed.jelly b/core/src/main/resources/hudson/PluginManager/installed.jelly index 81b175b955..3c9e356fce 100644 --- a/core/src/main/resources/hudson/PluginManager/installed.jelly +++ b/core/src/main/resources/hudson/PluginManager/installed.jelly @@ -78,6 +78,14 @@ THE SOFTWARE. + + + + + + + @@ -225,6 +233,20 @@ THE SOFTWARE. + + +
${%Name} ${%Released} + ${%healthTooltip} + + ${%Health} + +
${%Name} + ${%healthTooltip} + + ${%Health} + + ${%Enabled} @@ -161,6 +169,22 @@ THE SOFTWARE. + + + + + + + + + + + + + + +
diff --git a/core/src/main/resources/hudson/PluginManager/installed.properties b/core/src/main/resources/hudson/PluginManager/installed.properties index e08e426afc..6e9d6982bc 100644 --- a/core/src/main/resources/hudson/PluginManager/installed.properties +++ b/core/src/main/resources/hudson/PluginManager/installed.properties @@ -34,3 +34,6 @@ adoptThisPlugin=\ reportIssue=Report an issue with this plugin uninstall-title=Are you sure you want to uninstall {0}? uninstall-description=This will remove the plugin binary from your $JENKINS_HOME, but it will leave the configuration files of the plugin untouched +healthTooltip=\ + A score evaluating the health of a plugin, using metrics such as actively maintained, recommended repository configuration applied and documentation provided. \ + Learn more diff --git a/core/src/main/resources/hudson/PluginManager/installed_fr.properties b/core/src/main/resources/hudson/PluginManager/installed_fr.properties index 6857fd97e6..e34245c6f3 100644 --- a/core/src/main/resources/hudson/PluginManager/installed_fr.properties +++ b/core/src/main/resources/hudson/PluginManager/installed_fr.properties @@ -32,3 +32,4 @@ Uninstall=Désinstaller Version=Version Previously\ installed\ version=Version précédente downgradeTo=Rétrograder à {0} +Health=Santé diff --git a/core/src/main/resources/hudson/PluginManager/updates.jelly b/core/src/main/resources/hudson/PluginManager/updates.jelly index 5ce8a810ff..c921b8167f 100644 --- a/core/src/main/resources/hudson/PluginManager/updates.jelly +++ b/core/src/main/resources/hudson/PluginManager/updates.jelly @@ -84,6 +84,14 @@ THE SOFTWARE.
${%Name} ${%Released} ${%Installed} + ${%healthTooltip} + + ${%Health} + +
+ + + + + + + + + + +
diff --git a/core/src/main/resources/hudson/PluginManager/updates.properties b/core/src/main/resources/hudson/PluginManager/updates.properties index 521f5a0158..d5d5e0cdf2 100644 --- a/core/src/main/resources/hudson/PluginManager/updates.properties +++ b/core/src/main/resources/hudson/PluginManager/updates.properties @@ -48,3 +48,6 @@ adoptThisPlugin=\ deprecationWarning=This plugin is deprecated. In general, this means that it is either obsolete, no longer being developed, or may no longer work. Learn more. loading=Loading CompatibleTooltip=Selects plugin updates that are compatible with their current version +healthTooltip=\ + A score evaluating the health of a plugin, using metrics such as actively maintained, recommended repository configuration applied and documentation provided. \ + Learn more diff --git a/core/src/main/resources/hudson/PluginManager/updates_fr.properties b/core/src/main/resources/hudson/PluginManager/updates_fr.properties index b17716de3e..8aca839871 100644 --- a/core/src/main/resources/hudson/PluginManager/updates_fr.properties +++ b/core/src/main/resources/hudson/PluginManager/updates_fr.properties @@ -33,3 +33,4 @@ No\ updates\ available=Aucune mise à jour disponible Version=Version compatWarning=Avertissement: Cette nouvelle version n''est pas compatible avec la version installée. Il peut être nécessaire de reconfigurer les tâches utilisant ce plugin. coreWarning=Ce plugin est conçu pour Jenkins {0} ou une version plus récente. Il pourrait ne pas fonctionner avec votre version de Jenkins. +Health=Santé diff --git a/src/main/js/plugin-manager-ui.js b/src/main/js/plugin-manager-ui.js index 7ef187deae..68ced0d0b0 100644 --- a/src/main/js/plugin-manager-ui.js +++ b/src/main/js/plugin-manager-ui.js @@ -12,6 +12,7 @@ function applyFilter(searchQuery) { var pluginsTable = document.getElementById("plugins"); var tbody = pluginsTable.querySelector("tbody"); var admin = pluginsTable.dataset.hasadmin === "true"; + var includeHealthScores = pluginsTable.dataset.health === "true"; var selectedPlugins = []; var filterInput = document.getElementById("filter-box"); @@ -43,6 +44,7 @@ function applyFilter(searchQuery) { (plugin) => selectedPlugins.indexOf(plugin.name) === -1, ), admin, + includeHealthScores, }); tbody.insertAdjacentHTML("beforeend", rows); diff --git a/src/main/js/templates/plugin-manager/available.hbs b/src/main/js/templates/plugin-manager/available.hbs index 115a7b4d8e..de4888a549 100644 --- a/src/main/js/templates/plugin-manager/available.hbs +++ b/src/main/js/templates/plugin-manager/available.hbs @@ -61,13 +61,31 @@ {{/if}} {{#if this.releaseTimestamp }} - + {{else}} - + + {{/if}} + {{#if @root/includeHealthScores }} + +
+ {{#if this.healthScore }} + + {{ this.healthScore }} + + {{else}} +
+ + +
+ {{/if}} +
+ {{/if}} {{/each}} diff --git a/src/main/scss/components/_healthScore.scss b/src/main/scss/components/_healthScore.scss new file mode 100644 index 0000000000..a2c8c1842f --- /dev/null +++ b/src/main/scss/components/_healthScore.scss @@ -0,0 +1,33 @@ +.jenkins-healthScore { + &--badge { + --healthscore-badge-border-width: 4px; + --healthscore-badge-size: 32px; + + width: var(--healthscore-badge-size); + height: var(--healthscore-badge-size); + border-radius: 50%; + border-width: var(--badge-border-width); + border-style: solid; + border-color: var(--accent-color); + display: table-cell; + vertical-align: middle; + text-align: center; + background-color: var(--background); + + &:hover { + background-color: var(--light-grey); + } + } + + &--top { + border-color: var(--success-color); + } + + &--middle { + border-color: var(--warning-color); + } + + &--bottom { + border-color: var(--destructive-color); + } +} diff --git a/src/main/scss/components/_index.scss b/src/main/scss/components/_index.scss index 684c31aa0e..55da7be818 100644 --- a/src/main/scss/components/_index.scss +++ b/src/main/scss/components/_index.scss @@ -9,6 +9,7 @@ @use "content-blocks"; @use "dialogs"; @use "dropdowns"; +@use "healthScore"; @use "icons"; @use "notice"; @use "notifications"; diff --git a/test/src/test/java/hudson/PluginManagerInstalledGUITest.java b/test/src/test/java/hudson/PluginManagerInstalledGUITest.java index 7358befb54..4927767185 100644 --- a/test/src/test/java/hudson/PluginManagerInstalledGUITest.java +++ b/test/src/test/java/hudson/PluginManagerInstalledGUITest.java @@ -149,6 +149,7 @@ public class PluginManagerInstalledGUITest { private InstalledPlugins() throws IOException, SAXException { JenkinsRule.WebClient webClient = jenkinsRule.createWebClient(); HtmlPage installedPage = webClient.goTo("pluginManager/installed"); + final boolean healthScoresAvailable = jenkinsRule.jenkins.getUpdateCenter().isHealthScoresAvailable(); // Note for debugging... simply print installedPage to get the JenkinsRule // Jenkins URL and then add a long Thread.sleep here. It's useful re being @@ -159,7 +160,7 @@ public class PluginManagerInstalledGUITest { installedPlugins = new ArrayList<>(); for (DomElement htmlTableRow : tbody.getChildElements()) { - installedPlugins.add(new InstalledPlugin((HtmlTableRow) htmlTableRow)); + installedPlugins.add(new InstalledPlugin((HtmlTableRow) htmlTableRow, healthScoresAvailable)); } } @@ -178,9 +179,11 @@ public class PluginManagerInstalledGUITest { private class InstalledPlugin { private final HtmlTableRow pluginRow; + private final boolean hasHealth; - InstalledPlugin(HtmlTableRow pluginRow) { + InstalledPlugin(HtmlTableRow pluginRow, boolean hasHealth) { this.pluginRow = pluginRow; + this.hasHealth = hasHealth; } public String getId() { @@ -192,7 +195,7 @@ public class PluginManagerInstalledGUITest { } private HtmlInput getEnableWidget() { - HtmlElement input = pluginRow.getCells().get(1).getElementsByTagName("input").get(0); + HtmlElement input = pluginRow.getCells().get(hasHealth ? 2 : 1).getElementsByTagName("input").get(0); return (HtmlInput) input; }