mirror of
https://github.com/jenkinsci/jenkins.git
synced 2026-11-04 07:35:53 +00:00
Only allow disable or uninstall when all dependants or dependencies are disabled/enabled
This commit is contained in:
1 parent
7fb4696a6f
commit
3b5424e507
6 files changed
+504
-11
No files matched your search
@@ -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<String> 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<Dependency> 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.
|
||||
*
|
||||
|
||||
@@ -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<PluginWrapper>, ModelObject {
|
||||
*/
|
||||
/*package*/ boolean isBundled;
|
||||
|
||||
/**
|
||||
* List of plugins that depend on this plugin.
|
||||
*/
|
||||
private Set<String> 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<String> 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<String> 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<String> 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<PluginWrapper>, 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
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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('<div class="title">This plugin cannot be enabled</div><div class="subtitle">It has one or more disabled dependencies.</div>');
|
||||
|
||||
// Go through each dependency <span> 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('<div class="title">This plugin cannot be disabled</div><div class="subtitle">It has one or more enabled plugins that depend on it.</div>');
|
||||
|
||||
// Go through each dependant <span> 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('<div class="title">This plugin cannot be uninstalled</div><div class="subtitle">It has one or more plugins that depend on it.</div>');
|
||||
|
||||
// Go through each dependant <span> 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();
|
||||
});
|
||||
}());
|
||||
@@ -38,7 +38,7 @@ THE SOFTWARE.
|
||||
<local:tabBar page="installed" xmlns:local="/hudson/PluginManager" />
|
||||
|
||||
<div class="pane-frame">
|
||||
<table id="plugins" class="sortable pane bigtable stripped-odd">
|
||||
<table id="plugins" class="pane bigtable sortable stripped-odd">
|
||||
<j:choose>
|
||||
<j:when test="${empty(app.pluginManager.plugins) and empty(app.pluginManager.failedPlugins)}">
|
||||
<tr><td>
|
||||
@@ -55,16 +55,16 @@ THE SOFTWARE.
|
||||
<th width="1">${%Uninstall}</th>
|
||||
</tr>
|
||||
<j:forEach var="p" items="${app.pluginManager.plugins}">
|
||||
<tr class="plugin">
|
||||
<tr class="plugin ${p.hasDependants()?'has-dependants':''} ${p.isDeleted()?'deleted':''}" data-plugin-id="${p.shortName}" data-plugin-name="${p.displayName}">
|
||||
<j:set var="state" value="${p.enabled?'true':null}"/>
|
||||
<td class="center pane" data="${state}">
|
||||
<td class="center pane enable" data="${state}">
|
||||
<input type="checkbox" checked="${state}" onclick="flip(event)"
|
||||
url="plugin/${p.shortName}"
|
||||
original="${p.active?'true':'false'}"/>
|
||||
</td>
|
||||
<td class="pane">
|
||||
<td class="pane details">
|
||||
<div>
|
||||
<a href="${p.url}">
|
||||
<a href="${p.url}" class="display-name">
|
||||
${p.updateInfo.displayName?:p.displayName}
|
||||
</a>
|
||||
</div>
|
||||
@@ -106,16 +106,30 @@ THE SOFTWARE.
|
||||
<a href="${%wiki.url}" target="_blank"><l:icon class="icon-help icon-sm" style="vertical-align:top"/></a>
|
||||
</j:if>
|
||||
</td>
|
||||
<td class="center pane">
|
||||
<td class="center pane uninstall">
|
||||
<j:choose>
|
||||
<j:when test="${p.isDeleted()}">
|
||||
<p>${%Uninstallation pending}</p>
|
||||
</j:when>
|
||||
<j:when test="${!p.isBundled()}">
|
||||
<form method="post" action="plugin/${p.shortName}/uninstall">
|
||||
<input type="submit" value="${%Uninstall}"/>
|
||||
</form>
|
||||
</j:when>
|
||||
<j:otherwise>
|
||||
<j:if test="${p.hasDependants()}">
|
||||
<div class="dependant-list">
|
||||
<j:forEach var="dependant" items="${p.dependants}">
|
||||
<span data-plugin-id="${dependant}"></span>
|
||||
</j:forEach>
|
||||
</div>
|
||||
</j:if>
|
||||
<j:if test="${p.hasDependencies()}">
|
||||
<div class="dependency-list">
|
||||
<j:forEach var="dependency" items="${p.dependencies}">
|
||||
<span data-plugin-id="${dependency.shortName}"></span>
|
||||
</j:forEach>
|
||||
</div>
|
||||
</j:if>
|
||||
<form method="post" action="plugin/${p.shortName}/uninstall">
|
||||
<input class="uninstall" type="submit" value="${%Uninstall}"/>
|
||||
</form>
|
||||
</j:otherwise>
|
||||
</j:choose>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -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 ========================= */
|
||||
|
||||
|
||||
Reference in new issue
Block a user