diff --git a/core/src/main/java/hudson/PluginManager.java b/core/src/main/java/hudson/PluginManager.java index 623de0f137..491d30c72a 100644 --- a/core/src/main/java/hudson/PluginManager.java +++ b/core/src/main/java/hudson/PluginManager.java @@ -520,9 +520,33 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas } } + // Redo who depends on who. + resolveDependantPlugins(); + LOGGER.info("Plugin " + p.getShortName()+":"+p.getVersion() + " dynamically installed"); } + @Restricted(NoExternalUse.class) + public void resolveDependantPlugins() { + for (PluginWrapper plugin : plugins) { + Set dependants = new HashSet<>(); + for (PluginWrapper possibleDependant : plugins) { + // The plugin could have just been deleted. If so, it doesn't + // count as a dependant. + if (possibleDependant.isDeleted()) { + continue; + } + List dependencies = possibleDependant.getDependencies(); + for (Dependency dependency : dependencies) { + if (dependency.shortName.equals(plugin.getShortName())) { + dependants.add(possibleDependant.getShortName()); + } + } + } + plugin.setDependants(dependants); + } + } + /** * If the war file has any "/WEB-INF/plugins/[*.jpi | *.hpi]", extract them into the plugin directory. * diff --git a/core/src/main/java/hudson/PluginWrapper.java b/core/src/main/java/hudson/PluginWrapper.java index 50fad48550..e5fc9e76bc 100644 --- a/core/src/main/java/hudson/PluginWrapper.java +++ b/core/src/main/java/hudson/PluginWrapper.java @@ -24,6 +24,7 @@ */ package hudson; +import com.google.common.collect.ImmutableSet; import hudson.PluginManager.PluginInstanceStore; import hudson.model.Api; import hudson.model.ModelObject; @@ -40,7 +41,10 @@ import java.io.OutputStream; import java.io.Closeable; import java.net.URL; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.List; +import java.util.Set; import java.util.jar.Manifest; import java.util.logging.Logger; import static java.util.logging.Level.WARNING; @@ -57,6 +61,7 @@ import java.util.Enumeration; import java.util.jar.JarFile; import java.util.logging.Level; import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; /** * Represents a Jenkins plug-in and associated control information @@ -150,6 +155,54 @@ public class PluginWrapper implements Comparable, ModelObject { */ /*package*/ boolean isBundled; + /** + * List of plugins that depend on this plugin. + */ + private Set dependants = Collections.emptySet(); + + /** + * The core can depend on a plugin if it is bundled. Sometimes it's the only thing that + * depends on the plugin e.g. UI support library bundle plugin. + */ + private static Set CORE_ONLY_DEPENDANT = ImmutableSet.copyOf(Arrays.asList("jenkins-core")); + + /** + * Set the list of components that depend on this plugin. + * @param dependants The list of components that depend on this plugin. + */ + public void setDependants(@Nonnull Set dependants) { + this.dependants = dependants; + } + + /** + * Get the list of components that depend on this plugin. + * @return The list of components that depend on this plugin. + */ + public @Nonnull Set getDependants() { + if (isBundled && dependants.isEmpty()) { + return CORE_ONLY_DEPENDANT; + } else { + return dependants; + } + } + + /** + * Does this plugin have anything that depends on it. + * @return {@code true} if something (Jenkins core, or another plugin) depends on this + * plugin, otherwise {@code false}. + */ + public boolean hasDependants() { + return (isBundled || !dependants.isEmpty()); + } + + /** + * Does this plugin depend on any other plugins. + * @return {@code true} if this plugin depends on other plugins, otherwise {@code false}. + */ + public boolean hasDependencies() { + return (dependencies != null && !dependencies.isEmpty()); + } + @ExportedBean public static final class Dependency { @Exported @@ -634,6 +687,10 @@ public class PluginWrapper implements Comparable, ModelObject { public HttpResponse doDoUninstall() throws IOException { Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER); archive.delete(); + + // Redo who depends on who. + Jenkins.getActiveInstance().getPluginManager().resolveDependantPlugins(); + return HttpResponses.redirectViaContextPath("/pluginManager/installed"); // send back to plugin manager } diff --git a/core/src/main/java/jenkins/model/Jenkins.java b/core/src/main/java/jenkins/model/Jenkins.java index 4a156ab862..a4714fb597 100644 --- a/core/src/main/java/jenkins/model/Jenkins.java +++ b/core/src/main/java/jenkins/model/Jenkins.java @@ -864,6 +864,9 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve System.currentTimeMillis()-itemListenerStart,l.getClass().getName())); } + // All plugins are loaded. Now we can figure out who depends on who. + resolveDependantPlugins(); + if (LOG_STARTUP_PERFORMANCE) LOGGER.info(String.format("Took %dms for complete Jenkins startup", System.currentTimeMillis()-start)); @@ -872,6 +875,19 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve } } + private void resolveDependantPlugins() throws InterruptedException, ReactorException, IOException { + TaskGraphBuilder graphBuilder = new TaskGraphBuilder(); + + graphBuilder.add("Resolving Dependant Plugins Graph", new Executable() { + @Override + public void run(Reactor reactor) throws Exception { + pluginManager.resolveDependantPlugins(); + } + }); + + executeReactor(null, graphBuilder); + } + /** * Executes a reactor. * diff --git a/core/src/main/resources/hudson/PluginManager/_table.js b/core/src/main/resources/hudson/PluginManager/_table.js index b84bae0806..f6fede2004 100644 --- a/core/src/main/resources/hudson/PluginManager/_table.js +++ b/core/src/main/resources/hudson/PluginManager/_table.js @@ -44,3 +44,305 @@ Behaviour.specify("#filter-box", '_table', 0, function(e) { e.onkeyup = applyFilter; }); + +/** + * Code for handling the enable/disable behavior based on plugin + * dependencies and dependants. + */ +(function(){ + function selectAll(selector, element) { + if (element) { + return $(element).select(selector); + } else { + return Element.select(undefined, selector); + } + } + function select(selector, element) { + var elementsBySelector = selectAll(selector, element); + if (elementsBySelector.length > 0) { + return elementsBySelector[0]; + } else { + return undefined; + } + } + + /** + * Wait for document onload. + */ + Element.observe(window, "load", function() { + var pluginsTable = select('#plugins'); + var pluginTRs = selectAll('.plugin', pluginsTable); + + if (!pluginTRs) { + return; + } + + // Create a map of the plugin rows, making it easy to index them. + var plugins = {}; + for (var i = 0; i < pluginTRs.length; i++) { + var pluginTR = pluginTRs[i]; + var pluginId = pluginTR.readAttribute('data-plugin-id'); + + plugins[pluginId] = pluginTR; + } + + function getPluginTR(pluginId) { + return plugins[pluginId]; + } + function getPluginName(pluginId) { + return getPluginTR(pluginId).readAttribute('data-plugin-name'); + } + + function processSpanSet(spans) { + var ids = []; + for (var i = 0; i < spans.length; i++) { + var span = spans[i]; + var pluginId = span.readAttribute('data-plugin-id'); + var pluginName = getPluginName(pluginId); + + span.update(pluginName); + ids.push(pluginId); + } + return ids; + } + + function markAllDependantsDisabled(pluginTR) { + var jenkinsPluginMetadata = pluginTR.jenkinsPluginMetadata; + var dependantIds = jenkinsPluginMetadata.dependantIds; + + if (dependantIds) { + for (var i = 0; i < dependantIds.length; i++) { + var dependantPluginTr = getPluginTR(dependantIds[i]); + if (dependantPluginTr && dependantPluginTr.jenkinsPluginMetadata.enableInput.checked) { + // One of the plugins that depend on this plugin, is marked as enabled. + pluginTR.removeClassName('all-dependants-disabled'); + return; + } + } + } + + pluginTR.addClassName('all-dependants-disabled'); + } + + function markHasDisabledDependencies(pluginTR) { + var jenkinsPluginMetadata = pluginTR.jenkinsPluginMetadata; + var dependencyIds = jenkinsPluginMetadata.dependencyIds; + + if (dependencyIds) { + for (var i = 0; i < dependencyIds.length; i++) { + var dependencyPluginTr = getPluginTR(dependencyIds[i]); + if (dependencyPluginTr && !dependencyPluginTr.jenkinsPluginMetadata.enableInput.checked) { + // One of the plugins that this plugin depend on, is marked as disabled. + pluginTR.addClassName('has-disabled-dependency'); + return; + } + } + } + + pluginTR.removeClassName('has-disabled-dependency'); + } + + function setEnableWidgetStates() { + for (var i = 0; i < pluginTRs.length; i++) { + markAllDependantsDisabled(pluginTRs[i]); + markHasDisabledDependencies(pluginTRs[i]); + } + } + + function addDependencyInfoRow(pluginTR, infoTR) { + infoTR.addClassName('plugin-dependency-info'); + pluginTR.insert({ + after: infoTR + }); + } + function removeDependencyInfoRow(pluginTR) { + var nextRows = pluginTR.nextSiblings(); + if (nextRows && nextRows.length > 0) { + var nextRow = nextRows[0]; + if (nextRow.hasClassName('plugin-dependency-info')) { + nextRow.remove(); + } + } + } + + function populateEnableDisableInfo(pluginTR, infoContainer) { + var pluginMetadata = pluginTR.jenkinsPluginMetadata; + + // Remove all existing class info + infoContainer.removeAttribute('class'); + infoContainer.addClassName('enable-state-info'); + + if (pluginTR.hasClassName('has-disabled-dependency')) { + var dependenciesDiv = pluginMetadata.dependenciesDiv; + var dependencySpans = pluginMetadata.dependencies; + + infoContainer.update('
This plugin cannot be enabled
It has one or more disabled dependencies.
'); + + // Go through each dependency element. Show the spans where the dependency is + // disabled. Hide the others. + for (var i = 0; i < dependencySpans.length; i++) { + var dependencySpan = dependencySpans[i]; + var pluginId = dependencySpan.readAttribute('data-plugin-id'); + var depPluginTR = getPluginTR(pluginId); + var depPluginMetadata = depPluginTR.jenkinsPluginMetadata; + if (depPluginMetadata.enableInput.checked) { + // It's enabled ... hide the span + dependencySpan.setStyle({display: 'none'}); + } else { + // It's disabled ... show the span + dependencySpan.setStyle({display: 'inline-block'}); + } + } + + dependenciesDiv.setStyle({display: 'inherit'}); + infoContainer.appendChild(dependenciesDiv); + + return true; + } else if (pluginTR.hasClassName('has-dependants')) { + if (!pluginTR.hasClassName('all-dependants-disabled')) { + var dependantsDiv = pluginMetadata.dependantsDiv; + var dependantSpans = pluginMetadata.dependants; + + infoContainer.update('
This plugin cannot be disabled
It has one or more enabled plugins that depend on it.
'); + + // Go through each dependant element. Show the spans where the dependant is + // enabled. Hide the others. + for (var i = 0; i < dependantSpans.length; i++) { + var dependantSpan = dependantSpans[i]; + var pluginId = dependantSpan.readAttribute('data-plugin-id'); + var depPluginTR = getPluginTR(pluginId); + var depPluginMetadata = depPluginTR.jenkinsPluginMetadata; + if (depPluginMetadata.enableInput.checked) { + // It's disabled ... hide the span + dependantSpan.setStyle({display: 'inline-block'}); + } else { + // It's enabled ... show the span + dependantSpan.setStyle({display: 'none'}); + } + } + + dependantsDiv.setStyle({display: 'inherit'}); + infoContainer.appendChild(dependantsDiv); + + return true; + } + } + + return false; + } + + function populateUninstallInfo(pluginTR, infoContainer) { + // Remove all existing class info + infoContainer.removeAttribute('class'); + infoContainer.addClassName('uninstall-state-info'); + + if (pluginTR.hasClassName('has-dependants')) { + var pluginMetadata = pluginTR.jenkinsPluginMetadata; + var dependantsDiv = pluginMetadata.dependantsDiv; + var dependantSpans = pluginMetadata.dependants; + + infoContainer.update('
This plugin cannot be uninstalled
It has one or more plugins that depend on it.
'); + + // Go through each dependant element. Show them all. + for (var i = 0; i < dependantSpans.length; i++) { + var dependantSpan = dependantSpans[i]; + dependantSpan.setStyle({display: 'inline-block'}); + } + + dependantsDiv.setStyle({display: 'inherit'}); + infoContainer.appendChild(dependantsDiv); + + return true; + } + + return false; + } + + function initPluginRowHandling(pluginTR) { + var enableInput = select('.enable input', pluginTR); + var dependenciesDiv = select('.dependency-list', pluginTR); + var dependantsDiv = select('.dependant-list', pluginTR); + var enableTD = select('td.enable', pluginTR); + var uninstallTD = select('td.uninstall', pluginTR); + + pluginTR.jenkinsPluginMetadata = { + enableInput: enableInput, + dependenciesDiv: dependenciesDiv, + dependantsDiv: dependantsDiv + }; + + if (dependenciesDiv) { + pluginTR.jenkinsPluginMetadata.dependencies = selectAll('span', dependenciesDiv); + pluginTR.jenkinsPluginMetadata.dependencyIds = processSpanSet(pluginTR.jenkinsPluginMetadata.dependencies); + } + if (dependantsDiv) { + pluginTR.jenkinsPluginMetadata.dependants = selectAll('span', dependantsDiv); + pluginTR.jenkinsPluginMetadata.dependantIds = processSpanSet(pluginTR.jenkinsPluginMetadata.dependants); + } + + // Setup event handlers... + + // Toggling of the enable/disable checkbox requires a check and possible + // change of visibility on the same checkbox on other plugins. + Element.observe(enableInput, 'click', function() { + setEnableWidgetStates(); + }); + + // + var infoTR = document.createElement("tr"); + var infoTD = document.createElement("td"); + var infoDiv = document.createElement("div"); + infoTR.appendChild(infoTD) + infoTD.appendChild(infoDiv) + infoTD.setAttribute('colspan', '6'); // This is the cell that all info will be added to. + infoDiv.setStyle({display: 'inherit'}); + + // We don't want the info row to appear immediately. We wait for e.g. 1 second and if the mouse + // is still in there (hasn't left the cell) then we show. The following code is for clearing the + // show timeout where the mouse has left before the timeout has fired. + var showInfoTimeout = undefined; + function clearShowInfoTimeout() { + if (showInfoTimeout) { + clearTimeout(showInfoTimeout); + } + showInfoTimeout = undefined; + } + + // Handle mouse in/out of the enable/disable cell (left most cell). + Element.observe(enableTD, 'mouseenter', function() { + showInfoTimeout = setTimeout(function() { + showInfoTimeout = undefined; + infoDiv.update(''); + if (populateEnableDisableInfo(pluginTR, infoDiv)) { + addDependencyInfoRow(pluginTR, infoTR); + } + }, 1000); + }); + Element.observe(enableTD, 'mouseleave', function() { + clearShowInfoTimeout(); + removeDependencyInfoRow(pluginTR); + }); + + // Handle mouse in/out of the uninstall cell (right most cell). + Element.observe(uninstallTD, 'mouseenter', function() { + showInfoTimeout = setTimeout(function() { + showInfoTimeout = undefined; + infoDiv.update(''); + if (populateUninstallInfo(pluginTR, infoDiv)) { + addDependencyInfoRow(pluginTR, infoTR); + } + }, 1000); + }); + Element.observe(uninstallTD, 'mouseleave', function() { + clearShowInfoTimeout(); + removeDependencyInfoRow(pluginTR); + }); + } + + for (var i = 0; i < pluginTRs.length; i++) { + initPluginRowHandling(pluginTRs[i]); + } + + setEnableWidgetStates(); + }); +}()); \ No newline at end of file diff --git a/core/src/main/resources/hudson/PluginManager/installed.jelly b/core/src/main/resources/hudson/PluginManager/installed.jelly index 2d439b335e..6b907852df 100644 --- a/core/src/main/resources/hudson/PluginManager/installed.jelly +++ b/core/src/main/resources/hudson/PluginManager/installed.jelly @@ -38,7 +38,7 @@ THE SOFTWARE.
- +
- + - - - diff --git a/war/src/main/webapp/css/style.css b/war/src/main/webapp/css/style.css index c172e6177f..a2d66b17f1 100644 --- a/war/src/main/webapp/css/style.css +++ b/war/src/main/webapp/css/style.css @@ -1221,6 +1221,86 @@ TABLE.fingerprint-in-build TD { background-color: #e8eeee; } +#plugins input.uninstall { + font-size: 11px; + padding: 3px 5px; + border-radius: 1px; + font-family: Helvetica, Arial, sans-serif; + font-weight: bold; + background-color: #4b758b; + color: #eee; + border: 1px solid #5788a1; +} + +#plugins tr.has-dependants input.uninstall { + background-color: #e0e0e0; + border: 1px solid #cccccc; + color: #505050; + background-color: white; +} + +#plugins tr.all-dependants-disabled .enable input { + pointer-events: auto; + opacity: 1.0; + visibility: visible; +} + +#plugins tr.has-dependants input, #plugins tr.has-disabled-dependency .enable input { + pointer-events: none; + opacity: 0.2; +} + +#plugins tr.has-disabled-dependency .enable input { + opacity: 0.4; +} + +#plugins tr.deleted input { + visibility: hidden !important; +} + +#plugins .dependant-list, #plugins .dependency-list { + display: none; +} + +#plugins .enable-state-info, #plugins .uninstall-state-info { + padding: 5px 20px; + max-width: 70%; + border: solid 1px #f0ad4e; + border-radius: 3px; + text-align: center; +} + +#plugins .enable-state-info { + float: left; +} + +#plugins .uninstall-state-info { + float: right; +} + +#plugins .plugin-dependency-info .title, #plugins .plugin-dependency-info .subtitle { + opacity: 0.7; +} +#plugins .plugin-dependency-info .title { + margin-bottom: 5px; + font-size: larger; + font-weight: bolder; +} + +#plugins .plugin-dependency-info span { + margin: 5px 5px 0px 0px; + background-color: #337ab7; + display: inline-block; + padding: .2em .6em .3em; + font-size: 75%; + font-weight: 700; + line-height: 1; + color: #fff; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: .25em; +} /* ========================= repeatable elements ========================= */
@@ -55,16 +55,16 @@ THE SOFTWARE. ${%Uninstall}
+ + @@ -106,16 +106,30 @@ THE SOFTWARE. +

${%Uninstallation pending}

- -
- -
-
+ + +
+ + + +
+
+ +
+ + + +
+
+
+ +
+