Add experimental Manage Jenkins layout (#11222)

Co-authored-by: Tim Jacomb <21194782+timja@users.noreply.github.com>
This commit is contained in:
Jan FaracikandTim Jacomb authored and GitHub committed 2025-11-17 16:26:44 +00:00
1 parent 2a6f83dccb
commit 23ff4bcb63
34 files changed
+1001 -576

No files matched your search

@@ -460,7 +460,7 @@ public class OldDataMonitor extends AdministrativeMonitor {
@Override
public String getUrlName() {
return "administrativeMonitor/OldData/";
return "administrativeMonitor/OldData/manage";
}
@Override
@@ -28,16 +28,19 @@ import hudson.Extension;
import hudson.Util;
import hudson.util.HudsonIsLoading;
import hudson.util.HudsonIsRestarting;
import jakarta.servlet.ServletException;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import jenkins.management.Badge;
import jenkins.model.Jenkins;
import jenkins.model.ModelObjectWithContextMenu;
import jenkins.model.experimentalflags.NewManageJenkinsUserExperimentalFlag;
import org.apache.commons.jelly.JellyException;
import org.jenkinsci.Symbol;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.HttpRedirect;
import org.kohsuke.stapler.Stapler;
import org.kohsuke.stapler.StaplerFallback;
import org.kohsuke.stapler.StaplerRequest2;
@@ -76,6 +79,21 @@ public class ManageJenkinsAction implements RootAction, StaplerFallback, ModelOb
return true;
}
public HttpRedirect doIndex(StaplerRequest2 req, StaplerResponse2 rsp) throws IOException {
try {
var newUiEnabled = new NewManageJenkinsUserExperimentalFlag().getFlagValue();
if (newUiEnabled) {
return new HttpRedirect("configure");
}
req.getView(this, "index.jelly").forward(req, rsp);
} catch (ServletException e) {
throw new RuntimeException(e);
}
return null;
}
@Override
public Object getStaplerFallback() {
return Jenkins.get();
@@ -0,0 +1,49 @@
/*
* The MIT License
*
* Copyright (c) 2025, 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.
*/
package jenkins.model.experimentalflags;
import edu.umd.cs.findbugs.annotations.Nullable;
import hudson.Extension;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
@Extension
@Restricted(NoExternalUse.class)
public class NewManageJenkinsUserExperimentalFlag extends BooleanUserExperimentalFlag {
public NewManageJenkinsUserExperimentalFlag() {
super("new-manage-jenkins.flag");
}
@Override
public String getDisplayName() {
return "New Manage Jenkins UI";
}
@Nullable
@Override
public String getShortDescription() {
return "Enables a sidebar for the Manage Jenkins pages for easier navigation.";
}
}
@@ -25,18 +25,9 @@ THE SOFTWARE.
<!-- About Jenkins page -->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:l="/lib/layout" xmlns:t="/lib/hudson">
<l:layout type="one-column" permissions="${app.MANAGE_AND_SYSTEM_READ}" title="${%about(app.VERSION)}">
<l:header>
<script src="${resURL}/jsbundles/section-to-tabs.js" type="text/javascript" defer="true" />
</l:header>
<l:main-panel>
<div class="app-about-branding">
<div class="app-about-branding__aurora"></div>
<img src="${imagesURL}/svgs/logo.svg" alt="${%logo}" />
</div>
<div class="jenkins-app-bar">
<j:set var="header">
<l:view>
<div class="jenkins-app-bar jenkins-!-margin-top-2 jenkins-!-margin-bottom-4">
<div class="jenkins-app-bar__content">
<h1 class="app-about-heading">Jenkins</h1>
<p class="app-about-version">
@@ -50,93 +41,104 @@ THE SOFTWARE.
</a>
</div>
</div>
</l:view>
</j:set>
<p class="app-about-paragraph">${%blurb}</p>
<l:settings-subpage header="${header}"
placeholder="${null}"
permissions="${app.MANAGE_AND_SYSTEM_READ}">
<script src="${resURL}/jsbundles/section-to-tabs.js" type="text/javascript" defer="true" />
<div class="jenkins-tab-pane">
<h2 class="jenkins-tab-pane__title">${%maven.dependencies}</h2>
<j:set var="uri" value="${it.licensesURL}"/>
<j:choose>
<j:when test="${uri != null}">
<t:thirdPartyLicenses>
<j:include uri="${uri}"/>
</t:thirdPartyLicenses>
</j:when>
<j:otherwise>
<p>${%No information recorded}</p>
</j:otherwise>
</j:choose>
</div>
<div class="app-about-branding">
<div class="app-about-branding__aurora"></div>
<img src="${imagesURL}/svgs/logo.svg" alt="${%logo}" />
</div>
<div class="jenkins-tab-pane">
<h2 class="jenkins-tab-pane__title">${%static.dependencies}</h2>
<table class="jenkins-table sortable">
<thead>
<tr>
<th>${%Name}</th>
<th>${%Author}</th>
<th>${%Licence}</th>
</tr>
</thead>
<tbody>
<p class="jenkins-page-description jenkins-!-text-color">${%blurb}</p>
<div class="jenkins-tab-pane">
<h2 class="jenkins-tab-pane__title">${%maven.dependencies}</h2>
<j:set var="uri" value="${it.licensesURL}"/>
<j:choose>
<j:when test="${uri != null}">
<t:thirdPartyLicenses>
<j:include uri="${uri}"/>
</t:thirdPartyLicenses>
</j:when>
<j:otherwise>
<p>${%No information recorded}</p>
</j:otherwise>
</j:choose>
</div>
<div class="jenkins-tab-pane">
<h2 class="jenkins-tab-pane__title">${%static.dependencies}</h2>
<table class="jenkins-table sortable">
<thead>
<tr>
<th>${%Name}</th>
<th>${%Author}</th>
<th>${%Licence}</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<a class="jenkins-table__link" href="https://github.com/jenkins-contrib-themes/jenkins-core-theme">
Jenkins Contrib Themes
</a>
</td>
<td>
<a class="jenkins-table__link" href="https://github.com/afonsof">Afonso Franca</a>
</td>
<td>
<a class="jenkins-table__link" href="https://opensource.org/licenses/MIT">MIT License</a>
</td>
</tr>
<tr>
<td>
<a class="jenkins-table__link" href="https://ionic.io/ionicons">Ionicons</a>
</td>
<td>
<a class="jenkins-table__link" href="https://github.com/ionic-team">Ionic</a>
</td>
<td>
<a class="jenkins-table__link" href="https://github.com/ionic-team/ionicons/blob/master/LICENSE">MIT
License
</a>
</td>
</tr>
</tbody>
</table>
</div>
<div class="jenkins-tab-pane">
<h2 class="jenkins-tab-pane__title">${%plugin.dependencies}</h2>
<table class="jenkins-table sortable">
<thead>
<tr>
<th>${%Name}</th>
</tr>
</thead>
<tbody>
<j:forEach var="p" items="${app.pluginManager.plugins}">
<tr>
<td>
<a class="jenkins-table__link" href="https://github.com/jenkins-contrib-themes/jenkins-core-theme">
Jenkins Contrib Themes
</a>
</td>
<td>
<a class="jenkins-table__link" href="https://github.com/afonsof">Afonso Franca</a>
</td>
<td>
<a class="jenkins-table__link" href="https://opensource.org/licenses/MIT">MIT License</a>
</td>
</tr>
<tr>
<td>
<a class="jenkins-table__link" href="https://ionic.io/ionicons">Ionicons</a>
</td>
<td>
<a class="jenkins-table__link" href="https://github.com/ionic-team">Ionic</a>
</td>
<td>
<a class="jenkins-table__link" href="https://github.com/ionic-team/ionicons/blob/master/LICENSE">MIT
License
<a class="jenkins-table__link" href="${rootURL}/plugin/${p.shortName}/wrapper/thirdPartyLicenses">
<j:choose>
<j:when test="${p.active}">
${p.displayName}
</j:when>
<j:otherwise>
<strike>${p.displayName}</strike>
</j:otherwise>
</j:choose>
</a>
</td>
</tr>
</tbody>
</table>
</div>
<div class="jenkins-tab-pane">
<h2 class="jenkins-tab-pane__title">${%plugin.dependencies}</h2>
<table class="jenkins-table sortable">
<thead>
<tr>
<th>${%Name}</th>
</tr>
</thead>
<tbody>
<j:forEach var="p" items="${app.pluginManager.plugins}">
<tr>
<td>
<a class="jenkins-table__link" href="${rootURL}/plugin/${p.shortName}/wrapper/thirdPartyLicenses">
<j:choose>
<j:when test="${p.active}">
${p.displayName}
</j:when>
<j:otherwise>
<strike>${p.displayName}</strike>
</j:otherwise>
</j:choose>
</a>
</td>
</tr>
</j:forEach>
</tbody>
</table>
</div>
</l:main-panel>
</l:layout>
</j:forEach>
</tbody>
</table>
</div>
</l:settings-subpage>
</j:jelly>
@@ -24,20 +24,20 @@ 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">
<j:new var="managementLink" className="jenkins.management.CliLink" />
<l:layout title="${managementLink.displayName}" permission="${app.READ}" type="one-column">
<!-- no need for additional breadcrumb here as we're on an index page already including breadcrumb -->
<l:main-panel>
<l:app-bar title="${managementLink.displayName}">
<j:set var="header">
<l:view>
<l:app-bar title="${it.displayName}">
<t:help href="https://www.jenkins.io/redirect/cli" />
</l:app-bar>
<div class="jenkins-page-description">
${%description}
</div>
</l:view>
</j:set>
<l:settings-subpage header="${header}" permission="${app.READ}">
<!-- no need for additional breadcrumb here as we're on an index page already including breadcrumb -->
<f:section title="${%Getting started}">
<ol class="jenkins-instructions">
<li>
@@ -79,6 +79,5 @@ THE SOFTWARE.
</tbody>
</table>
</f:section>
</l:main-panel>
</l:layout>
</l:settings-subpage>
</j:jelly>
@@ -24,11 +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">
<l:layout title="${%Manage Old Data}" type="one-column">
<!-- no need for additional breadcrumb here as we're on an "index" page already including breadcrumb -->
<l:main-panel>
<h1>${%Manage Old Data}</h1>
<p>${%blurb.1}</p>
<j:set var="previousIt" value="${it}" />
<j:new var="it" className="hudson.diagnosis.OldDataMonitor$ManagementLinkImpl" />
<l:settings-subpage>
<j:set var="it" value="${previousIt}" />
<p class="jenkins-!-margin-top-0">${%blurb.1}</p>
<p>${%blurb.2}</p>
<!-- First set a flag to check if we'll have any valid data -->
@@ -132,6 +134,5 @@ THE SOFTWARE.
<f:submit value="${%Discard Unreadable Data}"/>
</form>
</j:if>
</l:main-panel>
</l:layout>
</l:settings-subpage>
</j:jelly>
@@ -27,22 +27,30 @@ 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">
<l:layout title="${%Log Recorders}" type="one-column">
<!-- no need for additional breadcrumb here as we're on an index page already including breadcrumb -->
<l:main-panel xmlns:local="local">
<l:app-bar title="${%Log Recorders}">
<l:isAdmin>
<a href="new" class="jenkins-button jenkins-button--primary">
<l:icon src="symbol-add" />
${%Add recorder}
</a>
</l:isAdmin>
<a href="levels" class="jenkins-button">
${%Log levels}
</a>
<t:help tooltip="${%Additional information on log recorders}" href="https://www.jenkins.io/redirect/log-recorders" />
</l:app-bar>
<j:set var="header">
<j:new var="it" className="jenkins.management.SystemLogLink" />
<l:view>
<l:app-bar title="${it.displayName}">
<l:isAdmin>
<a href="new" class="jenkins-button jenkins-button--primary">
<l:icon src="symbol-add" />
${%Add recorder}
</a>
</l:isAdmin>
<a href="levels" class="jenkins-button">
${%Log levels}
</a>
<t:help tooltip="${%Additional information on log recorders}" href="https://www.jenkins.io/redirect/log-recorders" />
</l:app-bar>
<div class="jenkins-page-description">
<j:out value="${it.description}" />
</div>
</l:view>
</j:set>
<l:settings-subpage header="${header}" xmlns:local="local">
<d:taglib uri="local">
<d:tag name="row">
<tr>
@@ -76,6 +84,5 @@ THE SOFTWARE.
</j:forEach>
</tbody>
</table>
</l:main-panel>
</l:layout>
</l:settings-subpage>
</j:jelly>
@@ -27,32 +27,40 @@ 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:s="/lib/form">
<l:layout title="${%Nodes}" type="one-column">
<!-- no need for additional breadcrumb here as we're on an index page already including breadcrumb -->
<l:main-panel>
<l:app-bar title="${%Nodes}">
<j:getStatic var="createPermission" className="hudson.model.Computer" field="CREATE"/>
<j:if test="${h.hasPermission(createPermission)}">
<a class="jenkins-button jenkins-button--primary" href="new">
<l:icon src="symbol-add" />
${%New Node}
</a>
</j:if>
<j:set var="hasPermission" value="${h.hasAnyPermission(it, app.MANAGE_AND_SYSTEM_READ)}" />
<j:if test="${hasPermission}">
<a class="jenkins-button" href="configure" tooltip="${%Configure Node Monitors}">
${%Configure Monitors}
</a>
</j:if>
<l:hasAdministerOrManage>
<form method="post" action="updateNow" class="jenkins-!-display-contents">
<button tooltip="${%Refresh status}" class="jenkins-button">
<l:icon src="symbol-refresh" />
</button>
</form>
</l:hasAdministerOrManage>
</l:app-bar>
<j:set var="header">
<l:view>
<j:new var="it" className="jenkins.management.NodesLink" />
<l:app-bar title="${it.displayName}">
<j:getStatic var="createPermission" className="hudson.model.Computer" field="CREATE"/>
<j:if test="${h.hasPermission(createPermission)}">
<a class="jenkins-button jenkins-button--primary" href="new">
<l:icon src="symbol-add" />
${%New Node}
</a>
</j:if>
<j:set var="hasPermission" value="${h.hasAnyPermission(it, app.MANAGE_AND_SYSTEM_READ)}" />
<j:if test="${hasPermission}">
<a class="jenkins-button" href="configure" tooltip="${%Configure Node Monitors}">
${%Configure Monitors}
</a>
</j:if>
<l:hasAdministerOrManage>
<form method="post" action="updateNow" class="jenkins-!-display-contents">
<button tooltip="${%Refresh status}" class="jenkins-button">
<l:icon src="symbol-refresh" />
</button>
</form>
</l:hasAdministerOrManage>
</l:app-bar>
<div class="jenkins-page-description">
${it.description}
</div>
</l:view>
</j:set>
<l:settings-subpage header="${header}">
<j:set var="monitors" value="${it._monitors}"/>
<j:set var="tableWidth" value="${3}"/>
<t:setIconSize/>
@@ -133,6 +141,5 @@ THE SOFTWARE.
<st:include page="_legend.jelly"/>
</template>
<script src="${resURL}/jsbundles/pages/computer-set.js" type="text/javascript" defer="true" />
</l:main-panel>
</l:layout>
</l:settings-subpage>
</j:jelly>
@@ -24,11 +24,12 @@ THE SOFTWARE.
<!-- renders an HTML fragment that shows trend graph -->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:f="/lib/form">
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:f="/lib/form" xmlns:l="/lib/layout">
<st:adjunct includes="hudson.model.LoadStatistics.resources"/>
<h1>
${%title(it.displayName)}
</h1>
<j:if test="${it != app}">
<l:app-bar title="${%title(it.displayName)}" />
</j:if>
<f:entry title="${%Timespan}" class="jenkins-form-item--small">
<div class="jenkins-select">
@@ -10,64 +10,60 @@ def f=namespace(lib.FormTagLib)
def l=namespace(lib.LayoutTagLib)
def st=namespace("jelly:stapler")
l.layout(permission:app.SYSTEM_READ, title:my.displayName, cssclass:request2.getParameter('decorate'), type:"one-column") {
l.main_panel {
l.app_bar(title: my.displayName)
l.'settings-subpage'(permission: app.SYSTEM_READ) {
set("readOnlyMode", !app.hasPermission(app.ADMINISTER))
set("readOnlyMode", !app.hasPermission(app.ADMINISTER))
l.skeleton()
f.form(method:"post", name:"config", action:"configure", class: "jenkins-form") {
set("instance", my)
set("descriptor", my.descriptor)
l.skeleton()
f.form(method:"post", name:"config", action:"configure", class: "jenkins-form") {
set("instance", my)
set("descriptor", my.descriptor)
f.section(title:_("Authentication")) {
f.entry(help: '/descriptor/hudson.security.GlobalSecurityConfiguration/help/disableRememberMe') {
f.checkbox(title:_("Disable remember me"), field: "disableRememberMe")
}
f.dropdownDescriptorSelector(title: _("Security Realm"), field: 'securityRealm', descriptors: h.filterDescriptors(app, SecurityRealm.all()))
f.dropdownDescriptorSelector(title: _("Authorization"), field: 'authorizationStrategy', descriptors: h.filterDescriptors(app, AuthorizationStrategy.all()))
f.section(title:_("Authentication")) {
f.entry(help: '/descriptor/hudson.security.GlobalSecurityConfiguration/help/disableRememberMe') {
f.checkbox(title:_("Disable remember me"), field: "disableRememberMe")
}
f.dropdownDescriptorSelector(title: _("Security Realm"), field: 'securityRealm', descriptors: h.filterDescriptors(app, SecurityRealm.all()))
f.dropdownDescriptorSelector(title: _("Authorization"), field: 'authorizationStrategy', descriptors: h.filterDescriptors(app, AuthorizationStrategy.all()))
}
f.section(title: _("Markup Formatter")) {
f.dropdownDescriptorSelector(title:_("Markup Formatter"), descriptors: MarkupFormatterDescriptor.all(), field: 'markupFormatter')
}
f.section(title: _("Markup Formatter")) {
f.dropdownDescriptorSelector(title:_("Markup Formatter"), descriptors: MarkupFormatterDescriptor.all(), field: 'markupFormatter')
}
f.section(title: _("Agents")) {
f.entry(title: _("TCP port for inbound agents"), field: "slaveAgentPort") {
if (my.slaveAgentPortEnforced) {
if (my.slaveAgentPort == -1) {
text(_("slaveAgentPortEnforcedDisabled"))
} else if (my.slaveAgentPort == 0) {
text(_("slaveAgentPortEnforcedRandom"))
} else {
text(_("slaveAgentPortEnforced", my.slaveAgentPort))
}
f.section(title: _("Agents")) {
f.entry(title: _("TCP port for inbound agents"), field: "slaveAgentPort") {
if (my.slaveAgentPortEnforced) {
if (my.slaveAgentPort == -1) {
text(_("slaveAgentPortEnforcedDisabled"))
} else if (my.slaveAgentPort == 0) {
text(_("slaveAgentPortEnforcedRandom"))
} else {
f.serverTcpPort()
text(_("slaveAgentPortEnforced", my.slaveAgentPort))
}
} else {
f.serverTcpPort()
}
}
}
Functions.getSortedDescriptorsForGlobalConfigByDescriptor(my.FILTER).each { Descriptor descriptor ->
set("descriptor", descriptor)
set("instance", descriptor)
f.rowSet(name:descriptor.jsonSafeClassName) {
st.include(from:descriptor, page:descriptor.globalConfigPage)
}
}
l.isAdmin() {
f.bottomButtonBar {
f.submit(value: _("Save"))
f.apply()
}
Functions.getSortedDescriptorsForGlobalConfigByDescriptor(my.FILTER).each { Descriptor descriptor ->
set("descriptor", descriptor)
set("instance", descriptor)
f.rowSet(name:descriptor.jsonSafeClassName) {
st.include(from:descriptor, page:descriptor.globalConfigPage)
}
}
l.isAdmin() {
st.adjunct(includes: "lib.form.confirm")
f.bottomButtonBar {
f.submit(value: _("Save"))
f.apply()
}
}
}
l.isAdmin() {
st.adjunct(includes: "lib.form.confirm")
}
}
@@ -23,9 +23,9 @@ THE SOFTWARE.
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:l="/lib/layout">
<l:layout permission="${app.ADMINISTER}" title="${%Users}" type="one-column">
<l:main-panel>
<j:jelly xmlns:j="jelly:core" xmlns:l="/lib/layout">
<j:set var="header">
<l:view>
<l:app-bar title="${%Users}" subtitle="${it.allUsers.size()}">
<l:isAdmin>
<a href="addUser" class="jenkins-button jenkins-button--primary">
@@ -35,53 +35,55 @@ THE SOFTWARE.
</l:isAdmin>
</l:app-bar>
<p class="jenkins-description">${%blurb}</p>
<p class="jenkins-page-description">${%blurb}</p>
</l:view>
</j:set>
<table class="jenkins-table sortable" id="people">
<thead>
<l:settings-subpage header="${header}" permission="${app.ADMINISTER}">
<table class="jenkins-table sortable" id="people">
<thead>
<tr>
<th class="jenkins-table__cell--tight" data-sort-disable="true" />
<th initialSortDir="down">${%User ID}</th>
<th>${%Name}</th>
<th class="jenkins-table__cell--tight" data-sort-disable="true" />
<th class="jenkins-table__cell--tight" data-sort-disable="true" />
</tr>
</thead>
<tbody>
<j:forEach var="user" items="${it.allUsers}">
<tr>
<th class="jenkins-table__cell--tight" data-sort-disable="true" />
<th initialSortDir="down">${%User ID}</th>
<th>${%Name}</th>
<th class="jenkins-table__cell--tight" data-sort-disable="true" />
<th class="jenkins-table__cell--tight" data-sort-disable="true" />
<td class="jenkins-table__cell--tight jenkins-table__icon">
<div class="jenkins-table__cell__button-wrapper">
<l:icon src="${h.getUserAvatar(user, '64x64')}" class="jenkins-avatar" />
</div>
</td>
<td>
<a href="${user.url}/" class="jenkins-table__link model-link inside">${user.id}</a>
</td>
<td>
${user.fullName}
</td>
<td>
<div class="jenkins-table__cell__button-wrapper">
<a href="${user.url}/" class="jenkins-button jenkins-button--tertiary">
<l:icon src="symbol-settings" />
</a>
</div>
</td>
<td>
<j:if test="${user.canDelete()}">
<div class="jenkins-table__cell__button-wrapper">
<l:confirmationLink href="${user.url}/doDelete" class="jenkins-button jenkins-button--tertiary jenkins-!-destructive-color"
post="true" message="${%delete.user(user.displayName)}">
<l:icon src="symbol-trash" />
</l:confirmationLink>
</div>
</j:if>
</td>
</tr>
</thead>
<tbody>
<j:forEach var="user" items="${it.allUsers}">
<tr>
<td class="jenkins-table__cell--tight jenkins-table__icon">
<div class="jenkins-table__cell__button-wrapper">
<l:icon src="${h.getUserAvatar(user, '64x64')}" class="jenkins-avatar" />
</div>
</td>
<td>
<a href="${user.url}/" class="jenkins-table__link model-link inside">${user.id}</a>
</td>
<td>
${user.fullName}
</td>
<td>
<div class="jenkins-table__cell__button-wrapper">
<a href="${user.url}/" class="jenkins-button jenkins-button--tertiary">
<l:icon src="symbol-settings" />
</a>
</div>
</td>
<td>
<j:if test="${user.canDelete()}">
<div class="jenkins-table__cell__button-wrapper">
<l:confirmationLink href="${user.url}/doDelete" class="jenkins-button jenkins-button--tertiary jenkins-!-destructive-color"
post="true" message="${%delete.user(user.displayName)}">
<l:icon src="symbol-trash" />
</l:confirmationLink>
</div>
</j:if>
</td>
</tr>
</j:forEach>
</tbody>
</table>
</l:main-panel>
</l:layout>
</j:forEach>
</tbody>
</table>
</l:settings-subpage>
</j:jelly>
@@ -27,124 +27,126 @@ THE SOFTWARE.
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:l="/lib/layout" xmlns:f="/lib/form">
<l:layout title="${it.displayName}" type="one-column">
<l:isAdmin>
<l:header>
<script src="${resURL}/jsbundles/pages/cloud-set.js" type="text/javascript" />
<link rel="stylesheet" href="${resURL}/jsbundles/pages/cloud-set.css" type="text/css" />
</l:header>
</l:isAdmin>
<l:main-panel>
<j:set var="readOnlyMode" value="${!app.hasPermission(app.ADMINISTER)}"/>
<j:choose>
<j:when test="${it.cloudAvailable and it.hasClouds()}">
<l:app-bar title="${%Clouds}">
<l:isAdmin>
<a class="jenkins-button jenkins-button--primary" href="new">
<l:icon src="symbol-add"/>
${%newCloud}
</a>
</l:isAdmin>
</l:app-bar>
<p class="description">${%description}</p>
<f:form method="post" name="config" action="reorder">
<table id="clouds" class="jenkins-table">
<thead>
<tr>
<th tooltip="${%Drag and drop cells to reorder}">${%Order}</th>
<th>${%Name}</th>
<th/>
</tr>
</thead>
<tbody class="with-drag-drop">
<j:forEach var="cloud" items="${it.clouds}">
<tr class="repeated-chunk" id="cloud_${cloud.name}">
<td data="${cloud.icon}" class="jenkins-table__cell--tight jenkins-table__icon">
<div class="jenkins-table__cell__button-wrapper dd-handle">
<l:icon src="${cloud.iconClassName}" tooltip="${cloud.iconAltText}"/>
</div>
</td>
<td>
<input type="hidden" name="name" value="${cloud.name}" />
<a href="${it.getCloudUrl(request2,app,cloud)}" class="jenkins-table__link model-link inside">${cloud.name}</a>
</td>
<td class="jenkins-table__cell--tight">
<div class="jenkins-table__cell__button-wrapper">
<a href="${it.getCloudUrl(request2,app,cloud)}configure" class="jenkins-button">
<l:icon src="symbol-settings"/>
</a>
</div>
</td>
</tr>
</j:forEach>
</tbody>
</table>
<l:isAdmin>
<f:bottomButtonBar>
<f:submit id="saveButton" value="${%Save}" clazz="jenkins-hidden" />
</f:bottomButtonBar>
</l:isAdmin>
</f:form>
<st:adjunct includes="lib.form.confirm" />
</j:when>
<j:otherwise>
<div class="empty-state-block">
<l:app-bar title="${%Clouds}"/>
<j:set var="previousIt" value="${it}" />
<j:new var="it" className="jenkins.agents.CloudsLink" />
<section class="empty-state-section">
<j:choose>
<j:when test="${it.cloudAvailable}">
<j:choose>
<j:when test="${h.hasPermission(app.ADMINISTER)}">
<p>${%noCloudAvailableCTA}</p>
</j:when>
<j:otherwise>
<p>${%noCloudAvailable}</p>
</j:otherwise>
</j:choose>
</j:when>
<j:otherwise>
<p>${%noCloudPlugin}</p>
</j:otherwise>
</j:choose>
<ul class="empty-state-section-list">
<l:isAdmin>
<j:if test="${it.cloudAvailable}">
<li class="content-block">
<a href="new"
class="content-block__link">
<span>${%newCloud}</span>
<span class="trailing-icon">
<l:icon src="symbol-add"/>
</span>
<l:settings-subpage header="${null}">
<j:set var="it" value="${previousIt}" />
<l:isAdmin>
<script src="${resURL}/jsbundles/pages/cloud-set.js" type="text/javascript" />
<link rel="stylesheet" href="${resURL}/jsbundles/pages/cloud-set.css" type="text/css" />
</l:isAdmin>
<j:set var="readOnlyMode" value="${!app.hasPermission(app.ADMINISTER)}"/>
<j:choose>
<j:when test="${it.cloudAvailable and it.hasClouds()}">
<l:app-bar title="${%Clouds}">
<l:isAdmin>
<a class="jenkins-button jenkins-button--primary" href="new">
<l:icon src="symbol-add"/>
${%newCloud}
</a>
</l:isAdmin>
</l:app-bar>
<p class="description">${%description}</p>
<f:form method="post" name="config" action="reorder">
<table id="clouds" class="jenkins-table">
<thead>
<tr>
<th tooltip="${%Drag and drop cells to reorder}">${%Order}</th>
<th>${%Name}</th>
<th/>
</tr>
</thead>
<tbody class="with-drag-drop">
<j:forEach var="cloud" items="${it.clouds}">
<tr class="repeated-chunk" id="cloud_${cloud.name}">
<td data="${cloud.icon}" class="jenkins-table__cell--tight jenkins-table__icon">
<div class="jenkins-table__cell__button-wrapper dd-handle">
<l:icon src="${cloud.iconClassName}" tooltip="${cloud.iconAltText}"/>
</div>
</td>
<td>
<input type="hidden" name="name" value="${cloud.name}" />
<a href="${it.getCloudUrl(request2,app,cloud)}" class="jenkins-table__link model-link inside">${cloud.name}</a>
</td>
<td class="jenkins-table__cell--tight">
<div class="jenkins-table__cell__button-wrapper">
<a href="${it.getCloudUrl(request2,app,cloud)}configure" class="jenkins-button">
<l:icon src="symbol-settings"/>
</a>
</li>
</j:if>
</div>
</td>
</tr>
</j:forEach>
</tbody>
</table>
<l:isAdmin>
<f:bottomButtonBar>
<f:submit id="saveButton" value="${%Save}" clazz="jenkins-hidden" />
</f:bottomButtonBar>
</l:isAdmin>
</f:form>
<st:adjunct includes="lib.form.confirm" />
</j:when>
<j:otherwise>
<div class="empty-state-block">
<l:app-bar title="${%Clouds}"/>
<section class="empty-state-section">
<j:choose>
<j:when test="${it.cloudAvailable}">
<j:choose>
<j:when test="${h.hasPermission(app.ADMINISTER)}">
<p>${%noCloudAvailableCTA}</p>
</j:when>
<j:otherwise>
<p>${%noCloudAvailable}</p>
</j:otherwise>
</j:choose>
</j:when>
<j:otherwise>
<p>${%noCloudPlugin}</p>
</j:otherwise>
</j:choose>
<ul class="empty-state-section-list">
<l:isAdmin>
<j:if test="${it.cloudAvailable}">
<li class="content-block">
<a href="${rootURL}/manage/pluginManager/available?filter=${it.cloudUpdateCenterCategoryLabel}"
<a href="new"
class="content-block__link">
<span>${%installCloudPlugin}</span>
<span>${%newCloud}</span>
<span class="trailing-icon">
<l:icon src="symbol-plugins" />
<l:icon src="symbol-add"/>
</span>
</a>
</li>
</l:isAdmin>
</j:if>
<li class="content-block">
<a href="https://www.jenkins.io/redirect/distributed-builds"
class="content-block__link"
target="_blank" rel="noopener noreferrer">
<span>${%learnMoreDistributedBuilds}</span>
<a href="${rootURL}/manage/pluginManager/available?filter=${it.cloudUpdateCenterCategoryLabel}"
class="content-block__link">
<span>${%installCloudPlugin}</span>
<span class="trailing-icon">
<l:icon src="symbol-help-circle" />
<l:icon src="symbol-plugins" />
</span>
</a>
</li>
</ul>
</section>
</div>
</j:otherwise>
</j:choose>
</l:main-panel>
</l:layout>
</l:isAdmin>
<li class="content-block">
<a href="https://www.jenkins.io/redirect/distributed-builds"
class="content-block__link"
target="_blank" rel="noopener noreferrer">
<span>${%learnMoreDistributedBuilds}</span>
<span class="trailing-icon">
<l:icon src="symbol-help-circle" />
</span>
</a>
</li>
</ul>
</section>
</div>
</j:otherwise>
</j:choose>
</l:settings-subpage>
</j:jelly>
@@ -24,13 +24,12 @@ THE SOFTWARE.
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:l="/lib/layout" xmlns:f="/lib/form">
<l:layout title="${%Appearance}" permissions="${app.MANAGE_AND_SYSTEM_READ}" type="one-column">
<j:set var="readOnlyMode" value="${!app.hasPermission(app.MANAGE)}"/>
<j:set var="pluginsUrl" value="${rootURL}/manage/pluginManager/available?filter=UI Themes" />
<j:set var="hasPlugins" value="${it.hasPlugins()}" />
<j:set var="header">
<l:view>
<j:set var="pluginsUrl" value="${rootURL}/manage/pluginManager/available?filter=UI Themes" />
<j:set var="hasPlugins" value="${it.hasPlugins()}" />
<l:main-panel>
<l:app-bar title="${%Appearance}">
<l:app-bar title="${it.displayName}">
<j:if test="${hasPlugins}">
<a href="${pluginsUrl}" class="jenkins-button">
<l:icon src="symbol-plugins" />
@@ -39,36 +38,46 @@ THE SOFTWARE.
</j:if>
</l:app-bar>
<j:choose>
<j:when test="${!hasPlugins}">
<l:notice title="${%noPlugins}" icon="symbol-brush-outline">
<a href="${pluginsUrl}">
${%findPlugins}
</a>
</l:notice>
</j:when>
<j:otherwise>
<l:skeleton />
<div class="jenkins-page-description">
${it.description}
</div>
</l:view>
</j:set>
<f:form method="post" name="config" action="configure">
<j:set var="instance" value="${it}" />
<j:set var="descriptor" value="${instance.descriptor}" />
<l:settings-subpage header="${header}" permissions="${app.MANAGE_AND_SYSTEM_READ}">
<j:set var="readOnlyMode" value="${!app.hasPermission(app.MANAGE)}"/>
<j:set var="pluginsUrl" value="${rootURL}/manage/pluginManager/available?filter=UI Themes" />
<j:set var="hasPlugins" value="${it.hasPlugins()}" />
<j:forEach var="descriptor" items="${h.getSortedDescriptorsForGlobalConfigByDescriptor(it.FILTER)}">
<j:set var="instance" value="${descriptor}" />
<f:rowSet name="${descriptor.jsonSafeClassName}">
<st:include page="${descriptor.globalConfigPage}" from="${descriptor}" />
</f:rowSet>
</j:forEach>
<j:choose>
<j:when test="${!hasPlugins}">
<l:notice title="${%noPlugins}" icon="symbol-brush-outline">
<a href="${pluginsUrl}">
${%findPlugins}
</a>
</l:notice>
</j:when>
<j:otherwise>
<l:skeleton />
<f:saveApplyBar/>
</f:form>
<f:form method="post" name="config" action="configure">
<j:set var="instance" value="${it}" />
<j:set var="descriptor" value="${instance.descriptor}" />
<l:hasAdministerOrManage>
<st:adjunct includes="lib.form.confirm" />
</l:hasAdministerOrManage>
</j:otherwise>
</j:choose>
</l:main-panel>
</l:layout>
<j:forEach var="descriptor" items="${h.getSortedDescriptorsForGlobalConfigByDescriptor(it.FILTER)}">
<j:set var="instance" value="${descriptor}" />
<f:rowSet name="${descriptor.jsonSafeClassName}">
<st:include page="${descriptor.globalConfigPage}" from="${descriptor}" />
</f:rowSet>
</j:forEach>
<f:saveApplyBar/>
</f:form>
<l:hasAdministerOrManage>
<st:adjunct includes="lib.form.confirm" />
</l:hasAdministerOrManage>
</j:otherwise>
</j:choose>
</l:settings-subpage>
</j:jelly>
@@ -6,35 +6,25 @@ def f = namespace(lib.FormTagLib)
def l = namespace(lib.LayoutTagLib)
def st = namespace("jelly:stapler")
l.layout(permission: app.MANAGE, title: my.displayName, type: "one-column") {
l.main_panel {
h1 {
text(Messages.ShutdownLink_DisplayName_prepare())
l.'settings-subpage'(permission: app.MANAGE) {
f.form(method: "post", name: "prepareShutdown", action: "prepare") {
f.entry(title: Messages.ShutdownLink_ShutDownReason_title()) {
f.textbox(name: "parameter.shutdownReason", value: app.quietDownReason ?: null)
}
p {
text(my.description)
f.bottomButtonBar {
f.submit(value: _(app.isQuietingDown()
? Messages.ShutdownLink_ShutDownReason_update()
: Messages.ShutdownLink_DisplayName_prepare()),
clazz: "jenkins-!-destructive-color")
}
}
f.form(method: "post", name: "prepareShutdown", action: "prepare") {
f.entry(title: Messages.ShutdownLink_ShutDownReason_title()) {
f.textbox(name: "parameter.shutdownReason", value: app.quietDownReason ?: null)
}
if (app.isQuietingDown()) {
f.form(method: "post", name: "cancelShutdown", action: "cancel") {
f.bottomButtonBar {
f.submit(value: _(app.isQuietingDown()
? Messages.ShutdownLink_ShutDownReason_update()
: Messages.ShutdownLink_DisplayName_prepare()),
clazz: "jenkins-!-destructive-color")
}
}
if (app.isQuietingDown()) {
f.form(method: "post", name: "cancelShutdown", action: "cancel") {
f.bottomButtonBar {
f.submit(value: _(Messages.ShutdownLink_DisplayName_cancel()))
}
f.submit(value: _(Messages.ShutdownLink_DisplayName_cancel()))
}
}
}
@@ -26,30 +26,40 @@ THE SOFTWARE.
Config page
-->
<?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">
<l:layout permissions="${app.MANAGE_AND_SYSTEM_READ}" title="${%System}" type="one-column">
<st:include page="sidepanel.jelly" />
<f:breadcrumb-config-outline title="${%System}" />
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:l="/lib/layout" xmlns:f="/lib/form">
<j:new className="jenkins.management.ConfigureLink" var="it" />
<l:main-panel>
<l:settings-subpage permissions="${app.MANAGE_AND_SYSTEM_READ}" includeBreadcrumb="true">
<j:set var="managementLink" value="${it}" />
<j:set var="it" value="${app}" />
<j:set var="readOnlyMode" value="${!h.hasPermission(app.MANAGE)}"/>
<l:app-bar title="${%System}" />
<l:skeleton />
<l:userExperimentalFlag var="newManageJenkins" flagClassName="jenkins.model.experimentalflags.NewManageJenkinsUserExperimentalFlag" />
<j:if test="${newManageJenkins}">
<section class="manage-messages">
<j:forEach var="am" items="${app.activeAdministrativeMonitors}">
<st:include page="message.jelly" it="${am}" />
</j:forEach>
<st:include page="downgrade.jelly" it="${app}" />
</section>
</j:if>
<f:form method="post" name="config" action="configSubmit" class="jenkins-form">
<j:set var="instance" value="${it}" />
<j:set var="descriptor" value="${instance.descriptor}" />
<f:entry title="${%Home directory}" description="${%By default, Jenkins stores all of its data in this directory on the file system}" help="/help/system-config/homeDirectory.html">
<div class="jenkins-quote jenkins-quote--monospace">
${it.rootDir}
<l:copyButton tooltip="${%Copy home directory}" text="${it.rootDir}" iconOnly="true" />
</div>
</f:entry>
<f:entry title="${%System Message}" description="${%This message will be displayed at the top of the Jenkins main page. This can be useful for posting notifications to your users}">
<f:textarea name="system_message" value="${it.systemMessage}" disabled="${readOnlyMode?'true':null}" readonly="${readOnlyMode?'true':null}"
codemirror-mode="${app.markupFormatter.codeMirrorMode}" codemirror-config="${app.markupFormatter.codeMirrorConfig}" previewEndpoint="/markupFormatter/previewDescription"/>
</f:entry>
<f:section title="General">
<f:entry title="${%Home directory}" description="${%By default, Jenkins stores all of its data in this directory on the file system}" help="/help/system-config/homeDirectory.html">
<div class="jenkins-quote jenkins-quote--monospace">
${it.rootDir}
<l:copyButton tooltip="${%Copy home directory}" text="${it.rootDir}" iconOnly="true" />
</div>
</f:entry>
<f:entry title="${%System Message}" description="${%This message will be displayed at the top of the Jenkins main page. This can be useful for posting notifications to your users}">
<f:textarea name="system_message" value="${it.systemMessage}" disabled="${readOnlyMode?'true':null}" readonly="${readOnlyMode?'true':null}"
codemirror-mode="${app.markupFormatter.codeMirrorMode}" codemirror-config="${app.markupFormatter.codeMirrorConfig}" previewEndpoint="/markupFormatter/previewDescription"/>
</f:entry>
</f:section>
<!-- TODO remove after January 2026 LTS line -->
<div class="jenkins-alert jenkins-alert-info">
@@ -69,6 +79,5 @@ THE SOFTWARE.
<j:set var="readOnlyMode" value="${!h.hasPermission(app.MANAGE)}"/>
<f:saveApplyBar/>
</f:form>
</l:main-panel>
</l:layout>
</l:settings-subpage>
</j:jelly>
@@ -24,11 +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" xmlns:i="jelly:fmt">
<l:layout title="${it.displayName} ${%Load Statistics}" type="one-column">
<l:breadcrumb title="${%Load Statistics}"/>
<l:main-panel>
<j:set var="prefix" value="overallLoad" />
<st:include page="main.jelly" from="${it.overallLoad}" />
</l:main-panel>
</l:layout>
<j:new className="jenkins.management.StatisticsLink" var="it" />
<l:settings-subpage includeBreadcrumb="true">
<j:set var="it" value="${app}" />
<j:set var="prefix" value="overallLoad" />
<st:include page="main.jelly" from="${it.overallLoad}" />
</l:settings-subpage>
</j:jelly>
@@ -27,85 +27,79 @@ 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">
<l:layout permissions="${app.MANAGE_AND_SYSTEM_READ}" title="${%System Information}" type="one-column">
<l:header>
<script src="${resURL}/jsbundles/section-to-tabs.js" type="text/javascript" defer="true" />
<script src="${resURL}/jsbundles/pages/manage-jenkins/system-information.js" type="text/javascript" defer="true" />
</l:header>
<l:breadcrumb title="${%System Information}"/>
<l:main-panel>
<l:app-bar title="${%System Information}" />
<j:new className="jenkins.management.SystemInfoLink" var="it" />
<l:hasPermission permission="${app.SYSTEM_READ}">
<l:tabPane title="${%System Properties}">
<t:propertyTable items="${h.systemProperties}" sensitive="true" />
</l:tabPane>
</l:hasPermission>
<l:settings-subpage permissions="${app.MANAGE_AND_SYSTEM_READ}" includeBreadcrumb="true">
<script src="${resURL}/jsbundles/section-to-tabs.js" type="text/javascript" defer="true" />
<script src="${resURL}/jsbundles/pages/manage-jenkins/system-information.js" type="text/javascript" defer="true" />
<l:hasPermission permission="${app.SYSTEM_READ}">
<l:tabPane title="${%Environment Variables}">
<t:propertyTable items="${h.envVars}" sensitive="true" />
</l:tabPane>
</l:hasPermission>
<l:hasPermission permission="${app.SYSTEM_READ}">
<l:tabPane title="${%System Properties}">
<t:propertyTable items="${h.systemProperties}" sensitive="true" />
</l:tabPane>
</l:hasPermission>
<l:tabPane title="${%Plugins}">
<l:hasPermission permission="${app.SYSTEM_READ}">
<l:tabPane title="${%Environment Variables}">
<t:propertyTable items="${h.envVars}" sensitive="true" />
</l:tabPane>
</l:hasPermission>
<j:choose>
<j:when test="${empty(app.pluginManager.plugins) and empty(app.pluginManager.failedPlugins)}">
<l:notice icon="symbol-plugins" title="${%No plugins installed}" />
</j:when>
<j:otherwise>
<table class="jenkins-table sortable">
<thead>
<l:tabPane title="${%Plugins}">
<j:choose>
<j:when test="${empty(app.pluginManager.plugins) and empty(app.pluginManager.failedPlugins)}">
<l:notice icon="symbol-plugins" title="${%No plugins installed}" />
</j:when>
<j:otherwise>
<table class="jenkins-table sortable">
<thead>
<tr>
<th initialSortDir="down">${%Name}</th>
<th>${%Version}</th>
<th>${%Enabled}</th>
</tr>
</thead>
<tbody>
<j:forEach var="p" items="${app.pluginManager.plugins}">
<j:set var="state" value="${p.enabled?'true':'false'}"/>
<tr>
<th initialSortDir="down">${%Name}</th>
<th>${%Version}</th>
<th>${%Enabled}</th>
<td>
<a class="jenkins-table__link" href="${p.url}" target="_blank" rel="noopener noreferrer">
<st:out value="${p.displayName}"/>
</a>
</td>
<td>
<st:out value="${p.version}"/>
</td>
<td>
<st:out value="${state}"/>
</td>
</tr>
</thead>
<tbody>
<j:forEach var="p" items="${app.pluginManager.plugins}">
<j:set var="state" value="${p.enabled?'true':'false'}"/>
<tr>
<td>
<a class="jenkins-table__link" href="${p.url}" target="_blank" rel="noopener noreferrer">
<st:out value="${p.displayName}"/>
</a>
</td>
<td>
<st:out value="${p.version}"/>
</td>
<td>
<st:out value="${state}"/>
</td>
</tr>
</j:forEach>
</tbody>
</table>
</j:otherwise>
</j:choose>
</j:forEach>
</tbody>
</table>
</j:otherwise>
</j:choose>
</l:tabPane>
<l:tabPane title="${%Memory Usage}">
<f:entry title="${%Timespan}" class="jenkins-form-item--small">
<div class="jenkins-select">
<select id="timespan-select" class="jenkins-select__input">
<option value="sec10">${%Short}</option>
<option value="min" selected="true">${%Medium}</option>
<option value="hour">${%Long}</option>
</select>
</div>
</f:entry>
<div id="graph-host" />
</l:tabPane>
<l:isAdmin>
<l:tabPane title="${%Thread Dumps}">
<p>${%threadDump_blurb('threadDump')}</p>
</l:tabPane>
<l:tabPane title="${%Memory Usage}">
<f:entry title="${%Timespan}" class="jenkins-form-item--small">
<div class="jenkins-select">
<select id="timespan-select" class="jenkins-select__input">
<option value="sec10">${%Short}</option>
<option value="min" selected="true">${%Medium}</option>
<option value="hour">${%Long}</option>
</select>
</div>
</f:entry>
<div id="graph-host" />
</l:tabPane>
<l:isAdmin>
<l:tabPane title="${%Thread Dumps}">
<p>${%threadDump_blurb('threadDump')}</p>
</l:tabPane>
</l:isAdmin>
</l:main-panel>
</l:layout>
</l:isAdmin>
</l:settings-subpage>
</j:jelly>
@@ -7,32 +7,29 @@ def f=namespace(lib.FormTagLib)
def l=namespace(lib.LayoutTagLib)
def st=namespace("jelly:stapler")
l.layout(permission:app.SYSTEM_READ, title:my.displayName, type:"one-column") {
l.'settings-subpage'(permission: app.SYSTEM_READ) {
set("readOnlyMode", !app.hasPermission(app.ADMINISTER))
l.main_panel {
l.app_bar(title: my.displayName)
l.skeleton()
l.skeleton()
f.form(method:"post",name:"config",action:"configure", class: "jenkins-form") {
Functions.getSortedDescriptorsForGlobalConfigByDescriptor(my.FILTER).each { Descriptor descriptor ->
set("descriptor",descriptor)
set("instance",descriptor)
f.rowSet(name:descriptor.jsonSafeClassName) {
st.include(from:descriptor, page:descriptor.globalConfigPage)
}
}
l.isAdmin() {
f.bottomButtonBar {
f.submit(value: _("Save"))
f.apply(value: _("Apply"))
}
f.form(method:"post",name:"config",action:"configure", class: "jenkins-form") {
Functions.getSortedDescriptorsForGlobalConfigByDescriptor(my.FILTER).each { Descriptor descriptor ->
set("descriptor",descriptor)
set("instance",descriptor)
f.rowSet(name:descriptor.jsonSafeClassName) {
st.include(from:descriptor, page:descriptor.globalConfigPage)
}
}
l.isAdmin() {
st.adjunct(includes: "lib.form.confirm")
f.bottomButtonBar {
f.submit(value: _("Save"))
f.apply(value: _("Apply"))
}
}
}
l.isAdmin() {
st.adjunct(includes: "lib.form.confirm")
}
}
@@ -40,23 +40,21 @@ THE SOFTWARE.
Generates a row containing the page title and an optional set of controls
</st:documentation>
<j:if test="${mode=='main-panel' or mode=='side-panel'}">
<div class="jenkins-app-bar ${attrs.icon != null ? 'jenkins-app-bar--with-icon' :''}">
<div class="jenkins-app-bar__content">
<j:if test="${attrs.icon != null}">
<l:icon src="${attrs.icon}" class="jenkins-avatar" />
</j:if>
<div class="jenkins-app-bar ${attrs.icon != null ? 'jenkins-app-bar--with-icon' :''}">
<div class="jenkins-app-bar__content">
<j:if test="${attrs.icon != null}">
<l:icon src="${attrs.icon}" class="jenkins-avatar" />
</j:if>
<x:element name="${headingLevel ?: 'h1'}">
${title}
<j:if test="${attrs.subtitle != null}">
<span class="jenkins-app-bar__subtitle">${attrs.subtitle}</span>
</j:if>
</x:element>
</div>
<div class="jenkins-app-bar__controls">
<d:invokeBody/>
</div>
<x:element name="${headingLevel ?: 'h1'}">
${title}
<j:if test="${attrs.subtitle != null}">
<span class="jenkins-app-bar__subtitle">${attrs.subtitle}</span>
</j:if>
</x:element>
</div>
</j:if>
<div class="jenkins-app-bar__controls">
<d:invokeBody/>
</div>
</div>
</j:jelly>
@@ -0,0 +1,33 @@
<!--
The MIT License
Copyright (c) 2025 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:d="jelly:define">
<st:documentation>
A lazy-loaded element.
</st:documentation>
<j:if test="${deferMode == 'children'}">
<d:invokeBody />
</j:if>
</j:jelly>
@@ -0,0 +1,43 @@
<!--
The MIT License
Copyright (c) 2025 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:d="jelly:define" xmlns:l="/lib/layout">
<st:documentation>
A lazy-loaded element.
</st:documentation>
<j:set var="deferMode" value="placeholder" scope="parent" />
<div class="jenkins-!-display-contents">
<d:invokeBody />
</div>
<j:set var="deferMode" value="children" scope="parent" />
<l:renderOnDemand clazz="defer-element" tag="div" capture="it,deferMode,previousIt">
<l:ajax>
<div>
<d:invokeBody />
</div>
</l:ajax>
</l:renderOnDemand>
</j:jelly>
@@ -0,0 +1,33 @@
<!--
The MIT License
Copyright (c) 2025 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:d="jelly:define">
<st:documentation>
A lazy-loaded element.
</st:documentation>
<j:if test="${deferMode == 'placeholder'}">
<d:invokeBody />
</j:if>
</j:jelly>
@@ -0,0 +1,144 @@
<!--
The MIT License
Copyright (c) 2025, 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:l="/lib/layout" xmlns:d="jelly:define" xmlns:st="jelly:stapler" xmlns:f="/lib/form">
<st:documentation>
A reusable container for subpages relating under Manage Jenkins.
<st:attribute name="permission">
If given, this page is only made available to users who have the specified permission.
(The permission will be checked against the "it" object.)
</st:attribute>
<st:attribute name="permissions">
If given, this page is made available to users who have one or more of the specified permissions.
(The permissions will be checked against the "it" object.)
</st:attribute>
<st:attribute name="placeholder">
Optional placeholder for loading this page, falls back to a generic skeleton if not set.
</st:attribute>
<st:attribute name="header">
Optional header for the page, falls back to a default header if not set.
</st:attribute>
<st:attribute name="includeBreadcrumb">
<![CDATA[
Optional breadcrumb, will add a breadcrumb for the current <code>it</code>
]]>
</st:attribute>
</st:documentation>
<j:new className="hudson.model.ManageJenkinsAction" var="manageJenkinsAction" />
<l:userExperimentalFlag var="newManageJenkins" flagClassName="jenkins.model.experimentalflags.NewManageJenkinsUserExperimentalFlag" />
<l:layout title="${it.displayName} - ${manageJenkinsAction.displayName}"
permission="${attrs.permission}"
permissions="${attrs.permissions}"
type="${newManageJenkins ? 'two-column' : 'one-column'}">
<j:if test="${attrs.includeBreadcrumb}">
<!-- Hacky - this will be improved in subsequent PRs -->
<j:choose>
<j:when test="${it.class.name eq 'jenkins.management.ConfigureLink'}">
<f:breadcrumb-config-outline title="${it.displayName}" />
</j:when>
<j:otherwise>
<l:breadcrumb title="${it.displayName}" />
</j:otherwise>
</j:choose>
</j:if>
<j:choose>
<j:when test="${newManageJenkins}">
<l:side-panel>
<l:app-bar title="${manageJenkinsAction.displayName}" />
<l:tasks>
<l:search-bar placeholder="${%Search}" id="settings-search-bar" />
<script src="${resURL}/jsbundles/pages/manage-jenkins.js" type="text/javascript" />
<j:invokeStatic var="j" className="jenkins.model.Jenkins" method="get" />
<j:forEach var="category" items="${j.categorizedManagementLinks.entrySet()}">
<summary class="jenkins-side-nav__heading">
${category.key.label}
</summary>
<j:forEach var="item" items="${category.value}">
<l:task title="${item.displayName}"
href="${rootURL}/manage/${item.urlName}"
icon="${item.iconFileName}"
badge="${item.badge}"
requiresConfirmation="${item.requiresConfirmation}"
post="${item.requiresPOST}"
confirmationMessage="${%sure}" />
</j:forEach>
</j:forEach>
</l:tasks>
</l:side-panel>
<l:main-panel>
<div class="app-settings-container">
<div class="app-settings-container__inner">
<j:if test="${!attrs.containsKey('header')}">
<l:app-bar title="${it.displayName}" />
<div class="jenkins-page-description">
${it.description}
</div>
</j:if>
<j:out value="${attrs.header}" />
<l:defer>
<l:defer.placeholder>
<j:choose>
<j:when test="${attrs.containsKey('placeholder')}">
<j:out value="${attrs.placeholder}" />
</j:when>
<j:otherwise>
<l:skeleton type="form" />
</j:otherwise>
</j:choose>
</l:defer.placeholder>
<l:defer.children>
<d:invokeBody />
</l:defer.children>
</l:defer>
</div>
</div>
</l:main-panel>
</j:when>
<j:otherwise>
<l:main-panel>
<j:if test="${!attrs.containsKey('header')}">
<l:app-bar title="${it.displayName}" />
<div class="jenkins-page-description">
${it.description}
</div>
</j:if>
<j:out value="${attrs.header}" />
<d:invokeBody />
</l:main-panel>
</j:otherwise>
</j:choose>
</l:layout>
</j:jelly>
@@ -43,6 +43,30 @@ THE SOFTWARE.
<div />
</div>
</j:case>
<j:case value="form">
<div class="jenkins-form-skeleton-2">
<span />
<div />
<span />
<div />
<span />
<div />
<span />
<div />
<span />
<div />
<span />
<div />
<span />
<div />
<span />
<div />
<span />
<div />
<span />
<div />
</div>
</j:case>
<j:default>
<div class="jenkins-form-skeleton">
<span />
+2 -1
View File
@@ -1,5 +1,6 @@
.jenkins-side-panel-skeleton,
.jenkins-form-skeleton {
.jenkins-form-skeleton,
.jenkins-form-skeleton-2 {
position: relative;
display: flex;
flex-direction: column;
+2
View File
@@ -6,9 +6,11 @@ import Tooltips from "@/components/tooltips";
import StopButtonLink from "@/components/stop-button-link";
import ConfirmationLink from "@/components/confirmation-link";
import Dialogs from "@/components/dialogs";
import Defer from "@/components/defer";
Dropdowns.init();
CommandPalette.init();
Defer.init();
Notifications.init();
SearchBar.init();
Tooltips.init();
+18
View File
@@ -0,0 +1,18 @@
import behaviorShim from "@/util/behavior-shim";
function init() {
behaviorShim.specify(".defer-element", "-defer-", 1000, (element) => {
const parent = element.parentElement;
const placeholder = element.previousElementSibling;
renderOnDemand(element, () => {
placeholder.remove();
// Emit DOMContentLoaded in case it's tracked in a script
const evt = new Event("DOMContentLoaded", { bubbles: true });
parent.dispatchEvent(evt);
});
});
}
export default { init };
@@ -1,4 +1,5 @@
import { toId } from "@/util/dom";
import BehaviorShim from "@/util/behavior-shim";
/*
* Generates a jump list for the active breadcrumb to jump to
@@ -8,13 +9,15 @@ function init() {
const inpageNavigationBreadcrumb = document.querySelector("#inpage-nav div");
if (inpageNavigationBreadcrumb) {
inpageNavigationBreadcrumb.items = Array.from(
document.querySelectorAll(
"form > div > .jenkins-section > .jenkins-section__title",
),
).map((section) => {
section.id = toId(section.textContent);
return { label: section.textContent, url: "#" + section.id };
BehaviorShim.specify("form", "inpage-nav", 999, (element) => {
inpageNavigationBreadcrumb.items = Array.from(
element.querySelectorAll(
"& > div > .jenkins-section > .jenkins-section__title",
),
).map((section) => {
section.id = toId(section.textContent);
return { label: section.textContent, url: "#" + section.id };
});
});
}
}
+11 -3
View File
@@ -1,13 +1,21 @@
const searchBarInput = document.querySelector("#settings-search-bar");
searchBarInput.suggestions = function () {
return Array.from(document.querySelectorAll(".jenkins-section__item"))
return Array.from(
document.querySelectorAll(
".jenkins-section__item, #tasks .task-link-wrapper",
),
)
.map((item) => ({
url: item.querySelector("a").href,
icon: item.querySelector(
".jenkins-section__item__icon svg, .jenkins-section__item__icon img",
".jenkins-section__item__icon svg, .jenkins-section__item__icon img, .task-icon-link svg, .task-icon-link img",
).outerHTML,
label: item.querySelector("dt").textContent,
label: (
item.querySelector("dt") ||
item.querySelector(".task-link-text") ||
item.querySelector(".task-link")
).textContent,
}))
.filter((item) => !item.url.endsWith("#"));
};
+1 -2
View File
@@ -1,6 +1,5 @@
// Converts a page's section headings into clickable tabs, see 'About Jenkins' page for example
const tabPanes = document.querySelectorAll(".jenkins-tab-pane");
const content = document.querySelector("#main-panel");
// Hide tab panes
tabPanes.forEach((tabPane) => {
@@ -47,4 +46,4 @@ tabPanes.forEach((tabPane, index) => {
tabBar.append(tab);
});
content.insertBefore(tabBar, tabPanes[0]);
tabPanes[0].parentElement.insertBefore(tabBar, tabPanes[0]);
@@ -94,10 +94,6 @@ $background-outset: 0.5rem;
font-weight: var(--font-bold-weight);
cursor: default;
svg * {
stroke-width: 35px;
}
&::before {
background-color: var(--item-background--active) !important;
}
@@ -114,3 +110,14 @@ $background-outset: 0.5rem;
}
}
}
.jenkins-side-nav__heading {
font-size: var(--font-size-sm);
color: var(--text-color-secondary);
font-weight: var(--font-bold-weight);
margin: 1rem 0 0.375rem;
&:first-of-type {
margin-top: 0.375rem;
}
}
+5 -1
View File
@@ -5,10 +5,14 @@
}
.jenkins-page-description {
font-size: 0.9375rem;
color: var(--text-color-secondary);
margin: 0 0 var(--section-padding) 0;
line-height: 1.66;
text-wrap: pretty;
}
.jenkins-app-bar + .jenkins-page-description {
margin-top: -0.5rem;
}
.jenkins-fieldset {
+1 -6
View File
@@ -127,13 +127,8 @@
}
.app-about-version {
font-size: var(--font-size-xs);
font-weight: var(--font-bold-weight);
color: var(--text-color-secondary);
margin: 0;
}
.app-about-paragraph {
font-size: 1rem;
margin-bottom: var(--section-padding);
font-weight: var(--font-bold-weight);
}
+30 -1
View File
@@ -138,7 +138,8 @@
}
}
body:has(.app-build-bar) {
body:has(.app-build-bar),
body:has(.app-settings-container) {
--top: color-mix(in sRGB, var(--text-color-secondary) 4%, var(--background));
--bottom: color-mix(
in sRGB,
@@ -150,6 +151,34 @@ body:has(.app-build-bar) {
background-attachment: fixed;
}
.app-settings-container {
--background: var(--card-background);
--top: color-mix(
in sRGB,
var(--text-color-secondary) 2%,
var(--card-background)
);
position: relative;
background-color: var(--card-background);
background-image: linear-gradient(var(--top), var(--card-background));
background-size: 100% 90vh;
background-repeat: no-repeat;
border: var(--jenkins-border);
border-radius: var(--form-input-border-radius);
z-index: 0;
min-height: calc(100vh - var(--header-height) - var(--section-padding));
&__inner {
max-width: 900px;
margin: auto;
overflow-x: clip;
padding: var(--section-padding);
display: flex;
flex-direction: column;
}
}
.app-build-bar {
position: relative;
display: grid;