mirror of
https://github.com/jenkinsci/jenkins.git
synced 2026-10-26 19:15:09 +00:00
Add initial implementation of new builds list for experimental Job UI (#27189)
This commit is contained in:
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();
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"),
|
||||
],
|
||||
|
||||
Reference in new issue
Block a user