Add initial implementation of new builds list for experimental Job UI (#27189)

This commit is contained in:
Jan Faracik authored and GitHub committed 2026-09-15 23:15:49 +01:00
1 parent c6938cc3cb
commit 52b837be4a
14 files changed
+809 -27

No files matched your search

@@ -41,6 +41,7 @@ import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import jenkins.model.HistoricalBuild;
import jenkins.model.experimentalflags.NewJobPageUserExperimentalFlag;
import jenkins.util.SystemProperties;
import jenkins.widgets.HistoryPageEntry;
import jenkins.widgets.HistoryPageFilter;
@@ -206,7 +207,8 @@ public class HistoryWidget<O extends ModelObject, T> extends Widget {
}
protected HistoryPageFilter<T> newPageFilter() {
HistoryPageFilter<T> historyPageFilter = new HistoryPageFilter<>(THRESHOLD);
int maxEntries = new NewJobPageUserExperimentalFlag().getFlagValue() ? NEW_JOB_PAGE_THRESHOLD : THRESHOLD;
HistoryPageFilter<T> historyPageFilter = new HistoryPageFilter<>(maxEntries);
historyPageFilter.widget = this;
if (newerThan != null) {
@@ -280,6 +282,11 @@ public class HistoryWidget<O extends ModelObject, T> extends Widget {
static final int THRESHOLD = SystemProperties.getInteger(HistoryWidget.class.getName() + ".threshold", 30);
/**
* Maximum number of builds shown in the builds list when the experimental new job page is enabled.
*/
static final int NEW_JOB_PAGE_THRESHOLD = 10;
public String getNextBuildNumberToFetch() {
return nextBuildNumberToFetch;
}
@@ -0,0 +1,34 @@
<!--
The MIT License
Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi
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.
-->
<!--
Render build histories.
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler">
<j:set target="${it}" property="nextBuildNumberToFetch" value="${it.nextBuildNumber}" />
<j:forEach var="pageEntry" items="${it.runs}">
<st:include page="/hudson/widgets/HistoryWidget/entry-new.jelly" />
</j:forEach>
</j:jelly>
@@ -26,23 +26,32 @@ THE SOFTWARE.
Render build histories.
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:i="jelly:fmt">
<j:set target="${it}" property="nextBuildNumberToFetch" value="${it.nextBuildNumber}" />
<j:if test="${h.isUserTimeZoneOverride()}">
<i:setTimeZone value="${h.getUserTimeZone()}" />
</j:if>
<j:new var="dateNow" className="java.util.Date"/>
<i:formatDate value="${dateNow}" var="now" type="date" pattern="YYYY-MM-dd"/>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:i="jelly:fmt" xmlns:l="/lib/layout">
<l:userExperimentalFlag var="newJobPage" flagClassName="jenkins.model.experimentalflags.NewJobPageUserExperimentalFlag" />
<j:forEach var="pageEntry" items="${it.runs}">
<i:formatDate value="${pageEntry.entry.timestamp.time}" var="date" type="date" dateStyle="long" />
<i:formatDate value="${pageEntry.entry.timestamp.time}" var="simpleDate" type="date" pattern="YYYY-MM-dd" />
<j:choose>
<j:when test="${newJobPage}">
<st:include page="/hudson/widgets/HistoryWidget/entries-new" />
</j:when>
<j:otherwise>
<j:set target="${it}" property="nextBuildNumberToFetch" value="${it.nextBuildNumber}" />
<j:if test="${h.isUserTimeZoneOverride()}">
<i:setTimeZone value="${h.getUserTimeZone()}" />
</j:if>
<j:new var="dateNow" className="java.util.Date"/>
<i:formatDate value="${dateNow}" var="now" type="date" pattern="YYYY-MM-dd"/>
<j:if test="${pastDate != date}">
<span class="app-builds-container__heading">${simpleDate == now ? "%Today" : date}</span>
<j:set var="pastDate" value="${date}" />
</j:if>
<j:forEach var="pageEntry" items="${it.runs}">
<i:formatDate value="${pageEntry.entry.timestamp.time}" var="date" type="date" dateStyle="long" />
<i:formatDate value="${pageEntry.entry.timestamp.time}" var="simpleDate" type="date" pattern="YYYY-MM-dd" />
<st:include page="/hudson/widgets/HistoryWidget/entry.jelly" />
</j:forEach>
<j:if test="${pastDate != date}">
<span class="app-builds-container__heading">${simpleDate == now ? "%Today" : date}</span>
<j:set var="pastDate" value="${date}" />
</j:if>
<st:include page="/hudson/widgets/HistoryWidget/entry.jelly" />
</j:forEach>
</j:otherwise>
</j:choose>
</j:jelly>
@@ -0,0 +1,88 @@
<!--
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.
-->
<!--
Render a single build history entry indicated by ${build} (jenkins.model.HistoricalBuild)
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:l="/lib/layout" xmlns:i="jelly:fmt">
<j:set var="build" value="${pageEntry.entry}" />
<j:set var="firstTransientBuildKey" value="${it.widget.firstTransientBuildKey}" />
<j:set var="transitive" value="${(firstTransientBuildKey!=null and (it.widget.adapter.compare(build,firstTransientBuildKey) ge 0)) ? 'transitive' : null}" />
<j:set var="link" value="${it.widget.baseUrl}/${build.number}/" />
<j:if test="${h.isUserTimeZoneOverride()}">
<i:setTimeZone value="${h.getUserTimeZone()}" />
</j:if>
<div class="app-temporary-list__item" page-entry-id="${pageEntry.entryId}">
<div class="app-build-item">
<j:set var="iconTooltip">
<div>${build.iconColor.description}</div>
<div class="jenkins-!-text-color-secondary">${%JumpToConsole(h.getConsoleProviderFor(build).descriptor.displayName)}</div>
</j:set>
<a href="${h.getConsoleUrl(build)}"
class="app-build-item__icon"
data-html-tooltip="${iconTooltip}"
data-tooltip-delay="500">
<l:icon src="symbol-status-${build.iconColor.iconName}" />
</a>
<div class="app-temporary-list__item__details" href="${link}">
<div class="app-temporary-list__item__details__text">
<a class="app-temporary-list__item__label" href="${rootURL}/${build.url}">${build.displayName}</a>
<div class="app-temporary-list__item__description">
<l:details-bar it="${build}" />
</div>
<j:set var="badges" value="${build.badgeActions}"/>
<j:if test="${!empty(badges)}">
<div class="app-temporary-list__item__description">
<j:forEach var="badge" items="${badges}">
<st:include it="${badge}" page="badge.jelly" />
</j:forEach>
</div>
</j:if>
<j:if test="${!empty build.truncatedDescription}">
<div class="app-temporary-list__item__description">
<div class="jenkins-details__item">
<div class="jenkins-details__item__icon">
<l:icon src="symbol-description" />
</div>
<j:out value="${app.markupFormatter.translate(build.truncatedDescription)}"/>
</div>
</div>
</j:if>
</div>
</div>
<div class="app-temporary-list__item__actions">
<button class="jenkins-button jenkins-button--tertiary jenkins-jumplist-link" data-href="${link}">
<l:icon src="symbol-menu" />
</button>
</div>
</div>
</div>
</j:jelly>
@@ -0,0 +1 @@
JumpToConsole=Click to jump to {0}
@@ -0,0 +1,45 @@
<!--
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">
<div class="app-temporary-list__item" style="height: 71px">
<div class="app-build-item">
<div class="app-build-item__icon">
<div style="width: 1.25rem; height: 1.25rem; border-radius: 100px" class="app-temporary-skeleton" />
</div>
<div class="app-temporary-list__item__details">
<div class="app-temporary-list__item__details__text">
<div class="app-temporary-list__item__label">
<div style="width: 3rem; height: 1rem;" class="app-temporary-skeleton" />
</div>
<div class="app-temporary-list__item__description" style="gap: 0.25rem">
<div style="width: 20rem; height: 1rem;" class="app-temporary-skeleton" />
</div>
</div>
</div>
</div>
</div>
</j:jelly>
@@ -0,0 +1,79 @@
<!--
The MIT License
Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi,
Seiji Sogabe, id:cactusman, Yahoo! Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:l="/lib/layout" xmlns:st="jelly:stapler">
<script src="${resURL}/jsbundles/pages/project/builds-card-new.js" type="text/javascript" async="true" />
<j:set var="page" value="${it.historyPageFilter}" />
<l:notice id="no-builds-yet"
icon="symbol-play"
title="${%No builds yet}"
clazz="jenkins-hidden">
${%Start the first build to see run history here.}
</l:notice>
<div id="buildHistoryPage" page-ajax="${it.baseUrl}/buildHistory/ajax"
data-page-entry-newest="${page.newestOnPage}"
data-page-entry-oldest="${page.oldestOnPage}"
data-page-has-up="${page.hasUpPage}"
data-page-has-down="${page.hasDownPage}">
<div id="jenkins-builds">
<div style="display: none">
<l:search-bar placeholder="${%find}"
clazz="${page.runs.isEmpty() and page.queueItems.isEmpty() ? 'jenkins-hidden' : ''}"/>
</div>
<div class="app-temporary-list">
<div id="loading-builds" class="app-temporary-list__placeholder">
<j:forEach begin="1" end="10" var="i">
<st:include page="/hudson/widgets/HistoryWidget/entry-skeleton.jelly" />
</j:forEach>
</div>
<div id="no-builds" style="display:none" class="app-temporary-list__placeholder">
${%No builds}
</div>
<div id="jenkins-build-history" />
<div class="app-temporary-list__controls jenkins-hidden" id="controls">
<button class="jenkins-button jenkins-button--tertiary jenkins-card__unveil" id="up">
<l:icon src="symbol-arrow-left" />
<span class="jenkins-visually-hidden">${%Newer builds}</span>
</button>
<button class="jenkins-button jenkins-button--tertiary jenkins-card__unveil" id="down">
<l:icon src="symbol-arrow-right" />
<span class="jenkins-visually-hidden">${%Older builds}</span>
</button>
</div>
</div>
<!--The value for `page-next-build` is modified inside of `entries.jelly` on render, so set the attribute-->
<!--after that component has been rendered to get the correct value to use in `builds-card.js`-->
<div id="properties" page-next-build="${it.nextBuildNumberToFetch ?: it.owner.nextBuildNumber}"/>
</div>
</div>
</j:jelly>
@@ -23,11 +23,11 @@ THE SOFTWARE.
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:dd="/lib/layout/dropdowns">
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:l="/lib/layout" xmlns:t="/lib/hudson">
<l:job-subpage>
<j:set var="it" value="${it.object}" />
<l:app-bar title="Overview">
<l:app-bar title="${%Builds}">
<l:details-bar it="${it}">
<t:editDescriptionButton permission="${it.CONFIGURE}" compact="true" />
</l:details-bar>
@@ -35,11 +35,16 @@ THE SOFTWARE.
<t:editableDescription permission="${it.CONFIGURE}" hideButton="true"/>
<div class="jenkins-!-margin-bottom-2">
<j:forEach var="w" items="${it.widgets}">
<st:include it="${w}" page="index.jelly" />
</j:forEach>
</div>
<j:forEach var="w" items="${it.widgets}">
<j:choose>
<j:when test="${w.class.name == 'hudson.widgets.BuildHistoryWidget'}">
<st:include it="${w}" page="index-new.jelly" />
</j:when>
<j:otherwise>
<st:include it="${w}" page="index.jelly" />
</j:otherwise>
</j:choose>
</j:forEach>
<l:card title="${%Legacy}">
<div>
@@ -23,7 +23,17 @@ THE SOFTWARE.
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler">
<st:include page="queue-items.jelly"/>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:l="/lib/layout">
<l:userExperimentalFlag var="newJobPage" flagClassName="jenkins.model.experimentalflags.NewJobPageUserExperimentalFlag" />
<j:choose>
<j:when test="${newJobPage}">
<st:include page="queue-items-new.jelly"/>
</j:when>
<j:otherwise>
<st:include page="queue-items.jelly"/>
</j:otherwise>
</j:choose>
<st:include page="/hudson/widgets/HistoryWidget/entries.jelly"/>
</j:jelly>
@@ -0,0 +1,80 @@
<!--
The MIT License
Copyright (c) 2026, Jan Faracik
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:l="/lib/layout">
<!-- pending build -->
<j:set var="queuedItems" value="${it.queueItems}" />
<j:if test="${!queuedItems.isEmpty()}">
<j:forEach var="pageEntry" items="${queuedItems}" indexVar="i">
<j:set var="item" value="${pageEntry.entry}" />
<j:set var="id" value="${h.generateId()}" />
<div class="app-temporary-list__item" page-entry-id="${pageEntry.entryId}">
<div class="app-build-item">
<div class="app-build-item__icon">
<l:icon src="symbol-status-nobuilt" />
</div>
<div class="app-temporary-list__item__details">
<div class="app-temporary-list__item__details__text">
<div class="app-temporary-list__item__label">
<!-- Don't use math unless needed, in case nextBuildNumber is not numeric -->
#${queuedItems.size() == 1 ? it.widget.owner.nextBuildNumber : it.widget.owner.nextBuildNumber+queuedItems.size()-i-1}
</div>
<div class="app-temporary-list__item__description">
<div class="jenkins-details">
<j:set var="cause" value="${item.getCauseOfBlockage()}" />
<j:if test="${cause!=null}">
<div class="jenkins-details__item">
<div class="jenkins-details__item__icon">
<l:icon src="symbol-information-circle" />
</div>
<st:include it="${cause}" page="summary.jelly" />
</div>
</j:if>
<j:if test="${!item.params.isEmpty()}">
<div tooltip="Build Parameters: ${item.params}" class="jenkins-details__item">
<div class="jenkins-details__item__icon">
<l:icon src="symbol-parameters" />
</div>
<span>${%Parameters}</span>
</div>
</j:if>
</div>
</div>
</div>
</div>
<div class="app-temporary-list__item__actions">
<j:if test="${item.hasCancelPermission()}">
<l:stopButton href="${rootURL}/queue/cancelItem?id=${item.id}" alt="${%Cancel}" />
</j:if>
</div>
</div>
</div>
</j:forEach>
</j:if>
</j:jelly>
@@ -31,9 +31,15 @@ THE SOFTWARE.
<st:attribute name="icon" use="required">
Sets the icon for the notice. Supports Jenkins Symbols and regular images.
</st:attribute>
<st:attribute name="id" use="optional">
If set, id will be the identifier for the component.
</st:attribute>
<st:attribute name="clazz" use="optional">
Additional CSS class(es) to add.
</st:attribute>
</st:documentation>
<div class="jenkins-notice">
<div class="jenkins-notice ${attrs.clazz}" id="${attrs.id}">
<l:icon src="${attrs.icon}" />
<div>${attrs.title}</div>
<div class="jenkins-notice__description">
@@ -0,0 +1,170 @@
import debounce from "lodash/debounce";
import BehaviorShim from "@/util/behavior-shim";
BehaviorShim.specify(
"#buildHistoryPage",
"build-history-page",
1000,
(buildHistoryPage) => {
// Card/item controls
const pageSearch = buildHistoryPage.querySelector(".jenkins-search");
const pageSearchInput = buildHistoryPage.querySelector("input");
const ajaxUrl = buildHistoryPage.getAttribute("page-ajax");
const card = document.querySelector("#jenkins-builds");
const contents = card.querySelector("#jenkins-build-history");
const container = card.querySelector(".app-temporary-list");
const loadingBuilds = card.querySelector("#loading-builds");
const noBuilds = card.querySelector("#no-builds");
const noBuildsYet = document.querySelector("#no-builds-yet");
// Pagination controls
const paginationControls = document.querySelector("#controls");
const paginationPrevious = document.querySelector("#up");
const paginationNext = document.querySelector("#down");
// Refresh variables
let buildRefreshTimeout;
const updateBuildsRefreshInterval = 5000;
/**
* Refresh the 'Builds' card
* @param {QueryParameters} options
*/
function load(options = {}) {
/** @type {QueryParameters} */
cancelRefreshTimeout();
const params = Object.assign({}, options, {
search: pageSearchInput.value,
});
const paginationOrFirst =
buildHistoryPage.dataset.pageHasUp === "false" ||
"older-than" in params ||
"newer-than" in params;
// Avoid fetching if the page isn't visible
if (document.hidden) {
return;
}
createRefreshTimeout();
// When we're not on the first page and this is not a load due to pagination
// we need to set the correct value for older-than so we fetch the same set of runs
if (!paginationOrFirst) {
params["older-than"] = (
BigInt(buildHistoryPage.dataset.pageEntryNewest) + 1n
).toString();
}
fetch(ajaxUrl + toQueryString(params)).then((rsp) => {
if (rsp.ok) {
rsp.text().then((responseText) => {
container.classList.remove("app-temporary-list--loading");
pageSearch.classList.remove("jenkins-search--loading");
// Show the 'No builds' text if there are no builds
if (responseText.trim() === "") {
contents.innerHTML = "";
if (params.search) {
noBuilds.style.display = "block";
} else {
noBuildsYet.classList.remove("jenkins-hidden");
card.classList.add("jenkins-hidden");
}
loadingBuilds.style.display = "none";
updateCardControls({
pageHasUp: false,
pageHasDown: false,
pageEntryNewest: false,
pageEntryOldest: false,
});
return;
}
// Show the refreshed builds list
contents.innerHTML = responseText;
if (params.search) {
noBuilds.style.display = "none";
} else {
noBuildsYet.classList.add("jenkins-hidden");
card.classList.remove("jenkins-hidden");
}
loadingBuilds.style.display = "none";
BehaviorShim.applySubtree(contents);
// Show the card controls
const dataset = contents.firstElementChild.dataset;
updateCardControls({
pageHasUp: dataset.pageHasUp === "true",
pageHasDown: dataset.pageHasDown === "true",
pageEntryNewest: dataset.pageEntryNewest,
pageEntryOldest: dataset.pageEntryOldest,
});
});
} else {
console.error(
"Failed to load 'Builds' card, response from API is:",
rsp,
);
}
});
}
/**
* Shows/hides the card's pagination controls depending on the passed parameter
* @param {CardControlsOptions} parameters
*/
function updateCardControls(parameters) {
paginationControls.classList.toggle(
"jenkins-hidden",
!parameters.pageHasUp && !parameters.pageHasDown,
);
paginationPrevious.disabled = !parameters.pageHasUp;
paginationNext.disabled = !parameters.pageHasDown;
buildHistoryPage.dataset.pageEntryNewest = parameters.pageEntryNewest;
buildHistoryPage.dataset.pageEntryOldest = parameters.pageEntryOldest;
buildHistoryPage.dataset.pageHasUp = parameters.pageHasUp;
}
paginationPrevious.addEventListener("click", () => {
load({ "newer-than": buildHistoryPage.dataset.pageEntryNewest });
});
paginationNext.addEventListener("click", () => {
load({ "older-than": buildHistoryPage.dataset.pageEntryOldest });
});
function createRefreshTimeout() {
cancelRefreshTimeout();
buildRefreshTimeout = window.setTimeout(
() => load(),
updateBuildsRefreshInterval,
);
}
function cancelRefreshTimeout() {
if (buildRefreshTimeout) {
window.clearTimeout(buildRefreshTimeout);
buildRefreshTimeout = undefined;
}
}
const debouncedLoad = debounce(() => {
load();
}, 150);
pageSearchInput.addEventListener("input", function () {
container.classList.add("app-temporary-list--loading");
pageSearch.classList.add("jenkins-search--loading");
debouncedLoad();
});
container.classList.add("app-temporary-list--loading");
load();
window.addEventListener("focus", function () {
load();
});
},
);
+245
View File
@@ -194,3 +194,248 @@
}
}
}
// Experimental UI
// Temporary classes will be adjusted and renamed in the future
// Not for external use
.app-temporary-skeleton {
background: var(--button-background);
border: var(--jenkins-border--subtle);
border-radius: var(--form-input-border-radius);
animation: pulse-skeleton 2s both ease-in-out infinite;
}
.app-temporary-list {
display: flex;
flex-direction: column;
border-radius: var(--form-input-border-radius);
background: var(--card-background);
border: var(--jenkins-border);
margin-bottom: var(--section-padding);
&__item {
--tertiary-color: var(--text-color-secondary);
position: relative;
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
transition: var(--standard-transition);
box-shadow:
0 0 0 0 transparent,
0 0 0 0.5rem transparent;
&:not(:last-of-type) {
border-bottom: var(--jenkins-border);
}
&:has(
a.app-temporary-list__item__label:active,
a.app-temporary-list__item__label:focus-visible
) {
border-bottom-color: transparent;
border-radius: calc(var(--form-input-border-radius) / 2);
}
&:has(a.app-temporary-list__item__label) {
&:hover {
--tertiary-color: var(--text-color);
background: var(--button-background);
}
&:has(.app-temporary-list__item__label:active) {
background: var(--button-background--hover);
box-shadow:
0 0 0 var(--jenkins-border-width)
color-mix(
in sRGB,
var(--text-color-secondary) 20%,
var(--card-background)
),
0 0 0 0.34375rem
color-mix(
in sRGB,
color-mix(
in sRGB,
var(--text-color-secondary) 15%,
var(--card-background)
)
65%,
transparent
);
}
&:has(.app-temporary-list__item__label:focus-visible) {
background: var(--button-background--hover);
box-shadow: 0 0 0 0.2rem var(--text-color) !important;
}
}
&:first-of-type {
border-top-left-radius: calc(
var(--form-input-border-radius) - var(--jenkins-border-width)
) !important;
border-top-right-radius: calc(
var(--form-input-border-radius) - var(--jenkins-border-width)
) !important;
}
&:last-of-type {
border-bottom-left-radius: calc(
var(--form-input-border-radius) - var(--jenkins-border-width)
) !important;
border-bottom-right-radius: calc(
var(--form-input-border-radius) - var(--jenkins-border-width)
) !important;
}
&__details {
display: grid;
grid-template-columns: auto 1fr;
gap: 0.875rem;
text-decoration: none !important;
overflow: hidden;
padding: 0.75rem 0.875rem;
outline: none;
}
&__details__text {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
&__actions {
display: flex;
align-items: center;
gap: var(--jenkins-border-width);
padding-inline: 0.875rem;
& > .jenkins-button {
padding: 0;
aspect-ratio: 1;
transition: color var(--standard-transition);
&.jenkins-button--tertiary:not(
[class*="color"],
:focus,
[aria-expanded="true"]
) {
color: var(--tertiary-color) !important;
}
}
&:empty {
display: none;
}
}
&__label {
display: flex;
gap: 0.5rem;
font-weight: var(--font-bold-weight);
margin: 0;
font-variant-numeric: tabular-nums;
color: var(--text-color) !important;
text-decoration: none !important;
outline: none;
&::before {
content: "";
position: absolute;
inset: 0;
background: transparent;
}
}
&__description {
display: flex;
align-items: center;
gap: 0.625rem;
color: var(--text-color-secondary);
white-space: nowrap;
font-size: var(--font-size-xs);
font-weight: normal;
opacity: 0.85;
margin: 3px 0 0;
.jenkins-details {
gap: 0.75rem;
}
.jenkins-details__item {
--item-background--hover: transparent !important;
--item-background--active: transparent !important;
min-height: 0 !important;
padding: 0 !important;
gap: 0.25rem;
.jenkins-details__item__icon {
align-self: center;
height: 1rem !important;
svg,
img {
width: 1rem !important;
height: 1rem !important;
}
}
&::before,
&::after {
inset: -0.25rem !important;
border-radius: 100px !important;
border: none !important;
box-shadow: none !important;
}
}
svg {
width: 1rem;
height: 1rem;
}
&:empty {
display: none;
}
}
}
&__controls {
border-top: var(--jenkins-border);
margin: 0 !important;
padding: 0.5rem !important;
}
}
.app-build-item {
display: grid;
grid-template-columns: auto 1fr auto;
& > * {
overflow: hidden;
}
&__icon {
display: flex;
align-self: start;
margin: 0.75rem 0 0 0.875rem;
z-index: 1;
outline: none;
border-radius: 100px;
transition: var(--standard-transition);
box-shadow: 0 0 0 0.5rem transparent;
&:focus-visible {
box-shadow: 0 0 0 0.2rem var(--text-color) !important;
}
svg {
width: 1.375rem;
height: 1.375rem;
}
}
}
+3
View File
@@ -54,6 +54,9 @@ module.exports = (env, argv) => ({
"pages/project/builds-card": [
path.join(__dirname, "src/main/js/pages/project/builds-card.js"),
],
"pages/project/builds-card-new": [
path.join(__dirname, "src/main/js/pages/project/builds-card-new.js"),
],
"pages/project/build": [
path.join(__dirname, "src/main/js/pages/project/build.js"),
],