Standardise experimental Jenkins pages, make the side panel independently scrollable + make the build bar sticky (#27380)

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Mark Waite <mark.earl.waite@gmail.com>
This commit is contained in:
authored and GitHub committed 2026-10-04 16:24:57 +01:00
1 parent d42ab94dcb
commit 47aca2ba97
24 files changed
+417 -435

No files matched your search

@@ -27,7 +27,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" xmlns:i="jelly:fmt">
<l:side-panel sticky="true">
<l:side-panel>
<l:app-bar title="${%Plugins}" />
<l:tasks>
<l:task href="${rootURL}/manage/pluginManager/" icon="symbol-download" title="${%Updates}" badge="${app.updateCenter.badge}"/>
@@ -36,14 +36,14 @@ THE SOFTWARE.
<l:breadcrumb title="${readOnlyMode ? '%Configuration' : '%Configure'}" />
<l:side-panel sticky="true">
<l:side-panel>
<l:app-bar title="${readOnlyMode ? '%Configuration' : '%Configure'}" />
<div id="tasks">
<l:skeleton type="side-panel" />
</div>
</l:side-panel>
<l:main-panel>
<l:main-panel width="narrow">
<f:form method="post" class="jenkins-form" action="configSubmit" name="config" tableClass="config-table">
<l:app-bar title="${%General}" headingLevel="h2">
<p:config-disableBuild />
@@ -0,0 +1,52 @@
<!--
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:l="/lib/layout" xmlns:dd="/lib/layout/dropdowns">
<footer class="page-footer">
<div class="page-footer__flex-row">
<div class="page-footer__footer-id-placeholder" id="footer"></div>
<div class="page-footer__links">
<l:overflowButton text="Jenkins ${h.version}"
clazz="jenkins-button--tertiary jenkins_ver"
icon="${null}"
tooltip="${null}">
<l:hasPermission permission="${app.SYSTEM_READ}">
<dd:item icon="symbol-jenkins"
text="${%About Jenkins}"
href="${rootURL}/manage/about" />
<dd:separator />
</l:hasPermission>
<dd:item icon="symbol-heart"
text="${%Get involved}"
href="https://www.jenkins.io/participate/" />
<dd:item icon="symbol-external"
text="${%Website}"
href="${h.getFooterURL()}" />
</l:overflowButton>
</div>
</div>
</footer>
</j:jelly>
@@ -48,6 +48,8 @@ THE SOFTWARE.
<j:set var="items" value="${it.items}"/>
<st:include page="main.jelly" />
<st:include page="footer.jelly" />
</l:main-panel>
<l:header>
<!-- for screen resolution detection -->
@@ -139,6 +139,8 @@ THE SOFTWARE.
<j:set var="items" value="${it.items}"/>
<st:include page="main.jelly" />
<st:include page="footer.jelly" />
</div>
</l:main-panel>
</l:layout>
+13 -39
View File
@@ -25,7 +25,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:i="jelly:fmt"
xmlns:x="jelly:xml" xmlns:dd="/lib/layout/dropdowns">
xmlns:x="jelly:xml">
<st:documentation> <![CDATA[
Outer-most tag for a normal (non-AJAX) HTML rendering.
This is used with nested <header>, <side-panel>, and <main-panel>
@@ -172,53 +172,27 @@ THE SOFTWARE.
logout="${%logout}"/>
</j:if>
<div id="page-body" class="app-page-body app-page-body--${layoutType} clear">
<j:if test="${layoutType != 'full-screen'}">
<div class="app-page-frame__blur" />
<div class="app-page-frame__mask" />
<div class="app-page-frame__background" />
</j:if>
<div id="page-body" class="${layoutType eq 'full-screen' ? 'app-full-screen-page-body' : 'app-page-body'} clear">
<j:if test="${layoutType=='two-column'}">
<j:set var="mode" value="side-panel" />
<d:invokeBody />
</j:if>
<div id="main-panel">
<div class="app-page-body__contents">
<j:set var="mode" value="main-panel" />
<d:invokeBody/>
</div>
</div>
<j:if test="${layoutType!='full-screen'}">
<footer class="page-footer jenkins-mobile-hide">
<div class="page-footer__flex-row">
<div class="page-footer__footer-id-placeholder" id="footer"></div>
<j:if test="${extensionsAvailable}">
<j:forEach var="pd" items="${h.pageDecorators}">
<st:include it="${pd}" page="footer.jelly" optional="true" />
</j:forEach>
</j:if>
<div class="page-footer__links">
<j:if test="${!empty(it.api)}">
<a class="jenkins-button jenkins-button--tertiary rest-api" href="api/">REST API</a>
</j:if>
<l:overflowButton text="Jenkins ${h.version}"
clazz="jenkins-button--tertiary jenkins_ver"
icon="${null}"
tooltip="${null}">
<l:hasPermission permission="${app.SYSTEM_READ}">
<dd:item icon="symbol-jenkins"
text="${%About Jenkins}"
href="${rootURL}/manage/about" />
<dd:separator />
</l:hasPermission>
<dd:item icon="symbol-heart"
text="${%Get involved}"
href="https://www.jenkins.io/participate/" />
<dd:item icon="symbol-external"
text="${%Website}"
href="${h.getFooterURL()}" />
</l:overflowButton>
</div>
</div>
</footer>
<j:if test="${layoutType!='full-screen' and extensionsAvailable}">
<j:forEach var="pd" items="${h.pageDecorators}">
<st:include it="${pd}" page="footer.jelly" optional="true" />
</j:forEach>
</j:if>
</body>
</html>
@@ -26,40 +26,47 @@ THE SOFTWARE.
<j:jelly xmlns:j="jelly:core" xmlns:x="jelly:xml" xmlns:st="jelly:stapler" xmlns:d="jelly:define">
<st:documentation>
Generates the body as the main content part of a Jenkins page.
<st:attribute name="width">
Optional, sets the width of the page. Available options are 'default', 'narrow', or 'wide'.
</st:attribute>
</st:documentation>
<j:if test="${mode=='main-panel'}">
<j:if test="${app.isQuietingDown()}">
<j:choose>
<j:when test="${app.isPreparingSafeRestart()}">
<div id='safe-restart-msg'>
<j:choose>
<j:when test="${!app.getQuietDownReason().trim().isEmpty()}">
${app.getQuietDownReason()}
</j:when>
<j:otherwise>
${%saferestart}
</j:otherwise>
</j:choose>
</div>
</j:when>
<j:otherwise>
<div id='shutdown-msg'>
<j:choose>
<j:when test="${app.getQuietDownReason() != null}">
${app.getQuietDownReason()}
</j:when>
<j:otherwise>
${%shutdown}
</j:otherwise>
</j:choose>
</div>
</j:otherwise>
</j:choose>
</j:if>
<a id="skip2content" />
<x:comment>&#10;start of main content ⇒&#10;</x:comment>
<d:invokeBody />
<x:comment>&#10;⇐ end of main content&#10;</x:comment>
<div id="main-panel" class="app-main-panel
${attrs.width eq 'wide' ? 'app-main-panel--wide' : ''}
${attrs.width eq 'narrow' ? 'app-main-panel--narrow' : ''}">
<j:if test="${app.isQuietingDown()}">
<j:choose>
<j:when test="${app.isPreparingSafeRestart()}">
<div id='safe-restart-msg'>
<j:choose>
<j:when test="${!app.getQuietDownReason().trim().isEmpty()}">
${app.getQuietDownReason()}
</j:when>
<j:otherwise>
${%saferestart}
</j:otherwise>
</j:choose>
</div>
</j:when>
<j:otherwise>
<div id='shutdown-msg'>
<j:choose>
<j:when test="${app.getQuietDownReason() != null}">
${app.getQuietDownReason()}
</j:when>
<j:otherwise>
${%shutdown}
</j:otherwise>
</j:choose>
</div>
</j:otherwise>
</j:choose>
</j:if>
<a id="skip2content" />
<x:comment>&#10;start of main content ⇒&#10;</x:comment>
<d:invokeBody />
<x:comment>&#10;⇐ end of main content&#10;</x:comment>
</div>
</j:if>
</j:jelly>
@@ -65,7 +65,7 @@ THE SOFTWARE.
Defaults to false if not set.
</st:attribute>
<st:attribute name="width">
Optional, sets the width of the page. Available options are 'default' or 'wide'.
Optional, sets the width of the page. Available options are 'narrow' or 'wide'.
</st:attribute>
</st:documentation>
@@ -78,8 +78,7 @@ THE SOFTWARE.
<l:layout title="${pageTitle} - ${manageJenkinsAction.displayName}"
permission="${attrs.permission}"
permissions="${attrs.permissions}"
type="${(newManageJenkins or attrs.includeSidepanel) ? 'two-column' : 'one-column'}">
permissions="${attrs.permissions}">
<j:if test="${attrs.includeBreadcrumb}">
<!-- Hacky - this will be improved in subsequent PRs -->
@@ -120,23 +119,19 @@ THE SOFTWARE.
</j:forEach>
</l:tasks>
</l:side-panel>
<l:main-panel>
<div class="app-settings-container">
<div class="app-settings-container__inner ${attrs.width eq 'wide' ? 'app-settings-container__inner--wide' : ''}">
<j:if test="${!attrs.containsKey('header')}">
<l:app-bar title="${pageTitle}" />
<j:if test="${!empty(description)}">
<div class="jenkins-page-description">
${description}
</div>
</j:if>
</j:if>
<l:main-panel width="${attrs.width ?: 'narrow'}">
<j:if test="${!attrs.containsKey('header')}">
<l:app-bar title="${pageTitle}" />
<j:if test="${!empty(description)}">
<div class="jenkins-page-description">
${description}
</div>
</j:if>
</j:if>
<j:out value="${attrs.header}" />
<j:out value="${attrs.header}" />
<d:invokeBody />
</div>
</div>
<d:invokeBody />
</l:main-panel>
</j:when>
<j:otherwise>
@@ -144,7 +139,7 @@ THE SOFTWARE.
<st:include page="sidepanel" />
</j:if>
<l:main-panel>
<l:main-panel width="${attrs.width ?: 'narrow'}">
<j:if test="${!attrs.containsKey('header')}">
<l:app-bar title="${pageTitle}" />
<j:if test="${!empty(description)}">
@@ -26,17 +26,11 @@ THE SOFTWARE.
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define">
<st:documentation>
Generates a left side content as part of a Jenkins page. Typically known as two-column or left-side + main-content layouts
<st:attribute name="sticky">
Make the side panel sticky. Should not be used on pages that include widgets in the sidepanel and when plugins can
add their own tasks, so the number of tasks is not fixed.
</st:attribute>
</st:documentation>
<j:if test="${mode=='side-panel'}">
<div id="side-panel" class="app-page-body__sidebar ${attrs.sticky == 'true' ? 'app-page-body__sidebar--sticky' : ''}">
<div id="side-panel" class="app-page-body__sidebar">
<d:invokeBody />
</div>
</j:if>
</j:jelly>
-35
View File
@@ -17,44 +17,9 @@ function init() {
computeOverflow();
});
// Fade in the page header on scroll, increasing opacity and intensity of the backdrop blur
window.addEventListener("scroll", () => {
const navigation = document.querySelector("#page-header");
const scrollY = Math.max(0, window.scrollY);
navigation.style.setProperty(
"--background-opacity",
Math.min(70, scrollY) + "%",
);
navigation.style.setProperty(
"--background-blur",
Math.min(40, scrollY) + "px",
);
if (
!document.querySelector("#main-panel > .jenkins-search--app-bar") &&
!document.querySelector(".app-page-body__sidebar--sticky")
) {
const prefersContrast = window.matchMedia(
"(prefers-contrast: more)",
).matches;
navigation.style.setProperty(
"--border-opacity",
Math.min(
prefersContrast ? 100 : 15,
prefersContrast ? scrollY * 3 : scrollY,
) + "%",
);
}
});
window.addEventListener("load", () => {
// We can't use :has due to HtmlUnit CSS Parser not supporting it, so
// these are workarounds for that same behaviour
if (document.querySelector("#main-panel > .jenkins-app-bar--sticky")) {
document
.querySelector(".jenkins-header")
.classList.add("jenkins-header--has-sticky-app-bar");
}
if (!document.querySelector(".jenkins-breadcrumbs__list-item")) {
document
.querySelector(".jenkins-header")
+16 -7
View File
@@ -78,11 +78,21 @@ $semantics: (
// Background colors
--background: rgb(254 254 254);
// Body
--body-background-top: color-mix(
in oklch,
white 1.5%,
var(--card-background)
);
// Header
--header-background: var(--background);
--header-border: var(--text-color-secondary);
--header-background: color-mix(
in sRGB,
var(--background),
var(--text-color-secondary) 2.5%
);
--header-color: var(--text-color);
--header-height: 4.125rem;
--header-height: 3.5rem;
// App bar
--bottom-app-bar-padding: 0.875rem;
@@ -152,7 +162,6 @@ $semantics: (
var(--jenkins-border-color--subtle);
@media (prefers-contrast: more) {
--header-border: var(--text-color);
--focus-input-border: var(--text-color);
--jenkins-border-color: var(--text-color);
--jenkins-border-color--subtle: var(--text-color);
@@ -268,7 +277,7 @@ $semantics: (
--panel-border-color: var(--light-grey);
// Form
--section-padding: 1.625rem;
--section-padding: 1.25rem;
--input-color: var(--white);
--input-border: color-mix(
in sRGB,
@@ -285,7 +294,7 @@ $semantics: (
--form-item-max-width--small: min(35vw, 1200px);
@media screen and (max-width: breakpoints.$tablet-breakpoint) {
--section-padding: 1.25rem;
--section-padding: 1rem;
--form-item-max-width: 100%;
--form-item-max-width--medium: 100%;
--form-item-max-width--small: 100%;
@@ -293,7 +302,7 @@ $semantics: (
--form-label-font-weight: var(--font-bold-weight);
--form-input-padding: 0.5rem 0.625rem;
--form-input-border-radius: 0.625rem;
--form-input-border-radius: 0.6875rem;
--form-input-glow: 0 0 0 0.5rem transparent;
--form-input-glow--focus: 0 0 0 0.25rem var(--focus-input-glow);
--pre-background: var(--button-background);
+3 -2
View File
@@ -1,9 +1,9 @@
html {
height: 100%;
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
color: var(--text-color);
scroll-padding-top: calc(var(--header-height) + var(--section-padding));
overscroll-behavior: none;
scrollbar-color: oklch(from var(--text-color-secondary) l c h / 0.35)
oklch(from var(--text-color-secondary) l c h / 0.05);
}
@@ -13,7 +13,8 @@ body {
min-height: 100vh;
display: flex;
flex-direction: column;
background-color: var(--background);
background: var(--header-background);
background-attachment: fixed;
}
*,
+176 -94
View File
@@ -1,112 +1,194 @@
@use "../base/breakpoints";
/* -------------------------------------- */
.app-page-body {
display: flex;
justify-content: flex-start;
position: relative;
display: grid;
align-items: stretch;
flex: 1 0 auto;
}
border: 1px solid transparent;
margin-inline: var(--page-inset);
isolation: isolate;
flex-grow: 1;
.app-page-body__sidebar {
@media (min-width: breakpoints.$tablet-breakpoint) {
&--sticky {
position: sticky;
top: var(--header-height);
align-self: flex-start;
&:has(> .app-page-body__sidebar) {
grid-template-columns: auto 1fr;
}
@media (width < 740px) {
display: flex;
flex-direction: column;
}
& > .app-page-body__sidebar {
padding: var(--section-padding);
@media (width >= 740px) {
width: 280px;
&:not(:has(.pane, .jenkins-card)) {
position: sticky;
top: calc(var(--header-height) + var(--jenkins-border-width));
height: calc(100vh - var(--header-height) - var(--page-inset));
overflow-y: auto;
border-right: var(--jenkins-border);
}
}
@media (width > breakpoints.$tablet-breakpoint) {
width: 320px;
}
}
& > .app-page-body__contents {
padding: var(--section-padding);
padding-bottom: 0;
}
// Remove this when sidepanels no longer contain widgets
&:has(#side-panel .pane, #side-panel .jenkins-card) {
& > .app-page-body__sidebar {
padding-right: 0;
#tasks {
margin-bottom: 1rem;
}
.jenkins-card,
.pane-frame {
margin-inline: -0.5rem;
}
}
}
}
.app-main-panel {
margin-bottom: var(--section-padding);
&--narrow {
max-width: 900px;
margin-inline: auto;
}
&--wide {
width: 70vw;
max-width: 1700px;
margin-inline: auto;
}
}
.app-full-screen-page-body {
display: flex;
flex-direction: column;
flex-grow: 1;
& > .app-page-body__contents {
display: flex;
flex-direction: column;
flex-grow: 1;
min-height: 0;
}
.app-main-panel {
display: flex;
flex-direction: column;
flex-grow: 1;
min-height: 0;
margin-bottom: 0;
}
}
// Page frame
.app-page-frame__background {
position: relative;
pointer-events: none;
&::before,
&::after {
content: "";
position: fixed;
inset: var(--header-height) var(--page-inset) var(--page-inset)
var(--page-inset);
border-radius: var(--form-input-border-radius);
}
&::before {
--background: var(--card-background);
background-color: var(--card-background);
background-image: linear-gradient(
in oklch,
var(--body-background-top),
var(--background)
);
background-size: 100% 90vh;
background-repeat: no-repeat;
z-index: -1;
}
&::after {
border: var(--jenkins-border);
z-index: 1;
}
}
.app-page-frame__mask {
position: fixed;
inset: 0;
z-index: 1;
pointer-events: none;
mask:
linear-gradient(#000 0 0),
linear-gradient(#000 0 0) left calc(var(--page-inset) + 1px) top
calc(var(--header-height) + 1px) /
calc(100% - calc(var(--page-inset) + 1px) * 2)
calc(
100% - calc(var(--header-height) + 1px) - calc(var(--page-inset) + 1px)
)
no-repeat;
mask-composite: exclude;
&::before {
content: "";
position: absolute;
inset: 0;
background: var(--header-background);
background-attachment: fixed;
opacity: 0.75;
}
}
.app-page-frame__blur {
position: fixed;
inset: 0;
z-index: 1;
pointer-events: none;
&::before,
&::after {
content: "";
position: absolute;
left: 0;
right: 0;
backdrop-filter: blur(15px);
}
&::before {
top: 0;
height: calc(var(--header-height) + var(--jenkins-border-width));
}
&::after {
bottom: 0;
height: var(--page-inset);
}
}
// Legacy
#page-body.clear::after {
clear: both;
content: "";
display: table;
}
#side-panel {
flex-shrink: 0;
}
#main-panel {
padding: 0 var(--section-padding) var(--section-padding)
var(--section-padding);
display: inline-block;
width: 100%;
}
.app-page-body--one-column {
--form-item-max-width: calc(85vw - 4rem);
@media screen and (width <= 1200px) {
--form-item-max-width: 100%;
}
max-width: 85vw;
#main-panel {
width: 100vw;
}
.jenkins-section {
max-width: unset;
}
@media (width <= 1200px) {
max-width: unset;
}
margin: auto;
}
body.two-column #main-panel {
width: calc(100% - var(--side-panel-width));
flex: 1;
display: block;
}
body.full-screen {
padding: 0;
}
body.full-screen #main-panel {
padding: 0;
}
body.two-column #side-panel {
width: var(--side-panel-width);
}
@media (max-width: breakpoints.$tablet-breakpoint) {
body.two-column #page-body {
flex-wrap: wrap;
}
body.two-column #side-panel {
width: 100%;
border-right: none;
}
body.two-column #main-panel {
margin-left: 0;
width: 100%;
}
}
.app-project-status-table {
width: 100%;
overflow-x: auto;
}
@media (width >= 1170px) {
body.two-column #main-panel {
width: calc(
100% - calc(var(--side-panel-width) + calc(var(--section-padding) * 2))
);
}
}
// Clearfixes extracted from responsive-grid.css as they essential
.clearfix::before,
.clearfix::after,
+14 -2
View File
@@ -3,13 +3,19 @@
// clicks.
// Keep this list in sync with the `position: sticky` rules across the SCSS sources.
:root[data-disable-sticky="true"] {
.app-page-frame__blur,
.app-page-frame__mask,
.app-page-frame__background {
display: none;
}
.jenkins-header,
.jenkins-app-bar--sticky,
.jenkins-bottom-app-bar__shadow,
.bottom-sticker,
#bottom-sticker,
.app-page-body__sidebar--sticky,
.app-plugin-manager__sidebar {
.app-build-bar,
.app-page-body__sidebar {
position: relative !important;
// Reset the sticky offsets too — with relative positioning these would otherwise
@@ -19,6 +25,12 @@
bottom: auto !important;
}
// Don't allow for sidebar scrolling
.app-page-body__sidebar {
overflow-y: visible !important;
height: unset !important;
}
// The bottom app bar shadow uses negative margins to overlap content; reset them so it
// cannot intercept clicks when sticky positioning is disabled.
.jenkins-bottom-app-bar__shadow {
+8 -6
View File
@@ -3,7 +3,7 @@
align-items: center;
justify-content: space-between;
margin-bottom: 0.875rem;
min-height: 2.875rem;
min-height: 2.5rem;
gap: var(--section-padding);
flex-wrap: wrap;
@@ -68,23 +68,23 @@
&--sticky {
position: sticky;
top: var(--header-height);
top: calc(var(--header-height) + 1.3125rem);
z-index: 2;
&::before,
&::after {
content: "";
position: absolute;
inset: calc(var(--header-height) * -1)
inset: calc(var(--section-padding) * -1)
calc(var(--section-padding) * -1) -0.875rem;
z-index: -1;
pointer-events: none;
}
&::before {
background: var(--background);
background: var(--body-background-top);
mask-image: linear-gradient(black 70%, transparent);
opacity: 0.55;
opacity: 0.6;
}
&::after {
@@ -106,7 +106,8 @@
h1,
h2 {
margin: 0;
font-size: 1.3125rem;
font-size: 1.1875rem;
font-weight: 550;
}
&__subtitle {
@@ -126,6 +127,7 @@
}
.jenkins-bottom-app-bar__shadow {
--background: var(--card-background);
--semi-translucent: color-mix(in sRGB, var(--background) 70%, transparent);
position: sticky;
@@ -9,6 +9,7 @@
&__list {
display: flex;
margin: 0;
& > * {
z-index: 1;
@@ -6,10 +6,6 @@
font-size: var(--font-size-sm);
}
.page-footer .container-fluid {
padding: 0;
}
.page-footer__flex-row {
display: flex;
align-items: stretch;
@@ -19,8 +15,6 @@
.page-footer__flex-row > *:not(script) {
display: flex;
align-items: center;
padding: calc(var(--section-padding) / 2)
calc(var(--section-padding) - 0.85rem);
gap: calc(var(--section-padding) / 2);
}
@@ -32,16 +26,3 @@
.page-footer span {
display: inline-block;
}
.page-footer .jenkins-button {
transition: var(--standard-transition);
&:hover {
color: var(--text-color) !important;
}
svg,
.jenkins-overflow-button__ellipsis {
display: none;
}
}
+5 -40
View File
@@ -1,13 +1,9 @@
@use "../abstracts/mixins";
.jenkins-header {
--background-opacity: 0%;
--background-blur: 0;
--border-opacity: 0%;
position: sticky;
top: 0;
z-index: 999;
z-index: 2;
display: flex;
align-items: center;
gap: var(--section-padding);
@@ -16,26 +12,6 @@
min-height: var(--header-height);
color: var(--header-color);
&::before {
content: "";
position: absolute;
inset: 0;
backdrop-filter: blur(var(--background-blur));
background: color-mix(
in sRGB,
var(--header-background) var(--background-opacity),
transparent
);
border-bottom: var(--jenkins-border-width) solid
color-mix(
in sRGB,
var(--header-border) var(--border-opacity),
transparent
);
background-clip: padding-box;
z-index: -1;
}
.app-jenkins-logo {
@include mixins.item($border: false);
@@ -79,7 +55,7 @@
}
#jenkins-head-icon {
width: 1.625rem;
width: 1.5rem;
}
}
@@ -91,11 +67,6 @@
// For customizable-header-plugin
color: inherit !important;
svg {
width: 1.25rem;
height: 1.25rem;
}
img {
width: 1.625rem;
height: 1.625rem;
@@ -121,7 +92,7 @@
&__main {
display: grid;
grid-template-columns: 1fr auto;
padding-left: var(--section-padding);
padding-left: calc(var(--section-padding) + var(--page-inset));
width: 100%;
}
@@ -135,14 +106,8 @@
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding-right: var(--section-padding);
}
&--has-sticky-app-bar {
&::before {
mask-image: linear-gradient(black 50%, transparent);
}
gap: 0.25rem;
padding-right: calc(var(--section-padding) + var(--page-inset));
}
&--no-breadcrumbs {
@@ -1,19 +1,14 @@
@use "../abstracts/mixins";
@use "../base/breakpoints";
$background-outset: 0.5rem;
$inset: 0.6875rem;
#tasks,
.tasks,
.subtasks {
display: flex;
flex-direction: column;
margin: 0 var(--section-padding) var(--section-padding) var(--section-padding);
gap: 0.125rem;
@media (min-width: breakpoints.$tablet-breakpoint) {
margin-right: calc($background-outset);
}
}
.subtasks {
@@ -27,12 +22,11 @@ $background-outset: 0.5rem;
#side-panel {
.jenkins-app-bar {
margin-left: var(--section-padding);
margin-right: var(--section-padding);
margin-inline: $inset;
}
& > #tasks > .jenkins-search-container {
margin: 0 0 1rem #{-$background-outset};
margin: 0 0 1rem;
.jenkins-search__icon {
width: 2.625rem;
@@ -45,10 +39,6 @@ $background-outset: 0.5rem;
}
}
#tasks .task {
margin: 0 0 0 calc($background-outset * -1);
}
#tasks,
.tasks {
.task .task-link {
@@ -57,7 +47,7 @@ $background-outset: 0.5rem;
display: flex;
align-items: center;
justify-content: flex-start;
padding: 0.5rem 0.6875rem;
padding: 0.5rem $inset;
gap: 0.625rem;
width: 100%;
font-size: var(--font-size-sm);
@@ -115,7 +105,7 @@ $background-outset: 0.5rem;
font-size: var(--font-size-sm);
color: var(--text-color-secondary);
font-weight: var(--font-bold-weight);
margin: 1rem 0 0.375rem;
margin: 1rem 0 0.375rem $inset;
&:first-of-type {
margin-top: 0.375rem;
@@ -8,10 +8,6 @@
overflow: hidden;
}
#side-panel .pane-frame {
margin-left: 10px;
}
#side-panel .pane-header,
.app-build-content .pane-header {
font-size: var(--font-size-sm);
+63 -84
View File
@@ -12,7 +12,6 @@
.app-build__grid {
gap: calc(var(--section-padding) / 2);
margin-bottom: calc(var(--section-padding) * -0.5);
& > * {
height: 350px;
@@ -139,101 +138,74 @@
}
}
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,
var(--text-color-secondary) 2%,
var(--background)
);
@keyframes app-build-bar-border-on-scroll {
from {
border-bottom-color: color-mix(
in sRGB,
var(--jenkins-border-color),
transparent
);
}
background: linear-gradient(var(--top), var(--bottom));
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;
padding: var(--section-padding);
display: flex;
flex-direction: column;
&--wide {
width: 70vw;
max-width: 1700px;
}
// TODO - this can be moved to app-bar.scss when Manage Jenkins flag is removed
.jenkins-app-bar--sticky {
padding: var(--section-padding) 0 !important;
margin: -1rem 0 0.5rem !important;
&::before,
&::after {
inset: 0 !important;
}
&::before {
background: color-mix(
in sRGB,
var(--text-color-secondary) 2%,
var(--card-background)
) !important;
mask-image: linear-gradient(black 60%, transparent) !important;
opacity: 0.75 !important;
}
&::after {
left: calc(var(--section-padding) * -1) !important;
right: calc(var(--section-padding) * -1) !important;
mask-image: linear-gradient(black 65%, transparent) !important;
}
}
to {
border-bottom-color: var(--jenkins-border-color);
}
}
.app-build-bar {
position: relative;
--app-build-bar-border-color: color-mix(
in sRGB,
var(--jenkins-border-color),
transparent
);
position: sticky;
top: calc(var(--header-height) + var(--jenkins-border-width));
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: start;
align-items: center;
gap: 0.5rem 1.5rem;
min-height: 2.875rem;
margin-top: -5px;
padding-bottom: 0.75rem;
min-height: var(--header-height);
background-color: color-mix(
in oklch,
transparent 25%,
var(--body-background-top)
);
padding-inline: var(--section-padding);
z-index: 102;
@media (width <= 1200px) {
display: flex;
flex-direction: column;
}
border-bottom: var(--jenkins-border-width) solid
var(--app-build-bar-border-color);
margin-left: calc(var(--section-padding) * -1);
margin-right: calc(var(--section-padding) * -1);
border-top-left-radius: var(--form-input-border-radius);
border-top-right-radius: var(--form-input-border-radius);
&::before {
content: "";
position: absolute;
inset: -1000px calc(var(--section-padding) * -1) 0;
background-color: var(--background);
border-bottom: var(--jenkins-border-width) solid var(--card-border-color);
inset: 0;
z-index: -1;
pointer-events: none;
backdrop-filter: blur(15px);
border-radius: inherit;
}
@supports (animation-timeline: scroll()) {
animation-name: app-build-bar-border-on-scroll;
animation-duration: 1s;
animation-fill-mode: both;
animation-timing-function: linear;
animation-timeline: scroll(nearest block);
animation-range: 0 var(--header-height);
}
@media (width < 740px) {
position: relative;
display: flex;
flex-direction: column;
align-items: start;
padding-block: var(--section-padding);
top: 0;
}
.app-build-bar__content {
@@ -247,6 +219,7 @@ body:has(.app-settings-container) {
display: inline-flex;
align-items: center;
justify-content: center;
margin-block: 0.5rem;
}
.app-build-bar__controls {
@@ -254,7 +227,7 @@ body:has(.app-settings-container) {
align-items: center;
justify-content: end;
flex: 1;
gap: 0.75rem;
gap: 0.5rem;
@media (width <= 600px) {
justify-content: stretch;
@@ -278,12 +251,14 @@ body:has(.app-settings-container) {
svg {
width: 2rem;
height: 2rem;
flex-shrink: 0;
}
}
h1 {
margin-bottom: 0;
font-size: 1.25rem;
font-size: 1.1875rem;
font-weight: 550;
}
.jenkins-button {
@@ -325,6 +300,10 @@ body:has(.app-settings-container) {
}
}
.app-page-body__contents:has(.app-build-bar) {
padding-top: 0;
}
.app-build-content {
display: flex;
flex-direction: column;
-6
View File
@@ -1,11 +1,5 @@
@use "../abstracts/mixins";
#side-panel {
#buildHistoryPage {
margin: 10px 0 10px 10px;
}
}
#buildHistoryPage {
.jenkins-search {
margin-inline: -0.25rem;
-10
View File
@@ -104,13 +104,3 @@
border-radius: 0.25em;
}
}
.app-plugin-manager__sidebar {
position: sticky;
top: calc(40px + 1.6rem);
h1 {
margin-top: 1rem !important;
margin-bottom: 0.1rem !important;
}
}
@@ -57,7 +57,6 @@ class ErrorPageTest {
final String content = ex.getResponse().getContentAsString(StandardCharsets.UTF_8);
assertThat(content, not(containsString(j.contextPath + "/login?from=")));
assertThat(content, containsString("This page does not exist."));
assertThat(content, not(containsString("REST API")));
}
{ // paths are fine on error page even when nested
@@ -66,7 +65,6 @@ class ErrorPageTest {
final String content = ex.getResponse().getContentAsString(StandardCharsets.UTF_8);
assertThat(content, not(containsString(j.contextPath + "/login?from=")));
assertThat(content, containsString("This page does not exist."));
assertThat(content, not(containsString("REST API")));
}
{ // resource root action have custom (less) error message content
@@ -76,7 +74,6 @@ class ErrorPageTest {
assertThat(content, not(containsString(j.contextPath + "/login?from=")));
assertThat(content, not(containsString("This page does not exist.")));
assertThat(content, not(containsString("This page may not exist, or you may not have permission to see it.")));
assertThat(content, not(containsString("REST API")));
}
/* Set up security realm and request as anonymous, we expect login link and hedged response */
@@ -90,7 +87,6 @@ class ErrorPageTest {
assertThat(content, containsString(j.contextPath + "/login?from=" + j.contextPath.replace("/", "%2F") + "%2Ffoo"));
assertThat(content, not(containsString(j.contextPath + "/login?from=" + j.contextPath.replace("/", "%2F") + "%2F404")));
assertThat(content, containsString("This page may not exist, or you may not have permission to see it."));
assertThat(content, not(containsString("REST API")));
}
{ // paths are fine on error page even when nested
@@ -100,7 +96,6 @@ class ErrorPageTest {
assertThat(content, containsString(j.contextPath + "/login?from=" + j.contextPath.replace("/", "%2F") + "%2Ffoo%2Fbar%2Fbaz%2F"));
assertThat(content, not(containsString(j.contextPath + "/login?from=" + j.contextPath.replace("/", "%2F") + "%2F404")));
assertThat(content, containsString("This page may not exist, or you may not have permission to see it."));
assertThat(content, not(containsString("REST API")));
}
{ // resource root action have custom (less) error message content
@@ -111,7 +106,6 @@ class ErrorPageTest {
assertThat(content, not(containsString(j.contextPath + "/login?from=" + j.contextPath.replace("/", "%2F") + "%2F404")));
assertThat(content, not(containsString("This page does not exist.")));
assertThat(content, not(containsString("This page may not exist, or you may not have permission to see it.")));
assertThat(content, not(containsString("REST API")));
}
/* With the security realm still set up, log in and expect the profile link to show */
@@ -124,7 +118,6 @@ class ErrorPageTest {
assertThat(content, not(containsString(j.contextPath + "/login?from=")));
assertThat(content, containsString("user/alice"));
assertThat(content, containsString("This page may not exist, or you may not have permission to see it."));
assertThat(content, not(containsString("REST API")));
}
{ // paths are fine on error page even when nested
@@ -134,7 +127,6 @@ class ErrorPageTest {
assertThat(content, not(containsString(j.contextPath + "/login?from=")));
assertThat(content, containsString("user/alice"));
assertThat(content, containsString("This page may not exist, or you may not have permission to see it."));
assertThat(content, not(containsString("REST API")));
}
{ // resource root action have custom (less) error message content
@@ -145,7 +137,6 @@ class ErrorPageTest {
assertThat(content, containsString("user/alice"));
assertThat(content, not(containsString("This page does not exist.")));
assertThat(content, not(containsString("This page may not exist, or you may not have permission to see it.")));
assertThat(content, not(containsString("REST API")));
}
} finally {
Dispatcher.TRACE = true;
@@ -176,13 +167,11 @@ class ErrorPageTest {
final String content = page.getWebResponse().getContentAsString(StandardCharsets.UTF_8);
assertThat(content, containsString("Back to Jenkins"));
assertThat(content, containsString("Jenkins serves only static files on this domain."));
assertThat(content, not(containsString("REST API")));
if (page.isHtmlPage()) {
final HtmlPage htmlPage = (HtmlPage) page;
final Page nextPage = htmlPage.getAnchorByText("Back to Jenkins").click();
final String nextContent = nextPage.getWebResponse().getContentAsString(StandardCharsets.UTF_8);
assertThat(nextContent, containsString("Welcome to Jenkins"));
assertThat(nextContent, containsString("REST API")); // Rest API exists for Jenkins main page
}
}
}