Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions core/src/main/java/hudson/Functions.java
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
import hudson.tasks.Publisher;
import hudson.tasks.UserAvatarResolver;
import hudson.util.Area;
import hudson.util.FormApply;
import hudson.util.FormValidation.CheckMethod;
import hudson.util.HudsonIsLoading;
import hudson.util.HudsonIsRestarting;
Expand Down Expand Up @@ -217,6 +218,11 @@ public class Functions {
public Functions() {
}

@Restricted(NoExternalUse.class)
public @CheckForNull FormApply.Notification getFormApplyNotification() {
return FormApply.getAndClearNotification(Stapler.getCurrentRequest2());
}

/**
* Generates an unique ID.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ public synchronized void doConfigure(StaplerRequest2 req, StaplerResponse2 rsp)
boolean result = configure(req, json);
LOGGER.log(Level.FINE, "security saved: " + result);
Jenkins.get().save();
FormApply.success(req.getContextPath() + "/manage").generateResponse(req, rsp, null);
FormApply.success(req.getContextPath() + "/manage/" + getUrlName()).generateResponse(req, rsp, null);
} catch (JSONException x) {
LOGGER.warning(() -> "Bad JSON:\n" + json.toString(2));
throw x;
Expand Down
47 changes: 47 additions & 0 deletions core/src/main/java/hudson/util/FormApply.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@

package hudson.util;

import edu.umd.cs.findbugs.annotations.CheckForNull;
import hudson.Functions;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import jenkins.model.Jenkins;
import org.kohsuke.stapler.HttpResponses.HttpResponseException;
Expand All @@ -40,6 +42,9 @@
* @since 1.453
*/
public class FormApply {
private static final String NOTIFICATION_MESSAGE_SESSION_ATTRIBUTE = FormApply.class.getName() + ".notificationMessage";
private static final String NOTIFICATION_TYPE_SESSION_ATTRIBUTE = FormApply.class.getName() + ".notificationType";

/**
* Generates the response for the form submission in such a way that it handles the "apply" button
* correctly.
Expand All @@ -56,6 +61,7 @@
showNotification(Messages.HttpResponses_Saved(), NotificationType.SUCCESS)
.generateResponse(req, rsp, node);
} else {
setNotificationInSession(req, Messages.HttpResponses_Saved(), NotificationType.SUCCESS);
rsp.sendRedirect(destination);
}
}
Expand Down Expand Up @@ -127,6 +133,47 @@
};
}

private static void setNotificationInSession(StaplerRequest2 req, String message, NotificationType notificationType) {
HttpSession session = req.getSession();
session.setAttribute(NOTIFICATION_MESSAGE_SESSION_ATTRIBUTE, message);
session.setAttribute(NOTIFICATION_TYPE_SESSION_ATTRIBUTE, notificationType.name());
}

public static @CheckForNull Notification getAndClearNotification(StaplerRequest2 req) {
HttpSession session = req.getSession(false);
if (session == null) {

Check warning on line 144 in core/src/main/java/hudson/util/FormApply.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 144 is only partially covered, one branch is missing
return null;

Check warning on line 145 in core/src/main/java/hudson/util/FormApply.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 145 is not covered by tests
}

String message = (String) session.getAttribute(NOTIFICATION_MESSAGE_SESSION_ATTRIBUTE);
String notificationType = (String) session.getAttribute(NOTIFICATION_TYPE_SESSION_ATTRIBUTE);
session.removeAttribute(NOTIFICATION_MESSAGE_SESSION_ATTRIBUTE);
session.removeAttribute(NOTIFICATION_TYPE_SESSION_ATTRIBUTE);

if (message == null || notificationType == null) {

Check warning on line 153 in core/src/main/java/hudson/util/FormApply.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 153 is only partially covered, one branch is missing
return null;
}

return new Notification(message, NotificationType.valueOf(notificationType));
}

public static final class Notification {
private final String message;
private final NotificationType notificationType;

private Notification(String message, NotificationType notificationType) {
this.message = message;
this.notificationType = notificationType;
}

public String getMessage() {
return message;
}

public NotificationType getNotificationType() {
return notificationType;
}
}

/**
* Corresponds to types declared in <a href="https://github.com/jenkinsci/jenkins/blob/74610e024a6b8fd8feccdc51b8f7741aa6c30e3b/war/src/main/js/components/notifications/index.js#L13-L25">index.js</a>
Expand Down
2 changes: 1 addition & 1 deletion core/src/main/java/jenkins/agents/CloudSet.java
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@
var clouds = new ArrayList<>(Jenkins.get().clouds);
clouds.sort(Comparator.comparingInt(c -> getIndexOf(namesList, c)));
Jenkins.get().clouds.replaceBy(clouds);
FormApply.success(req.getContextPath() + "/manage").generateResponse(req, rsp, null);
FormApply.success(req.getContextPath() + "/manage/" + getUrlName()).generateResponse(req, rsp, null);

Check warning on line 288 in core/src/main/java/jenkins/agents/CloudSet.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 288 is not covered by tests
}

private static int getIndexOf(List<String> namesList, Cloud cloud) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@
public synchronized void doConfigure(StaplerRequest2 req, StaplerResponse2 rsp) throws IOException, ServletException, Descriptor.FormException {
boolean result = configure(req, req.getSubmittedForm());
LOGGER.log(Level.FINE, "appearance saved: " + result);
FormApply.success(req.getContextPath() + "/manage").generateResponse(req, rsp, null);
FormApply.success(req.getContextPath() + "/manage/" + getUrlName()).generateResponse(req, rsp, null);

Check warning on line 104 in core/src/main/java/jenkins/appearance/AppearanceGlobalConfiguration.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 104 is not covered by tests
}

private boolean configure(StaplerRequest2 req, JSONObject json) throws Descriptor.FormException, IOException {
Expand Down
2 changes: 1 addition & 1 deletion core/src/main/java/jenkins/model/Jenkins.java
Original file line number Diff line number Diff line change
Expand Up @@ -4045,7 +4045,7 @@ public synchronized void doConfigSubmit(StaplerRequest2 req, StaplerResponse2 rs
save();
updateComputers(this);
if (result)
FormApply.success(req.getContextPath() + '/').generateResponse(req, rsp, null);
FormApply.success(req.getContextPath() + "/manage/configure").generateResponse(req, rsp, null);
else
FormApply.success("configure").generateResponse(req, rsp, null); // back to config

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ public Category getCategory() {
public synchronized void doConfigure(StaplerRequest2 req, StaplerResponse2 rsp) throws IOException, ServletException, Descriptor.FormException {
boolean result = configure(req, req.getSubmittedForm());
LOGGER.log(Level.FINE, "tools saved: " + result);
FormApply.success(req.getContextPath() + "/manage").generateResponse(req, rsp, null);
FormApply.success(req.getContextPath() + "/manage/" + getUrlName()).generateResponse(req, rsp, null);
}

private boolean configure(StaplerRequest2 req, JSONObject json) throws Descriptor.FormException, IOException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import hudson.markup.MarkupFormatterDescriptor
import hudson.security.AuthorizationStrategy
import hudson.Functions
import hudson.model.Descriptor
import jenkins.model.experimentalflags.UserExperimentalFlag

def f=namespace(lib.FormTagLib)
def l=namespace(lib.LayoutTagLib)
def st=namespace("jelly:stapler")
def newManageJenkins = UserExperimentalFlag.getFlagValueForCurrentUser("jenkins.model.experimentalflags.NewManageJenkinsUserExperimentalFlag")

l.'settings-subpage'(permission: app.SYSTEM_READ) {
set("readOnlyMode", !app.hasPermission(app.ADMINISTER))
Expand Down Expand Up @@ -55,15 +57,11 @@ l.'settings-subpage'(permission: app.SYSTEM_READ) {
}

l.isAdmin() {
f.bottomButtonBar {
f.submit(value: _("Save"))
f.apply()
if (newManageJenkins) {
f.saveBar()
} else {
f.saveApplyBar()
}
}
}

l.isAdmin() {
st.adjunct(includes: "lib.form.confirm")
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ THE SOFTWARE.

<l:settings-subpage header="${header}" permissions="${app.MANAGE_AND_SYSTEM_READ}">
<j:set var="readOnlyMode" value="${!app.hasPermission(app.MANAGE)}"/>
<l:userExperimentalFlag var="newManageJenkins" flagClassName="jenkins.model.experimentalflags.NewManageJenkinsUserExperimentalFlag" />
<j:set var="pluginsUrl" value="${rootURL}/manage/pluginManager/available?filter=UI Themes" />
<j:set var="hasPlugins" value="${it.hasPlugins()}" />

Expand All @@ -71,12 +72,15 @@ THE SOFTWARE.
</f:rowSet>
</j:forEach>

<f:saveApplyBar/>
<j:choose>
<j:when test="${newManageJenkins}">
<f:saveBar/>
</j:when>
<j:otherwise>
<f:saveApplyBar/>
</j:otherwise>
</j:choose>
</f:form>

<l:hasAdministerOrManage>
<st:adjunct includes="lib.form.confirm" />
</l:hasAdministerOrManage>
</j:otherwise>
</j:choose>
</l:settings-subpage>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,14 @@ THE SOFTWARE.
</f:rowSet>
</j:forEach>
<j:set var="readOnlyMode" value="${!h.hasPermission(app.MANAGE)}"/>
<f:saveApplyBar/>
<j:choose>
<j:when test="${newManageJenkins}">
<f:saveBar/>
</j:when>
<j:otherwise>
<f:saveApplyBar/>
</j:otherwise>
</j:choose>
</f:form>
</l:settings-subpage>
</j:jelly>
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ package jenkins.tools.GlobalToolConfiguration

import hudson.Functions
import hudson.model.Descriptor
import jenkins.model.experimentalflags.UserExperimentalFlag

def f=namespace(lib.FormTagLib)
def l=namespace(lib.LayoutTagLib)
def st=namespace("jelly:stapler")
def newManageJenkins = UserExperimentalFlag.getFlagValueForCurrentUser("jenkins.model.experimentalflags.NewManageJenkinsUserExperimentalFlag")

l.'settings-subpage'(permission: app.SYSTEM_READ) {
set("readOnlyMode", !app.hasPermission(app.ADMINISTER))
Expand All @@ -22,14 +24,11 @@ l.'settings-subpage'(permission: app.SYSTEM_READ) {
}

l.isAdmin() {
f.bottomButtonBar {
f.submit(value: _("Save"))
f.apply(value: _("Apply"))
if (newManageJenkins) {
f.saveBar()
} else {
f.saveApplyBar()
}
}
}

l.isAdmin() {
st.adjunct(includes: "lib.form.confirm")
}
}
43 changes: 43 additions & 0 deletions core/src/main/resources/lib/form/saveBar.jelly
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<!--
The MIT License

Copyright (c) 2026, Jan Faracik

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.
-->

<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:f="/lib/form">
<st:documentation>
Creates the bottom bar for the "Save" button.
In read only mode (&lt;j:set var="readOnlyMode" value="true"/&gt;) the button is not displayed.

<st:attribute name="hidden">
Optionally hides the save bar, can then be made visible with JavaScript.
Defaults to false.
</st:attribute>
</st:documentation>

<j:if test="${!readOnlyMode}">
<f:bottomButtonBar hidden="${attrs.hidden}">
<f:submit value="${%Save}" />
</f:bottomButtonBar>
<st:adjunct includes="lib.form.confirm" />
</j:if>
</j:jelly>
5 changes: 4 additions & 1 deletion core/src/main/resources/lib/layout/layout.jelly
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ THE SOFTWARE.
</j:if>

<j:set var="_" value="${request2.getSession()}"/>
<j:set var="formApplyNotification" value="${h.formApplyNotification}"/>
<j:set var="extensionsAvailable" value="${h.extensionsAvailable}"/>
<j:if test="${request2.servletPath=='/' || request2.servletPath==''}">
${h.advertiseHeaders(response2)}
Expand Down Expand Up @@ -155,7 +156,9 @@ THE SOFTWARE.
</head>
<body id="jenkins" class="${layoutType} jenkins-${h.version}" data-version="${h.version}" data-model-type="${it.class.name}"
data-search-url="${rootURL + '/search/suggest'}"
data-search-help-url="${%searchBox.url}">
data-search-help-url="${%searchBox.url}"
data-notification-message="${formApplyNotification.message}"
data-notification-type="${formApplyNotification.notificationType}">
<l:command-palette />

<j:if test="${layoutType!='full-screen'}">
Expand Down
15 changes: 15 additions & 0 deletions src/main/js/components/notifications/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,21 @@ function init() {
}
},
};

tryShowPageLoadNotification();
}

function tryShowPageLoadNotification() {
const { notificationMessage, notificationType } = document.body.dataset;

if (!notificationMessage) {
return;
}

window.notificationBar.show(
notificationMessage,
window.notificationBar[notificationType],
);
}

export default { init };
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ public void testFallbackAdminMonitorAndSetup(JenkinsRule j) throws IOException,

final Page afterSavingPage = HtmlFormUtil.submit(setupPage.getFormByName("config"), setupPage.getFormByName("config").getButtonByName("Submit"));
assertThat(afterSavingPage, instanceOf(HtmlPage.class));
assertThat(afterSavingPage.getUrl().getPath(), is(j.contextPath + "/manage/"));
assertThat(afterSavingPage.getUrl().getPath(), is(j.contextPath + "/manage/configureSecurity/"));
assertThat(afterSavingPage.getWebResponse().getResponseHeaderValue("Content-Security-Policy"), not(nullValue()));
assertThat(afterSavingPage.getWebResponse().getResponseHeaderValue("Content-Security-Policy-Report-Only"), nullValue());

Expand Down
Loading