Refine how administrative monitors are displayed (#26417)

Co-authored-by: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Co-authored-by: Tim Jacomb <21194782+timja@users.noreply.github.com>
Co-authored-by: Tim Jacomb <timjacomb1@gmail.com>
This commit is contained in:
authored and GitHub committed 2026-04-03 21:27:54 +01:00
1 parent e19879e61f
commit 6f84809f99
57 files changed
+637 -507

No files matched your search

@@ -66,10 +66,9 @@ import org.kohsuke.stapler.interceptor.RequirePOST;
* <dd>
* If {@link #isActivated()} returns true, Jenkins will use the {@code message.jelly}
* view of this object to render the warning text. This happens in the
* {@code http://SERVER/jenkins/manage} page. This view should typically render
* a DIV box with class='alert alert-danger' or class='alert alert-warning' with a human-readable text
* inside it. It often also contains a link to a page that provides more details
* about the problem.<br>
* {@code http://SERVER/jenkins/manage} page. This view should use {@code l:adminMonitor}
* to display the warning text in a consistent way.
* <br>
* Additionally 2 numbers are shown in the Jenkins header of administrators, one with the number or active
* non-security relevant monitors and one with the number of active security relevant monitors.
* </dd>
@@ -24,12 +24,13 @@ THE SOFTWARE.
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<div class="jenkins-alert jenkins-alert-danger">
<l:adminMonitor severity="danger">
<dl>
<dt>${%PluginCycles}</dt>
<j:forEach var="p" items="${it.pluginsWithCycle}">
<dd><j:out value="${p.longName} v${p.version}"/></dd>
</j:forEach>
<dt>${%PluginCycles}</dt>
<dd>abc 123.345</dd>
<j:forEach var="p" items="${it.pluginsWithCycle}">
<dd><j:out value="${p.longName} v${p.version}"/></dd>
</j:forEach>
</dl>
</div>
</l:adminMonitor>
</j:jelly>
@@ -24,7 +24,7 @@ THE SOFTWARE.
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<div class="jenkins-alert jenkins-alert-warning">
<l:adminMonitor>
<dl>
<dt>${%DeprecatedPlugins}</dt>
<j:forEach var="e" items="${it.getDeprecatedPlugins()}">
@@ -32,5 +32,5 @@ THE SOFTWARE.
</j:forEach>
</dl>
<p>${%DeprecatedPluginsDescription}</p>
</div>
</l:adminMonitor>
</j:jelly>
@@ -24,12 +24,12 @@ THE SOFTWARE.
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<div class="jenkins-alert jenkins-alert-danger">
<l:adminMonitor severity="danger">
<dl>
<dt>${%RequiredPluginUpdates}</dt>
<j:forEach var="p" items="${it.pluginsToBeUpdated}">
<dd><j:out value="${p.pluginName}"/> — <j:out value="${p.message}"/></dd>
</j:forEach>
</dl>
</div>
</l:adminMonitor>
</j:jelly>
@@ -1,10 +1,10 @@
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form">
<div class="jenkins-alert jenkins-alert-danger" role="alert">
<form method="post" action="${rootURL}/${it.url}/act" name="${it.id}">
<f:submit name="correct" value="${%Correct}"/>
</form>
<p>${%blurbOriginal}</p>
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form" xmlns:l="/lib/layout">
<j:set var="controls">
<a href="${rootURL}/pluginManager" class="jenkins-button jenkins-button--primary">${%Correct}</a>
</j:set>
<l:adminMonitor severity="danger" controls="${controls}">
${%blurbOriginal}
<p><strong>${%DirectlyDependentPlugins.title}</strong></p>
<j:forEach items="${it.plugins}" var="plugin">
<j:if test="${plugin.hasOriginalDependencyErrors()}">
@@ -30,5 +30,5 @@
</j:forEach>
</dl>
</j:if>
</div>
</l:adminMonitor>
</j:jelly>
@@ -27,7 +27,7 @@ THE SOFTWARE.
<l:layout title="${%JENKINS_HOME is almost full}">
<l:main-panel>
<h1>
<l:icon class="icon-warning icon-xlg"/>
<l:icon src="symbol-warning" class="icon-xlg jenkins-!-warning-color"/>
${%blurb}
</h1>
@@ -24,11 +24,10 @@ THE SOFTWARE.
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<div class="jenkins-alert jenkins-alert-warning">
<form method="post" action="${rootURL}/${it.url}/act" name="${it.id}">
<f:submit name="yes" value="${%Tell me more}"/>
<f:submit name="no" value="${%Dismiss}"/>
</form>
${%blurb(app.rootDir)}
</div>
<j:set var="controls">
<a href="${rootURL}/${it.url}/" class="jenkins-button jenkins-button--primary">${%Tell me more}</a>
</j:set>
<l:adminMonitor controls="${controls}">
${%blurb(app.rootDir)}
</l:adminMonitor>
</j:jelly>
@@ -24,12 +24,12 @@ THE SOFTWARE.
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<div class="jenkins-alert jenkins-alert-danger">
<dl>
<dt>${%blurb}</dt>
<j:forEach var="d" items="${it.problems}">
<dd>${%problem(d, d.displayName, app.pluginManager.whichPlugin(d.getClass()))}</dd>
</j:forEach>
</dl>
</div>
<l:adminMonitor severity="danger">
<dl>
<dt>${%blurb}</dt>
<j:forEach var="d" items="${it.problems}">
<dd>${%problem(d, d.displayName, app.pluginManager.whichPlugin(d.getClass()))}</dd>
</j:forEach>
</dl>
</l:adminMonitor>
</j:jelly>
@@ -24,11 +24,10 @@ THE SOFTWARE.
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<div class="jenkins-alert jenkins-alert-warning">
<form method="post" action="${rootURL}/${it.url}/act" name="${it.id}">
<f:submit name="yes" value="${%Manage}"/>
<f:submit name="no" value="${%Dismiss}"/>
</form>
<j:set var="controls">
<a href="${rootURL}/${it.url}/manage" class="jenkins-button jenkins-button--primary">${%Manage}</a>
</j:set>
<l:adminMonitor controls="${controls}">
${%You have data stored in an older format and/or unreadable data.}
</div>
</l:adminMonitor>
</j:jelly>
@@ -23,16 +23,12 @@ THE SOFTWARE.
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:f="/lib/form" xmlns:l="/lib/layout">
<div id="redirect-error" class="jenkins-alert jenkins-alert-danger reverse-proxy__hidden"
data-url="${rootURL}/${it.url}/test" data-context="${rootURL}">
<form method="post" action="${rootURL}/${it.url}/act" name="${it.id}">
<f:submit name="yes" value="${%More Info}"/>
<l:isAdmin>
<f:submit name="no" value="${%Dismiss}"/>
</l:isAdmin>
</form>
<div>${%blurb}</div>
<div class="js-context-message reverse-proxy__hidden">${%missingContextMessage(rootURL)}</div>
</div>
<j:set var="controls">
<a href="https://www.jenkins.io/redirect/troubleshooting/broken-reverse-proxy" class="jenkins-button jenkins-button--primary">${%More Info}</a>
</j:set>
<l:adminMonitor severity="danger" dismissable="true" class="jenkins-hidden" id="redirect-error" controls="${controls}">
<div id="redirect-data" data-url="${rootURL}/${it.url}/test" data-context="${rootURL}"/>
<div>${%blurb} <span class="js-context-message jenkins-hidden">${%missingContextMessage(rootURL)}</span></div>
</l:adminMonitor>
<st:adjunct includes="hudson.diagnosis.ReverseProxySetupMonitor.resources"/>
</j:jelly>
@@ -1,3 +0,0 @@
.reverse-proxy__hidden {
display: none;
}
@@ -1,5 +1,7 @@
(function () {
var redirectForm = document.getElementById("redirect-error");
const redirectData = document.getElementById("redirect-data");
if (!redirectForm) {
console.warn(
'This script expects to have an element with id="redirect-error" to be working.',
@@ -7,7 +9,7 @@
return;
}
var urlToTest = redirectForm.getAttribute("data-url");
var urlToTest = redirectData.getAttribute("data-url");
var callUrlToTest = function (testWithContext, callback) {
var headers = {};
var body = null;
@@ -28,19 +30,19 @@
};
var displayWarningMessage = function (withContextMessage) {
redirectForm.classList.remove("reverse-proxy__hidden");
redirectForm.classList.remove("jenkins-hidden");
if (withContextMessage === true) {
redirectForm
.querySelectorAll(".js-context-message")
.forEach(function (node) {
return node.classList.remove("reverse-proxy__hidden");
return node.classList.remove("jenkins-hidden");
});
}
};
callUrlToTest(false, function (response) {
if (response.status !== 200) {
var context = redirectForm.getAttribute("data-context");
var context = redirectData.getAttribute("data-context");
// to cover the case where the JenkinsRootUrl is configured without the context
if (context) {
callUrlToTest(true, function (response2) {
@@ -24,13 +24,10 @@ THE SOFTWARE.
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<div id="tooManyJobsButNoView" class="jenkins-alert jenkins-alert-warning">
<l:isAdmin>
<form method="post" action="${rootURL}/${it.url}/act" name="${it.id}">
<f:submit name="yes" value="${%Create a view now}"/>
<f:submit name="no" value="${%Dismiss}"/>
</form>
</l:isAdmin>
<j:set var="controls">
<a href="${rootURL}/newView" class="jenkins-button jenkins-button--primary">${%Create a view now}</a>
</j:set>
<l:adminMonitor controls="${controls}">
${%blurb}
</div>
</l:adminMonitor>
</j:jelly>
@@ -24,9 +24,33 @@ THE SOFTWARE.
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<div class="jenkins-alert jenkins-alert-info">
<j:set var="ucData" value="${it.data}" />
<j:set var="upJob" value="${app.updateCenter.hudsonJob}" />
<j:set var="ucData" value="${it.data}" />
<j:set var="upJob" value="${app.updateCenter.hudsonJob}" />
<j:set var="controls">
<j:choose>
<j:when test="${upJob.status.success}">
</j:when>
<j:when test="${upJob!=null and upJob.error != null}">
<j:if test="${ucData.canUpgrade()}">
<form method="post" action="${rootURL}/updateCenter/upgrade">
<f:submit value="${%Retry}"/>
</form>
</j:if>
</j:when>
<j:when test="${upJob!=null}">
</j:when>
<j:otherwise>
<l:isAdmin>
<j:if test="${ucData.canUpgrade()}">
<form method="post" action="${rootURL}/updateCenter/upgrade">
<f:submit value="${%Or Upgrade Automatically}"/>
</form>
</j:if>
</l:isAdmin>
</j:otherwise>
</j:choose>
</j:set>
<l:adminMonitor severity="info" controls="${controls}">
<j:choose>
<j:when test="${upJob.status.success}">
<j:invokeStatic var="lifecycle" className="hudson.lifecycle.Lifecycle" method="get" />
@@ -41,11 +65,6 @@ THE SOFTWARE.
</j:when>
<j:when test="${upJob!=null and upJob.error != null}">
${%UpgradeFailed(ucData.core.version,upJob.errorMessage,rootURL+'/updateCenter/')}
<j:if test="${ucData.canUpgrade()}">
<form method="post" action="${rootURL}/updateCenter/upgrade">
<f:submit value="${%Retry}"/>
</form>
</j:if>
</j:when>
<j:when test="${upJob!=null}">
${%UpgradeProgress(ucData.core.version,rootURL+'/updateCenter/')}
@@ -53,14 +72,7 @@ THE SOFTWARE.
<j:otherwise>
<j:set var="changelog_url" value="${app.CHANGELOG_URL}"/>
${%NewVersionAvailable(ucData.core.version,ucData.core.url,changelog_url)}
<l:isAdmin>
<j:if test="${ucData.canUpgrade()}">
<form method="post" action="${rootURL}/updateCenter/upgrade">
<f:submit value="${%Or Upgrade Automatically}"/>
</form>
</j:if>
</l:isAdmin>
</j:otherwise>
</j:choose>
</div>
</l:adminMonitor>
</j:jelly>
@@ -24,10 +24,7 @@ THE SOFTWARE.
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<div class="jenkins-alert jenkins-alert-warning">
<form method="post" action="${rootURL}/${it.url}/disable">
<f:submit value="${%Dismiss}"/>
</form>
<l:adminMonitor>
${%blurb(rootURL)}
</div>
</l:adminMonitor>
</j:jelly>
@@ -23,12 +23,11 @@ THE SOFTWARE.
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form">
<div class="jenkins-alert jenkins-alert-warning">
<form method="post" action="${rootURL}/${it.url}/act" name="${it.id}">
<f:submit name="learn" value="${%Learn more}"/>
<f:submit name="dismiss" value="${%Dismiss}"/>
</form>
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form" xmlns:l="/lib/layout">
<j:set var="controls">
<a href="https://www.jenkins.io/redirect/csrf-protection/" class="jenkins-button jenkins-button--primary">${%Learn more}</a>
</j:set>
<l:adminMonitor controls="${controls}">
${%blurb}
</div>
</l:adminMonitor>
</j:jelly>
@@ -23,8 +23,8 @@ THE SOFTWARE.
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:t="/lib/hudson">
<div class="jenkins-alert jenkins-alert-warning">
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout">
<l:adminMonitor>
${%blurb(rootURL)}
</div>
</l:adminMonitor>
</j:jelly>
@@ -1,48 +0,0 @@
package hudson.triggers.SlowTriggerAdminMonitor
import hudson.Util
import hudson.triggers.SlowTriggerAdminMonitor
import jenkins.model.Jenkins
import org.apache.commons.jelly.tags.fmt.FmtTagLibrary
SlowTriggerAdminMonitor tam = my
dl {
div(class: "jenkins-alert jenkins-alert-warning") {
form(method: "post", name: "clear", action: rootURL + "/" + tam.url + "/clear") {
button(name: "clear", type: "submit", class: "jenkins-button jenkins-submit-button jenkins-button--primary") {
raw _("Dismiss")
}
}
text(_("blurb"))
table(class: "sortable jenkins-table jenkins-!-margin-top-2", width: "100%") {
thead {
tr {
th(_("Trigger"))
th(_("Most Recent Occurrence"))
th(_("Most Recently Occurring Job"))
th(_("Duration"))
}
}
tam.errors.each { trigger, val ->
def job = Jenkins.get().getItemByFullName(val.fullJobName)
tr {
td(Jenkins.get().getDescriptorByType(val.trigger).getDisplayName())
td(val.getTimeString())
if (job == null) {
td(val.fullJobName)
} else {
td {
a(job.getFullDisplayName(), href: rootURL + '/' + job.getUrl(), class: 'model-link')
}
}
td(Util.getTimeSpanString(val.duration))
}
}
}
}
}
@@ -0,0 +1,39 @@
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<j:set var="controls">
<form method="post" name="clear" action="${rootURL}/${it.url}/clear">
<f:submit value="${%Clear}"/>
</form>
</j:set>
<l:adminMonitor controls="${controls}" dismissValue="${%Disable}">
${%blurb}
<table class="sortable jenkins-table jenkins-!-margin-top-3 jenkins-!-margin-bottom-0">
<thead>
<tr>
<th>${%Trigger}</th>
<th>${%Most Recent Occurrence}</th>
<th>${%Most Recently Occurring Job}</th>
<th>${%Duration}</th>
</tr>
</thead>
<tbody>
<j:forEach var="trigger" items="${it.getErrors().entrySet()}">
<j:set var="trigger" value="${trigger.key}"/>
<j:set var="val" value="${trigger.value}"/>
<j:set var="job" value="${app.getItemByFullName(trigger.fullJobName)}"/>
<tr>
<td>${app.getDescriptorByType(val.trigger).getDisplayName()}</td>
<td>${val.getTimeString()}</td>
<j:if test="${job!=null}">
<td><a href="${rootURL}/${job.url}" class="model-link">${job.fullDisplayName}</a></td>
</j:if>
<j:if test="${job==null}">
<td>${val.fullJobName}</td>
</j:if>
<td>${h.getTimeSpanString(val.duration)}</td>
</tr>
</j:forEach>
</tbody>
</table>
</l:adminMonitor>
</j:jelly>
@@ -24,24 +24,22 @@ THE SOFTWARE.
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:i="jelly:fmt">
<div class="jenkins-alert jenkins-alert-warning">
<p>
${%message(it.home)}
</p>
<table>
<tr>
<td>${%This Jenkins}:</td>
<td>${it.id}</td>
</tr>
<tr>
<td>${%Other Jenkins}:</td>
<td>${it.collidingId}</td>
</tr>
</table>
<div>
<form method="post" action="${rootURL}/${it.url}/disable">
<f:submit value="${%label}" />
</form>
</div>
</div>
<j:set var="controls">
<form method="post" action="${rootURL}/${it.url}/disable">
<f:submit value="${%label}" />
</form>
</j:set>
<l:adminMonitor dismissable="false" controls="${controls}">
${%message(it.home)}
<table>
<tr>
<td>${%This Jenkins}:</td>
<td>${it.id}</td>
</tr>
<tr>
<td>${%Other Jenkins}:</td>
<td>${it.collidingId}</td>
</tr>
</table>
</l:adminMonitor>
</j:jelly>
@@ -24,7 +24,7 @@ THE SOFTWARE.
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<div class="jenkins-alert jenkins-alert-danger">
<l:adminMonitor severity="danger">
${%blurb(rootURL+'/'+it.url)}
</div>
</l:adminMonitor>
</j:jelly>
@@ -1,11 +1,11 @@
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<div class="jenkins-alert jenkins-alert-warning">
${%blurb(app.initLevel)}
${%Example: usage of} <code>@Initializer(after = InitMilestone.COMPLETED)</code> ${%in a plugin}
(<a href="https://www.jenkins.io/redirect/troubleshooting/initialization-not-completed" rel="noopener noreferrer" target="_blank">${%See documentation}</a>).
${%Please} <a href="https://www.jenkins.io/redirect/report-an-issue" rel="noopener noreferrer" target="_blank">${%report a bug}</a> ${%in the Jenkins bugtracker}.
<!--TODO: Nice2have: cause diagnostics-->
<!-- FIXME TODO XXX i18n does not take into account different sentence structure, rewrite with arguments -->
</div>
<l:adminMonitor>
${%blurb(app.initLevel)}
${%Example: usage of} <code>@Initializer(after = InitMilestone.COMPLETED)</code> ${%in a plugin}
(<a href="https://www.jenkins.io/redirect/troubleshooting/initialization-not-completed" rel="noopener noreferrer" target="_blank">${%See documentation}</a>).
${%Please} <a href="https://www.jenkins.io/redirect/report-an-issue" rel="noopener noreferrer" target="_blank">${%report a bug}</a> ${%in the Jenkins bugtracker}.
<!--TODO: Nice2have: cause diagnostics-->
<!-- FIXME TODO XXX i18n does not take into account different sentence structure, rewrite with arguments -->
</l:adminMonitor>
</j:jelly>
@@ -24,12 +24,10 @@ THE SOFTWARE.
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<div class="jenkins-alert jenkins-alert-warning">
<form method="post" action="${rootURL}/${it.url}/act" name="${it.id}">
<f:submit name="yes" value="${%Manage}"/>
<f:submit name="no" value="${%Dismiss}"/>
</form>
<j:set var="controls">
<a href="${rootURL}/computer/(built-in)/configure" class="jenkins-button jenkins-button--primary">${%Manage}</a>
</j:set>
<l:adminMonitor controls="${controls}">
${%ExecutorsWarning}
</div>
</l:adminMonitor>
</j:jelly>
@@ -24,12 +24,11 @@ THE SOFTWARE.
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<div class="jenkins-alert jenkins-alert-warning">
<form method="post" action="${rootURL}/${it.url}/act" name="${it.id}">
<f:submit name="agent" value="${%Set up agent}"/>
<f:submit name="cloud" value="${%Set up cloud}"/>
<f:submit name="no" value="${%Dismiss}"/>
</form>
<j:set var="controls">
<a href="${rootURL}/computer/new" class="jenkins-button jenkins-button--primary">${%Set up agent}</a>
<a href="${rootURL}/manage/cloud" class="jenkins-button jenkins-button--primary">${%Set up cloud}</a>
</j:set>
<l:adminMonitor controls="${controls}">
${%ExecutorsWarning}
</div>
</l:adminMonitor>
</j:jelly>
@@ -23,12 +23,7 @@ THE SOFTWARE.
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form" xmlns:l="/lib/layout">
<div class="jenkins-alert jenkins-alert-warning">
<l:isAdmin>
<form method="post" action="${rootURL}/${it.url}/disable">
<f:submit value="${%Dismiss}"/>
</form>
</l:isAdmin>
<l:adminMonitor>
<j:set var="actionAnchor">
<a href="${rootURL}/configure">${%actionUrlContent}</a>
</j:set>
@@ -45,5 +40,5 @@ THE SOFTWARE.
<l:isAdmin>
<j:out value="${%actionToTake(actionAnchor)}"/>
</l:isAdmin>
</div>
</l:adminMonitor>
</j:jelly>
@@ -24,11 +24,10 @@ THE SOFTWARE.
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<div class="jenkins-alert jenkins-alert-warning">
<form method="post" action="${rootURL}/${it.url}/act" name="${it.id}">
<f:submit name="yes" value="${%Setup Security}"/>
<f:submit name="no" value="${%Dismiss}"/>
</form>
<j:set var="controls">
<a href="${rootURL}/configureSecurity" class="jenkins-button jenkins-button--primary">${%Setup Security}</a>
</j:set>
<l:adminMonitor controls="${controls}">
${%blurb}
</div>
</l:adminMonitor>
</j:jelly>
@@ -1,7 +1,7 @@
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<j:if test="${it.checkEnabled}">
<div id="URICheckEncodingMonitor-message" data-url="${rootURL}/${it.url}/checkURIEncoding"/>
<st:adjunct includes="jenkins.diagnostics.URICheckEncodingMonitor.uri-check-encoding-monitor"/>
</j:if>
<l:adminMonitor class="jenkins-hidden">
<div id="URICheckEncodingMonitor-message" data-url="${rootURL}/${it.url}/checkURIEncoding"/>
</l:adminMonitor>
<st:adjunct includes="jenkins.diagnostics.URICheckEncodingMonitor.uri-check-encoding-monitor"/>
</j:jelly>
@@ -7,10 +7,11 @@ Behaviour.specify(
var params = new URLSearchParams({ value: "\u57f7\u4e8b" });
fetch(url + "?" + params).then((rsp) => {
rsp.text().then((responseText) => {
var message = document.getElementById(
"URICheckEncodingMonitor-message",
);
message.innerHTML = responseText;
if (responseText !== "<div/>") {
const alertDiv = element.closest(".app-adminmonitor");
alertDiv.classList.remove("jenkins-hidden");
element.innerHTML = responseText;
}
});
});
},
@@ -1,10 +1,11 @@
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<div class="jenkins-alert jenkins-alert-warning">
<form method="post" action="${rootURL}/${it.url}/act" name="${it.id}">
<f:submit name="yes" value="${%Apply Migration}"/>
<f:submit name="no" value="${%Dismiss}"/>
</form>
${%blurb}
</div>
<j:set var="controls">
<form method="post" action="${rootURL}/${it.url}/act" name="${it.id}">
<f:submit name="yes" value="${%Apply Migration}"/>
</form>
</j:set>
<l:adminMonitor controls="${controls}">
${%blurb}
</l:adminMonitor>
</j:jelly>
@@ -23,10 +23,12 @@ THE SOFTWARE.
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:i="jelly:fmt">
<div class="jenkins-alert jenkins-alert-warning">
<j:set var="controls">
<form method="post" action="${rootURL}/${it.url}/act" name="${it.id}">
<f:submit name="reset" value="${%reset(it.expectedPort)}"/>
</form>
</j:set>
<l:adminMonitor controls="${controls}">
${%description(it.systemPropertyName, it.expectedPort)}
</div>
</l:adminMonitor>
</j:jelly>
@@ -24,12 +24,14 @@ THE SOFTWARE.
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<j:if test="${app.updateCenter.downgradable}">
<div class="jenkins-alert jenkins-alert-warning">
${%Restore the previous version of Jenkins}
<form method="post" action="${rootURL}/updateCenter/downgrade">
<f:submit value="${%buttonText(app.updateCenter.backupVersion)}"/>
</form>
</div>
</j:if>
<j:if test="${app.updateCenter.downgradable}">
<j:set var="controls">
<form method="post" action="${rootURL}/updateCenter/downgrade">
<f:submit value="${%buttonText(app.updateCenter.backupVersion)}"/>
</form>
</j:set>
<l:adminMonitor dismissable="false" controls="${controls}">
${%Restore the previous version of Jenkins}
</l:adminMonitor>
</j:if>
</j:jelly>
@@ -24,14 +24,11 @@ THE SOFTWARE.
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:l="/lib/layout" xmlns:f="/lib/form">
<div class="jenkins-alert jenkins-alert-${it.severityAsString}">
<form id="JavaVersionRecommendationAdminMonitor" method="post" action="${rootURL}/${it.url}/act" name="${it.id}">
<f:submit name="yes" value="${%More Info}"/>
<l:isAdmin>
<f:submit value="${%Ignore}" name="no"/>
</l:isAdmin>
</form>
<h2>${%Recommended_Java_Version_Heading(it.javaVersion)}</h2>
${%Recommended_Java_Version(it.javaVersion, it.endOfLifeAsDate)}
</div>
<j:set var="controls">
<a href="https://jenkins.io/redirect/java-support/" class="jenkins-button jenkins-button--primary">${%More Info}</a>
</j:set>
<l:adminMonitor severity="${it.severityAsString}" controls="${controls}">
<h4 class="jenkins-!-margin-bottom-1">${%Recommended_Java_Version_Heading(it.javaVersion)}</h4>
${%Recommended_Java_Version(it.javaVersion, it.endOfLifeAsDate)}
</l:adminMonitor>
</j:jelly>
@@ -25,14 +25,11 @@ THE SOFTWARE.
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:l="/lib/layout" xmlns:f="/lib/form">
<div class="jenkins-alert jenkins-alert-danger">
<form id="${it.id}" method="post" action="${rootURL}/${it.url}/act" name="${it.id}">
<f:submit name="yes" value="${%More Info}"/>
<l:isAdmin>
<f:submit value="${%Ignore}" name="no"/>
</l:isAdmin>
</form>
<h2 class="h3">${it.displayName}</h2>
<j:set var="controls">
<a href="https://www.jenkins.io/redirect/operating-system-end-of-life" class="jenkins-button jenkins-button--primary">${%More Info}</a>
</j:set>
<l:adminMonitor severity="danger" controls="${controls}">
<h4 class="jenkins-!-margin-bottom-1">${it.displayName}</h4>
<j:choose>
<j:when test="${it.afterEndOfLifeDate}">
${%Past_End_of_Life(it.operatingSystemName, it.endOfLifeDate, it.documentationUrl)}
@@ -41,5 +38,5 @@ THE SOFTWARE.
${%Upcoming_End_of_Life(it.operatingSystemName, it.endOfLifeDate, it.documentationUrl)}
</j:otherwise>
</j:choose>
</div>
</l:adminMonitor>
</j:jelly>
@@ -1,41 +0,0 @@
/*
* The MIT License
*
* Copyright 2019 CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package jenkins.security.ResourceDomainRecommendation
def f = namespace(lib.FormTagLib)
def l = namespace(lib.LayoutTagLib)
dl {
div(class: "jenkins-alert jenkins-alert-info") {
a(name: "resource-root-url")
l.isAdmin() {
form(method: "post", action: "${rootURL}/${my.url}/act") {
f.submit(name: 'redirect', value: _("Configure resource root URL"))
f.submit(name: 'dismiss', value: _("Dismiss"))
}
}
raw(_("blurb"))
}
}
@@ -0,0 +1,13 @@
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:l="/lib/layout" xmlns:f="/lib/form">
<j:set var="controls">
<l:isAdmin>
<a href="${rootURL}/configure" class="jenkins-button jenkins-button--primary">
${%Configure resource root URL}
</a>
</l:isAdmin>
</j:set>
<l:adminMonitor severity="info" controls="${controls}">
${%blurb}
</l:adminMonitor>
</j:jelly>
@@ -38,11 +38,16 @@ def printEntry(warning, title, checked) {
div(class: "setting-description") {
a(warning.url, href: warning.url)
}
if (!warning.isFixable()) {
div(class: "error") {
text(_("No fix available"))
}
}
}
}
f.section(title:_("Hidden security warnings")) {
div(id: "hidden-security-warnings")
f.advanced(title: _("Security warnings"), align:"left") {
f.block {
text(_("blurb"))
@@ -8,9 +8,9 @@
<p>
Checked entries (the default) are
<em>active</em>
, i.e. they're shown to administrators in an administrative monitor. Entries
can be unchecked to hide them. This can be useful if you've evaluated a
specific warning and are confident it does not apply to your environment or
, i.e. they're shown in the plugin manager and in an administrative monitor.
Entries can be unchecked to hide them. This can be useful if you've evaluated
a specific warning and are confident it does not apply to your environment or
configuration, and continued use of the specified component does not
constitute a security problem.
</p>
@@ -1,102 +0,0 @@
/*
* The MIT License
*
* Copyright (c) 2016, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package jenkins.security.UpdateSiteWarningsMonitor
def f = namespace(lib.FormTagLib)
def l = namespace(lib.LayoutTagLib)
def listWarnings(warnings, boolean core) {
def fixables = 0
warnings.each { warning ->
dd {
a(warning.message, href: warning.url, rel: 'noopener noreferrer', target: "_blank")
def fixable = warning.isFixable()
if (fixable != null) {
if (fixable) {
fixables++
} else {
raw(_(core ? "unfixableCore" : "unfixable"))
}
}
}
}
if (fixables == warnings.size()) {
dd {
if (fixables == 1) {
raw(_(core ? "allFixable1Core" : "allFixable1", rootURL))
} else {
raw(_(core ? "allFixableCore" : "allFixable", rootURL))
}
}
} else if (fixables > 0) {
dd {
raw(_(core ? "someFixableCore" : "someFixable", rootURL))
}
} else {
dd {
raw(_(core ? "noneFixableCore" : "noneFixable"))
}
}
}
def coreWarnings = my.activeCoreWarnings
def pluginWarnings = my.activePluginWarningsByPlugin
div(class: "jenkins-alert jenkins-alert-danger", role: "alert") {
l.isAdmin() {
form(method: "post", action: "${rootURL}/${my.url}/forward") {
if (!pluginWarnings.isEmpty()) {
f.submit(name: 'fix', value: _("pluginManager.link"))
}
f.submit(name: 'configure', value: _("configureSecurity.link"))
}
}
text(_("blurb"))
if (!coreWarnings.isEmpty()) {
dl {
dt {
text(_("coreTitle", jenkins.model.Jenkins.version))
}
listWarnings(coreWarnings, true)
}
}
if (!pluginWarnings.isEmpty()) {
dl {
pluginWarnings.each { plugin, warnings ->
dt {
a(_("pluginTitle", plugin.displayName, plugin.version), href: plugin.url, rel: 'noopener noreferrer', target: "_blank")
}
listWarnings(warnings, false)
}
}
}
if (my.hasApplicableHiddenWarnings()) {
text(_("more"))
}
}
@@ -0,0 +1,70 @@
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:local="local">
<d:taglib uri="local">
<d:tag name="message">
<j:choose>
<j:when test="${attrs.core}">
<j:out value="${attrs.c}"/>
</j:when>
<j:otherwise>
<j:out value="${attrs.p}"/>
</j:otherwise>
</j:choose>
</d:tag>
<d:tag name="listWarnings" >
<j:set var="fixables" value="0"/>
<j:forEach var="warning" items="${attrs.warnings}">
<dd>
<a href="${warning.url}" rel="noopener noreferrer" target="_blank">${warning.message}</a>
<j:set var="fixable" value="${warning.isFixable()}"/>
<j:if test="${fixable != null}">
<j:if test="${fixable}">
<j:set var="fixables" value="${fixables + 1}"/>
</j:if>
<j:if test="${!fixable}">
${%unfixable}
</j:if>
</j:if>
</dd>
</j:forEach>
<j:choose>
<j:when test="${fixables eq warnings.size()}">
<dd>
<j:if test="${fixables == 1}">
<local:message core="${attrs.core}" c="${%allFixable1Core(rootURL)}" p="${%allFixable1(rootURL)}"/>
</j:if>
<j:if test="${fixables != 1}">
<local:message core="${attrs.core}" c="${%allFixableCore(rootURL)}" p="${%allFixable(rootURL)}"/>
</j:if>
</dd>
</j:when>
<j:when test="${fixables > 0}">
<dd>
<local:message core="${attrs.core}" c="${%someFixableCore(rootURL)}" p="${%someFixable(rootURL)}"/>
</dd>
</j:when>
<j:otherwise>
<dd>
<local:message core="${attrs.core}" c="${%noneFixableCore}" p="${%noneFixable}"/>
</dd>
</j:otherwise>
</j:choose>
</d:tag>
</d:taglib>
<j:set var="coreWarnings" value="${it.activeCoreWarnings}"/>
<j:set var="pluginWarnings" value="${it.activePluginWarningsByPlugin}"/>
<j:set var="controls">
<l:isAdmin>
<a href="${rootURL}/configureSecurity/#hidden-security-warnings" class="jenkins-button jenkins-button--primary">${%configureSecurity.link}</a>
<j:if test="${pluginWarnings.size() > 0}">
<a href="${rootURL}/pluginManager" class="jenkins-button jenkins-button--primary">${%pluginManager.link}</a>
</j:if>
</l:isAdmin>
</j:set>
<l:adminMonitor severity="danger" controls="${controls}">
<div class="jenkins-!-margin-bottom-1">${%blurb}</div>
<j:if test="${it.hasApplicableHiddenWarnings()}">
${%more}
</j:if>
</l:adminMonitor>
</j:jelly>
@@ -23,8 +23,8 @@
pluginTitle = {0} {1}
coreTitle = Jenkins {0} core and libraries
blurb = Warnings have been published for the following currently installed components:
more = Additional warnings are hidden due to the current security configuration.
blurb = Warnings have been published for some installed components.
more = Some of these warnings are hidden in the plugin manager due to the current security configuration.
allFixable = Fixes for all of these issues are available. \
Go to the <a href="{0}/pluginManager">plugin manager</a> to update the plugin.
@@ -50,5 +50,5 @@ noneFixableCore = No fixes for these issues are available. \
# Leading space to separate from link
unfixableCore = \u0020(no fix available)
pluginManager.link = Go to plugin manager
configureSecurity.link = Configure which of these warnings are shown
pluginManager.link = Plugin manager
configureSecurity.link = View
@@ -22,12 +22,13 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form">
<div class="jenkins-alert jenkins-alert-warning">
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form" xmlns:l="/lib/layout">
<j:set var="controls">
<form method="post" action="${rootURL}/${it.url}/act" name="${it.id}">
<f:submit name="yes" value="${%Disable automatic generation of legacy API tokens}"/>
<f:submit name="no" value="${%Dismiss}"/>
</form>
</j:set>
<l:adminMonitor controls="${controls}">
${%warningMessage}
</div>
</l:adminMonitor>
</j:jelly>
@@ -22,12 +22,13 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form">
<div class="jenkins-alert jenkins-alert-warning">
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form" xmlns:l="/lib/layout">
<j:set var="controls">
<form method="post" action="${rootURL}/${it.url}/act" name="${it.id}">
<f:submit name="yes" value="${%Prevent users from manually creating legacy API tokens}"/>
<f:submit name="no" value="${%Dismiss}"/>
</form>
</j:set>
<l:adminMonitor controls="${controls}">
${%warningMessage}
</div>
</l:adminMonitor>
</j:jelly>
@@ -22,8 +22,8 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core">
<div class="jenkins-alert jenkins-alert-warning">
${%warningMessage(rootURL, it.url)}
</div>
<j:jelly xmlns:j="jelly:core" xmlns:l="/lib/layout">
<l:adminMonitor>
${%warningMessage(rootURL, it.url)}
</l:adminMonitor>
</j:jelly>
@@ -22,12 +22,11 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form">
<div class="jenkins-alert jenkins-alert-info">
<form method="post" action="${rootURL}/${it.url}/act" name="${it.id}">
<f:submit name="more" value="${%Learn more}"/>
<f:submit name="dismiss" primary="false" value="${%Dismiss}"/>
</form>
${%blurb}
</div>
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form" xmlns:l="/lib/layout">
<j:set var="controls">
<a href="${rootURL}/manage/administrativeMonitor/jenkins.security.csp.impl.CspRecommendation" class="jenkins-button jenkins-button--primary">${%Learn more}</a>
</j:set>
<l:adminMonitor severity="info" controls="${controls}">
${%blurb}
</l:adminMonitor>
</j:jelly>
@@ -22,11 +22,8 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form">
<div class="jenkins-alert jenkins-alert-warning">
<form method="post" action="${rootURL}/${it.url}/disable">
<f:submit value="${%Dismiss}"/>
</form>
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form" xmlns:l="/lib/layout">
<l:adminMonitor>
<j:set var="referenceAnchor">
<a href="https://www.jenkins.io/redirect/csrf-protection" rel="noopener noreferrer" target="_blank">${%referenceUrlContent}</a>
</j:set>
@@ -36,5 +33,5 @@ THE SOFTWARE.
<j:out value="${%warningMessage(referenceAnchor)}"/>
<p />
<j:out value="${%actionMessage(actionAnchor)}"/>
</div>
</l:adminMonitor>
</j:jelly>
@@ -0,0 +1,34 @@
Behaviour.specify(
".app-adminmonitor-dismiss-button",
"admin-monitor",
0,
function (element) {
element.addEventListener("click", function (event) {
event.preventDefault();
const url = element.dataset.url;
fetch(url, {
method: "POST",
cache: "no-cache",
headers: crumb.wrap({
"Content-Type": "application/x-www-form-urlencoded",
}),
body: new URLSearchParams({}),
})
.then((response) => {
if (response.ok) {
const parent = element.closest(".app-adminmonitor");
if (parent) {
parent.remove();
}
} else {
console.warn(
`AdminMonitor dismiss request failed with status ${response.status}`,
);
}
})
.catch((error) =>
console.warn(`AdminMonitor dismiss request failed: ${error}`),
);
});
},
);
@@ -0,0 +1,71 @@
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:d="jelly:define">
<st:documentation> <![CDATA[
Displays an administrative monitor alert.
@since TODO
]]>
<st:attribute name="severity" use="optional" >
Severity of the alert, one of info, warning, danger or success. Defaults to warning if not specified.
</st:attribute>
<st:attribute name="dismissable" use="optional">
If the alert can be dismissed by the user, defaults to true if not specified. Dismissed alerts can be re-enabled
in the global configuration.
</st:attribute>
<st:attribute name="dismissValue" use="optional">
The value of the dismiss tooltip. Defaults to "Dismiss" if not specified.
</st:attribute>
<st:attribute name="class" use="optional">
Additional CSS class to add to the alert div.
</st:attribute>
<st:attribute name="id" use="optional">
The id attribute to add to the alert div.
</st:attribute>
<st:attribute name="controls" use="optional"><![CDATA[
Shows additional controls in the top right of the alert besides the dismiss button.
Define the controls like this:
<j:set var="controls">
<form method="post" name="clear" action="${rootURL}/${it.url}/clear">
<f:submit value="${%Clear}"/>
</form>
</j:set>
To be able to set controls, You must use a message.jelly. It is not possible
to do this in groovy.
]]>
</st:attribute>
</st:documentation>
<j:set var="severity" value="${attrs.severity ?: 'warning'}"/>
<j:set var="dismissable" value="${attrs.dismissable ?: true}"/>
<j:set var="dismissValue" value="${attrs.dismissValue ?: '%Dismiss'}"/>
<st:adjunct includes="lib.layout.admin-monitor"/>
<div id="${attrs.id}" class="app-adminmonitor jenkins-alert jenkins-alert-${severity} ${attrs.class}" data-monitor-id="${it.id}">
<j:choose>
<j:when test="${severity == 'danger'}">
<l:icon src="symbol-alert-circle-outline" class="app-adminmonitor-icon"/>
</j:when>
<j:when test="${severity == 'warning'}">
<l:icon src="symbol-warning-outline" class="app-adminmonitor-icon"/>
</j:when>
<j:otherwise>
<l:icon src="symbol-information-circle" class="app-adminmonitor-icon"/>
</j:otherwise>
</j:choose>
<div class="app-adminmonitor-content">
<div class="app-adminmonitor-controls">
<j:out value="${attrs.controls}"/>
<l:isAdmin>
<j:if test="${dismissable}">
<button class="app-adminmonitor-dismiss-button jenkins-button" type="button" tooltip="${dismissValue}" data-url="${rootURL}/${it.url}/disable">
<l:icon src="symbol-close" />
</button>
</j:if>
</l:isAdmin>
</div>
<d:invokeBody />
</div>
</div>
</j:jelly>
+53
View File
@@ -19,3 +19,56 @@ searchBarInput.suggestions = function () {
}))
.filter((item) => !item.url.endsWith("#"));
};
document.addEventListener("DOMContentLoaded", function () {
const messagesContainer = document.querySelector(".manage-messages");
if (!messagesContainer) {
return;
}
const updateLastVisibleMessageMargin = () => {
const messageDivs = Array.from(messagesContainer.children).filter(
(el) => el.tagName === "DIV",
);
messageDivs.forEach((el) => {
el.style.marginBottom = "";
});
const visibleDivs = messageDivs.filter((el) => {
const style = window.getComputedStyle(el);
return (
style.display !== "none" &&
style.visibility !== "hidden" &&
!el.hasAttribute("hidden") &&
el.getClientRects().length > 0
);
});
const lastVisible = visibleDivs[visibleDivs.length - 1];
if (lastVisible) {
lastVisible.style.marginBottom = "var(--section-padding)";
}
};
let rafId = null;
const scheduleUpdate = () => {
if (rafId !== null) {
return;
}
rafId = requestAnimationFrame(() => {
rafId = null;
updateLastVisibleMessageMargin();
});
};
updateLastVisibleMessageMargin();
const observer = new MutationObserver(scheduleUpdate);
observer.observe(messagesContainer, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ["style", "class", "hidden", "aria-hidden"],
});
});
+9 -5
View File
@@ -88,11 +88,15 @@ $semantics: (
--alert-success-text-color: var(--success-color);
--alert-success-bg-color: color-mix(
in sRGB,
var(--success-color) 10%,
var(--success-color) 4%,
transparent
);
--alert-info-text-color: var(--accent-color);
--alert-info-bg-color: color-mix(
in sRGB,
var(--accent-color) 4%,
transparent
);
--alert-info-text-color: var(--blue);
--alert-info-bg-color: color-mix(in sRGB, var(--blue) 10%, transparent);
--alert-warning-text-color: color-mix(
in sRGB,
var(--warning-color) 80%,
@@ -100,13 +104,13 @@ $semantics: (
);
--alert-warning-bg-color: color-mix(
in sRGB,
var(--warning-color) 10%,
var(--warning-color) 4%,
transparent
);
--alert-danger-text-color: var(--error-color);
--alert-danger-bg-color: color-mix(
in sRGB,
var(--error-color) 10%,
var(--error-color) 4%,
transparent
);
+2 -2
View File
@@ -1,9 +1,9 @@
.alert,
.jenkins-alert {
font-size: var(--font-size-sm);
padding: 15px;
padding: 1rem;
margin-bottom: var(--section-padding);
border: var(--jenkins-border--inherit-subtle);
border: var(--jenkins-border-width) solid rgb(from currentColor r g b / 0.1);
border-radius: var(--form-input-border-radius);
strong {
+71 -6
View File
@@ -1,3 +1,72 @@
@use "../abstracts/mixins";
.app-adminmonitor {
display: grid;
grid-template-columns: auto 1fr;
align-items: start;
gap: 0.5rem;
font-weight: 450;
&-icon {
width: 1.375rem;
height: 1.375rem;
margin: -0.0625rem 0 -0.0625rem -0.0625rem;
* {
stroke-width: 34px;
}
}
&-content {
margin-bottom: -8px;
}
&-controls {
margin: -8px -8px 0 4px !important;
float: right;
display: flex;
align-items: center;
gap: 0.5rem;
& > form {
margin: 0 !important;
}
.jenkins-button {
min-height: 36px;
padding: 0 14px;
}
}
&-dismiss-button {
--button-background: none;
--button-background--hover: oklch(from currentColor l c h / 0.1);
--button-background--active: oklch(from currentColor l c h / 0.2);
--button-box-shadow--focus: oklch(from currentColor l c h / 0.1);
&:not(.jenkins-button--primary) {
&::before {
border: var(--jenkins-border-width) solid
rgb(from currentColor r g b / 0.1);
}
}
color: var(--color) !important;
min-width: 36px;
padding: 0 !important;
}
}
.manage-messages {
display: flex;
flex-direction: column;
gap: calc(var(--section-padding) / 3);
& > div {
margin-bottom: 0;
}
}
.manage-messages a:not(.jenkins-button),
.manage-messages a:visited:not(.jenkins-button) {
color: inherit !important;
@@ -9,7 +78,7 @@
text-decoration: underline;
}
.manage-messages dl:first-child {
.manage-messages dl:first-of-type {
margin-top: 0;
}
@@ -36,10 +105,6 @@
.manage-messages .alert,
.manage-messages .jenkins-alert {
&:last-of-type {
margin-bottom: 30px;
}
a {
text-decoration: underline;
}
@@ -47,7 +112,7 @@
form {
position: relative;
float: right;
margin: -6px 0 0 !important;
margin: -6px 0 0;
display: flex;
gap: 0.5rem;
@@ -2,29 +2,21 @@ package hudson.diagnosis;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import hudson.model.User;
import hudson.security.GlobalMatrixAuthorizationStrategy;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URI;
import java.util.List;
import jenkins.model.Jenkins;
import org.htmlunit.ElementNotFoundException;
import org.htmlunit.HttpMethod;
import org.htmlunit.Page;
import org.htmlunit.WebRequest;
import org.htmlunit.html.HtmlForm;
import org.htmlunit.html.HtmlPage;
import org.htmlunit.util.NameValuePair;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.junit.jupiter.WithJenkins;
import org.xml.sax.SAXException;
/**
* @author Kohsuke Kawaguchi
@@ -39,25 +31,6 @@ class HudsonHomeDiskUsageMonitorTest {
j = rule;
}
@Test
void flow() throws Exception {
// manually activate this
HudsonHomeDiskUsageMonitor mon = HudsonHomeDiskUsageMonitor.get();
mon.activated = true;
// clicking yes should take us to somewhere
j.submit(getForm(mon), "yes");
assertTrue(mon.isEnabled());
// now dismiss
// submit(getForm(mon),"no"); TODO: figure out why this test is fragile
mon.doAct("no");
assertFalse(mon.isEnabled());
// and make sure it's gone
assertThrows(ElementNotFoundException.class, () -> getForm(mon));
}
@Issue("SECURITY-371")
@Test
void noAccessForNonAdmin() throws Exception {
@@ -77,9 +50,7 @@ class HudsonHomeDiskUsageMonitorTest {
User bob = User.getById("bob", true);
User administrator = User.getById("administrator", true);
WebRequest request = new WebRequest(new URI(wc.getContextPath() + "administrativeMonitor/hudsonHomeIsFull/act").toURL(), HttpMethod.POST);
NameValuePair param = new NameValuePair("no", "true");
request.setRequestParameters(List.of(param));
WebRequest request = new WebRequest(new URI(wc.getContextPath() + "administrativeMonitor/hudsonHomeIsFull/disable").toURL(), HttpMethod.POST);
HudsonHomeDiskUsageMonitor mon = HudsonHomeDiskUsageMonitor.get();
@@ -94,18 +65,9 @@ class HudsonHomeDiskUsageMonitorTest {
assertEquals(HttpURLConnection.HTTP_FORBIDDEN, p.getWebResponse().getStatusCode());
wc.withBasicApiToken(administrator);
request = new WebRequest(new URI(wc.getContextPath() + "administrativeMonitor/hudsonHomeIsFull/act").toURL(), HttpMethod.POST);
request.setRequestParameters(List.of(param));
request = new WebRequest(new URI(wc.getContextPath() + "administrativeMonitor/hudsonHomeIsFull/disable").toURL(), HttpMethod.POST);
p = wc.getPage(request);
assertEquals(HttpURLConnection.HTTP_OK, p.getWebResponse().getStatusCode());
assertFalse(mon.isEnabled());
}
/**
* Gets the warning form.
*/
private HtmlForm getForm(HudsonHomeDiskUsageMonitor mon) throws IOException, SAXException {
HtmlPage p = j.createWebClient().goTo("manage");
return p.getFormByName(mon.id);
}
}
@@ -5,19 +5,16 @@ import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import hudson.model.AdministrativeMonitor;
import hudson.model.Item;
import hudson.model.ListView;
import hudson.model.View;
import java.io.IOException;
import java.net.URL;
import jenkins.model.Jenkins;
import org.htmlunit.ElementNotFoundException;
import org.htmlunit.html.DomElement;
import org.htmlunit.html.HtmlForm;
import org.htmlunit.html.HtmlAnchor;
import org.htmlunit.html.HtmlPage;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -39,7 +36,6 @@ class TooManyJobsButNoViewTest {
@BeforeEach
void setUp(JenkinsRule rule) {
r = rule;
mon = AdministrativeMonitor.all().get(TooManyJobsButNoView.class);
}
/**
@@ -47,12 +43,9 @@ class TooManyJobsButNoViewTest {
*/
@Test
void initialState() throws Exception {
verifyNoForm();
}
private void verifyNoForm() throws IOException, SAXException {
HtmlPage p = r.createWebClient().goTo("manage");
assertThrows(ElementNotFoundException.class, () -> p.getFormByName(mon.id));
try (JenkinsRule.WebClient wc = r.createWebClient()) {
verifyNoMonitor(wc);
}
}
/**
@@ -63,22 +56,23 @@ class TooManyJobsButNoViewTest {
for (int i = 0; i <= TooManyJobsButNoView.THRESHOLD; i++)
r.createFreeStyleProject();
HtmlPage p = r.createWebClient().goTo("manage");
HtmlForm f = p.getFormByName(mon.id);
assertNotNull(f);
try (JenkinsRule.WebClient wc = r.createWebClient()) {
HtmlPage p = wc.goTo("manage");
HtmlAnchor link = p.querySelector("div[data-monitor-id=\"hudson.diagnosis.TooManyJobsButNoView\"] a");
assertNotNull(link);
// this should take us to the new view page
URL url = r.submit(f, "yes").getUrl();
assertTrue(url.toExternalForm().endsWith("/newView"), url.toExternalForm());
// this should take us to the new view page
URL url = link.click().getUrl();
assertTrue(url.toExternalForm().endsWith("/newView"), url.toExternalForm());
// since we didn't create a view, if we go back, we should see the warning again
p = r.createWebClient().goTo("manage");
assertNotNull(p.getFormByName(mon.id));
// since we didn't create a view, if we go back, we should see the warning again
verifyMonitor(wc);
// once we create a view, the message should disappear
r.jenkins.addView(new ListView("test"));
// once we create a view, the message should disappear
r.jenkins.addView(new ListView("test"));
verifyNoForm();
verifyNoMonitor(wc);
}
}
@Test
@@ -102,7 +96,7 @@ class TooManyJobsButNoViewTest {
private void verifyNoMonitor(JenkinsRule.WebClient wc) throws IOException, SAXException {
HtmlPage p = wc.goTo("manage");
DomElement adminMonitorDiv = p.getElementById("tooManyJobsButNoView");
DomElement adminMonitorDiv = p.querySelector("div[data-monitor-id=\"hudson.diagnosis.TooManyJobsButNoView\"]");
assertThat(adminMonitorDiv, is(nullValue()));
}
@@ -129,9 +123,8 @@ class TooManyJobsButNoViewTest {
private void verifyMonitor(JenkinsRule.WebClient wc) throws IOException, SAXException {
HtmlPage p = wc.goTo("manage");
DomElement adminMonitorDiv = p.getElementById("tooManyJobsButNoView");
DomElement adminMonitorDiv = p.querySelector("div[data-monitor-id=\"hudson.diagnosis.TooManyJobsButNoView\"]");
assertThat(adminMonitorDiv, is(notNullValue()));
assertThat(adminMonitorDiv.getTextContent(), is(notNullValue()));
}
}
@@ -19,6 +19,7 @@ import jenkins.security.csp.CspHeaderDecider;
import org.hamcrest.Matcher;
import org.htmlunit.Page;
import org.htmlunit.html.DomElement;
import org.htmlunit.html.HtmlAnchor;
import org.htmlunit.html.HtmlCheckBoxInput;
import org.htmlunit.html.HtmlFormUtil;
import org.htmlunit.html.HtmlPage;
@@ -198,7 +199,8 @@ public class CspHeaderDeciderTest {
assertTrue(ExtensionList.lookupSingleton(CspRecommendation.class).isActivated());
final HtmlPage htmlPage = webClient.goTo("manage/");
final Page recommendationClick = htmlPage.getElementByName("more").click();
HtmlAnchor link = htmlPage.querySelector("div[data-monitor-id=\"jenkins.security.csp.impl.CspRecommendation\"] a");
final Page recommendationClick = link.click();
assertThat(recommendationClick, instanceOf(HtmlPage.class));
HtmlPage recommendationPage = (HtmlPage) recommendationClick;
assertThat(
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"><path d="M448 256c0-106-86-192-192-192S64 150 64 256s86 192 192 192 192-86 192-192z" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"/><path d="M250.26 166.05L256 288l5.73-121.95a5.74 5.74 0 00-5.79-6h0a5.74 5.74 0 00-5.68 6z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/><path d="M256 367.91a20 20 0 1120-20 20 20 0 01-20 20z" fill="currentColor"/></svg>

After

Width:  |  Height:  |  Size: 514 B

@@ -1 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"><path d="M248 64C146.39 64 64 146.39 64 248s82.39 184 184 184 184-82.39 184-184S349.61 64 248 64z" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"/><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M220 220h32v116"/><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-miterlimit="10" stroke-width="32" d="M208 340h88"/><path fill="currentColor" d="M248 130a26 26 0 1026 26 26 26 0 00-26-26z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<path d="M448 256c0-106-86-192-192-192S64 150 64 256s86 192 192 192 192-86 192-192z" fill="none" stroke="currentColor"
stroke-miterlimit="10" stroke-width="32" />
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="32"
d="M224 220h32v116"
/>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-miterlimit="10"
stroke-width="32"
d="M212 340h88"
/>
<path
fill="currentColor"
d="M255 130a26 26 0 1026 26 26 26 0 00-26-26z"
/>
</svg>

Before

Width:  |  Height:  |  Size: 581 B

After

Width:  |  Height:  |  Size: 666 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"><path d="M85.57 446.25h340.86a32 32 0 0028.17-47.17L284.18 82.58c-12.09-22.44-44.27-22.44-56.36 0L57.4 399.08a32 32 0 0028.17 47.17z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/><path d="M250.26 195.39l5.74 122 5.73-121.95a5.74 5.74 0 00-5.79-6h0a5.74 5.74 0 00-5.68 5.95z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/><path d="M256 397.25a20 20 0 1120-20 20 20 0 01-20 20z" fill="currentColor"/></svg>

After

Width:  |  Height:  |  Size: 591 B