diff --git a/core/src/main/resources/hudson/widgets/HistoryWidget/entry.jelly b/core/src/main/resources/hudson/widgets/HistoryWidget/entry.jelly
index 31dbb322fbd0..05f5a8309042 100644
--- a/core/src/main/resources/hudson/widgets/HistoryWidget/entry.jelly
+++ b/core/src/main/resources/hudson/widgets/HistoryWidget/entry.jelly
@@ -57,23 +57,20 @@ THE SOFTWARE.
-
-
diff --git a/test/src/test/java/hudson/model/RunTest.java b/test/src/test/java/hudson/model/RunTest.java
index c59245c78c2c..398791b75f6e 100644
--- a/test/src/test/java/hudson/model/RunTest.java
+++ b/test/src/test/java/hudson/model/RunTest.java
@@ -157,7 +157,7 @@ private void ensureXssIsPrevented(FreeStyleProject upProject, String validationP
HtmlPage htmlPage = wc.goTo(upProject.getUrl());
// trigger the tooltip display
- htmlPage.executeJavaScript("document.querySelector('#buildHistory table .build-badge svg')._tippy.show()");
+ htmlPage.executeJavaScript("document.querySelector('#buildHistory table .run-badge svg')._tippy.show()");
wc.waitForBackgroundJavaScript(500);
ScriptResult result = htmlPage.executeJavaScript("document.querySelector('.tippy-content').innerHTML;");
Object jsResult = result.getJavaScriptResult();
diff --git a/war/src/main/js/filter-build-history.js b/war/src/main/js/filter-build-history.js
index 05fa29d05fae..8b20ef959f36 100644
--- a/war/src/main/js/filter-build-history.js
+++ b/war/src/main/js/filter-build-history.js
@@ -20,9 +20,14 @@ const pageOne = buildHistoryPageNav.querySelector(".pageOne");
const pageUp = buildHistoryPageNav.querySelector(".pageUp");
const pageDown = buildHistoryPageNav.querySelector(".pageDown");
-const leftRightPadding = 4;
+const leftRightPadding = 8; // the left + right padding of a build-row-cell
+const multiLinePadding = 20; // the left padding of the second/third line
+const tabletBreakpoint = 900; // the breakpoint between tablet view and normal view,
+// keep in sync with _breakpoints.scss
const updateBuildsRefreshInterval = 5000;
+let lastClientWidth = 0;
+
function updateBuilds(params) {
if (isPageVisible()) {
fetch(ajaxUrl + toQueryString(params), {
@@ -169,12 +174,23 @@ function togglePageUpDown() {
}
}
-function checkRowCellOverflows(row) {
+/*
+ * Arranges name, details (timestamp) and the badges for a build
+ * so that it makes best use of the limited available space.
+ * There are 6 possibilities how the parts can be arranged
+ * 1. put everything in one row, with the name having a fixed width so that details are aligned.
+ * 2. put name and badges in first row, details in first row
+ * 3. put name in first row, details and badges in second row
+ * 4. put name and details in first row, badges in second row
+ * 5. put everything in separate rows
+ * 6. there are no badges and name and details don't fit in one row
+ */
+function checkRowCellOverflows(row, recalculate = false) {
if (!row) {
return;
}
- if (row.classList.contains("overflow-checked")) {
+ if (row.classList.contains("overflow-checked") && !recalculate) {
// already done.
return;
}
@@ -195,9 +211,6 @@ function checkRowCellOverflows(row) {
var div = document.createElement("div");
div.classList.add("block");
- div.classList.add("wrap");
- el1.classList.add("wrapped");
- el2.classList.add("wrapped");
el1.parentNode.insertBefore(div, el1);
el1.parentNode.removeChild(el1);
@@ -207,40 +220,53 @@ function checkRowCellOverflows(row) {
return div;
}
- function blockUnwrap(element) {
- element.querySelectorAll(".wrapped").forEach(function (wrappedEl) {
- wrappedEl.parentNode.removeChild(wrappedEl);
- element.parentNode.insertBefore(wrappedEl, element);
- wrappedEl.classList.remove("wrapped");
- });
- element.parentNode.removeChild(element);
- }
- var buildName = row.querySelector(".build-name");
- var buildDetails = row.querySelector(".build-details");
-
- if (!buildName || !buildDetails) {
+ var cell = row.querySelector(".build-row-cell");
+ if (!cell) {
return;
}
+ var buildName = cell.querySelector(".build-name");
+ var buildDetails = cell.querySelector(".build-details");
+ var insertDiv = cell.querySelector(".left-bar");
+ var desc = cell.querySelector(".desc");
+ if (desc !== null) {
+ insertDiv = desc;
+ }
- var buildControls = row.querySelector(".build-controls");
- var desc = row.querySelector(".desc");
+ var buildBadges = row.querySelector(".build-badges");
+ if (buildBadges && buildBadges.childElementCount === 0) {
+ buildBadges.remove();
+ buildBadges = null;
+ }
function resetCellOverflows() {
markSingleline();
- // undo block wraps
- row.querySelectorAll(".block.wrap").forEach(function (blockWrap) {
- blockUnwrap(blockWrap);
- });
-
+ cell.insertBefore(buildName, insertDiv);
+ cell.insertBefore(buildDetails, insertDiv);
+ if (buildBadges) {
+ cell.insertBefore(buildBadges, insertDiv);
+ }
buildName.classList.remove("block");
+ buildName.classList.remove("block");
+ buildName.classList.remove("indent-multiline");
buildName.removeAttribute("style");
buildDetails.classList.remove("block");
buildDetails.removeAttribute("style");
- if (buildControls) {
- buildControls.classList.remove("block");
- buildDetails.removeAttribute("style");
+ buildDetails.classList.remove("indent-multiline");
+ if (buildBadges) {
+ buildBadges.classList.remove("block");
+ buildBadges.removeAttribute("style");
+ buildBadges.classList.remove("indent-multiline");
+ }
+ const nameBadges = cell.querySelector(".build-name-badges");
+ if (nameBadges) {
+ nameBadges.remove();
+ }
+
+ const detailsBadges = cell.querySelector(".build-details-badges");
+ if (detailsBadges) {
+ detailsBadges.remove();
}
}
@@ -252,208 +278,166 @@ function checkRowCellOverflows(row) {
markMultiline();
}
+ //
+ function getElementOverflowData(element, width) {
+ // First we force it to wrap so we can get those dimension.
+ // Then we force it to "nowrap", so we can get those dimension.
+ // We can then compare the two sets, which will indicate if
+ // wrapping is potentially happening, or not.
+ // The scrollWidth is calculated based on the content and not the actual
+ // width of the element
+
+ // Force it to wrap.
+ const oldWidth = element.style.width;
+ element.style.width = width + "px";
+ element.classList.add("force-wrap");
+ var wrappedClientHeight = element.clientHeight;
+ element.classList.remove("force-wrap");
+
+ // Force it to nowrap. Return the comparisons.
+ element.classList.add("force-nowrap");
+ element.style.width = "fit-content";
+ var nowrapClientHeight = element.clientHeight;
+ try {
+ var overflowParams = {
+ element: element,
+ scrollWidth: element.scrollWidth + 5, // 1 for rounding + 4 for left/right padding
+ isOverflowed: wrappedClientHeight > nowrapClientHeight,
+ };
+ return overflowParams;
+ } finally {
+ element.classList.remove("force-nowrap");
+ element.style.width = oldWidth;
+ }
+ }
+
+ // eslint-disable-next-line no-inner-declarations
+ function expandLeftWithRight(
+ leftCellOverFlowParams,
+ rightCellOverflowParams,
+ ) {
+ // Float them left and right...
+ leftCellOverFlowParams.element.style.float = "left";
+ rightCellOverflowParams.element.style.float = "right";
+
+ leftCellOverFlowParams.element.style.width =
+ leftCellOverFlowParams.scrollWidth + "px";
+ rightCellOverflowParams.element.style.width =
+ rightCellOverflowParams.scrollWidth + "px";
+ }
+
var rowWidth = buildHistoryContainer.clientWidth;
var usableRowWidth = rowWidth - leftRightPadding * 2;
- var nameOverflowParams = getElementOverflowParams(buildName);
- var detailsOverflowParams = getElementOverflowParams(buildDetails);
- var controlsOverflowParams;
- if (buildControls) {
- controlsOverflowParams = getElementOverflowParams(buildControls);
- }
+ let nameWidth = usableRowWidth * 0.32;
+ let detailsWidth = usableRowWidth * 0.5;
+ let badgesWidth = usableRowWidth * 0.18;
- function fitToControlsHeight(element) {
- if (buildControls) {
- if (element.clientHeight < buildControls.clientHeight) {
- element.style.height = buildControls.clientHeight.toString() + "px";
- }
- }
+ var nameOverflowParams = getElementOverflowData(buildName, nameWidth);
+ var detailsOverflowParams = getElementOverflowData(
+ buildDetails,
+ detailsWidth,
+ );
+ var badgesOverflowParams;
+ if (buildBadges) {
+ badgesOverflowParams = getElementOverflowData(buildBadges, badgesWidth);
+ } else {
+ badgesOverflowParams = {
+ element: null,
+ scrollWidth: 0,
+ isOverflowed: false,
+ };
}
- function setBuildControlWidths() {
- if (buildControls) {
- var buildBadge = buildControls.querySelector(".build-badge");
-
- if (buildBadge) {
- var buildControlsWidth = buildControls.clientWidth;
- var buildBadgeWidth;
-
- var buildStop = buildControls.querySelector(".build-stop");
- if (buildStop) {
- buildStop.style.width = "24px";
- // Minus 24 for the buildStop width,
- // minus 4 for left+right padding in the controls container
- buildBadgeWidth = buildControlsWidth - 24 - leftRightPadding;
- if (buildControls.classList.contains("indent-multiline")) {
- buildBadgeWidth = buildBadgeWidth - 20;
- }
- buildBadge.style.width = buildBadgeWidth + "px";
- } else {
- buildBadge.style.width = "100%";
- }
- }
- controlsOverflowParams = getElementOverflowParams(buildControls);
- }
+ function setBuildBadgesWidths() {
+ buildBadges.style.width = "100%";
}
- setBuildControlWidths();
-
- var controlsRepositioned = false;
-
- if (nameOverflowParams.isOverflowed || detailsOverflowParams.isOverflowed) {
- // At least one of the cells (name or details) needs to move to a row of its own.
+ if (
+ !nameOverflowParams.isOverflowed &&
+ nameWidth +
+ detailsOverflowParams.scrollWidth +
+ badgesOverflowParams.scrollWidth <
+ usableRowWidth
+ ) {
+ // Everything fits in one row
+ buildDetails.style.width = "fit-content";
+ if (buildBadges) {
+ buildBadges.style.float = "right";
+ buildBadges.style.width = "fit-content";
+ }
+ } else {
markMultiline();
-
- if (buildControls) {
- // We have build controls. Lets see can we find a combination that allows the build controls
+ if (buildBadges) {
+ // We have build badges. Lets see can we find a combination that allows the build badges
// to sit beside either the build name or the build details.
- var badgesOverflowing = false;
- var nameLessThanHalf = true;
- var detailsLessThanHalf = true;
- var buildBadge = buildControls.querySelector(".build-badge");
- if (buildBadge) {
- var badgeOverflowParams = getElementOverflowParams(buildBadge);
-
- if (badgeOverflowParams.isOverflowed) {
- // The badges are also overflowing. In this case, we will only attempt to
- // put the controls on the same line as the name or details (see below)
- // if the name or details is using less than half the width of the build history
- // widget.
- badgesOverflowing = true;
- nameLessThanHalf =
- nameOverflowParams.scrollWidth < usableRowWidth / 2;
- detailsLessThanHalf =
- detailsOverflowParams.scrollWidth < usableRowWidth / 2;
- }
- }
- // eslint-disable-next-line no-inner-declarations
- function expandLeftWithRight(
- leftCellOverFlowParams,
- rightCellOverflowParams,
- ) {
- // Float them left and right...
- leftCellOverFlowParams.element.style.float = "left";
- rightCellOverflowParams.element.style.float = "right";
-
- if (
- !leftCellOverFlowParams.isOverflowed &&
- !rightCellOverflowParams.isOverflowed
- ) {
- // If neither left nor right are overflowed, just leave as is and let them float left and right.
- return;
- }
- if (
- leftCellOverFlowParams.isOverflowed &&
- !rightCellOverflowParams.isOverflowed
- ) {
- leftCellOverFlowParams.element.style.width =
- leftCellOverFlowParams.scrollWidth + "px";
- return;
- }
- if (
- !leftCellOverFlowParams.isOverflowed &&
- rightCellOverflowParams.isOverflowed
- ) {
- rightCellOverflowParams.element.style.width =
- rightCellOverflowParams.scrollWidth + "px";
- return;
- }
- }
-
if (
- (!badgesOverflowing || nameLessThanHalf) &&
- nameOverflowParams.scrollWidth + controlsOverflowParams.scrollWidth <=
- usableRowWidth
+ nameOverflowParams.scrollWidth + badgesOverflowParams.scrollWidth <=
+ usableRowWidth
) {
- // Build name and controls can go on one row (first row). Need to move build details down
+ // Build name and badges can go on one row (first row). Need to move build details down
// to a row of its own (second row) by making it a block element, forcing it to wrap. If there
- // are controls, we move them up to position them after the build name by inserting before the
+ // are badges, we move them up to position them after the build name by inserting before the
// build details.
buildDetails.classList.add("block");
- buildControls.parentNode.removeChild(buildControls);
- buildDetails.parentNode.insertBefore(buildControls, buildDetails);
- var wrap = blockWrap(buildName, buildControls);
- wrap.classList.add("build-name-controls");
+ buildBadges.parentNode.removeChild(buildBadges);
+ buildDetails.parentNode.insertBefore(buildBadges, buildDetails);
+ var wrap = blockWrap(buildName, buildBadges);
+ wrap.classList.add("build-name-badges");
indentMultiline(buildDetails);
- nameOverflowParams = getElementOverflowParams(buildName); // recalculate
- expandLeftWithRight(nameOverflowParams, controlsOverflowParams);
- setBuildControlWidths();
- fitToControlsHeight(buildName);
+ expandLeftWithRight(nameOverflowParams, badgesOverflowParams);
} else if (
- (!badgesOverflowing || detailsLessThanHalf) &&
detailsOverflowParams.scrollWidth +
- controlsOverflowParams.scrollWidth <=
- usableRowWidth
+ badgesOverflowParams.scrollWidth +
+ multiLinePadding <=
+ usableRowWidth
) {
- // Build details and controls can go on one row. Need to make the
- // build name (first field) a block element, forcing the details and controls to wrap
+ // Build details and badges can go on one row. Need to make the
+ // build name (first field) a block element, forcing the details and badges to wrap
// onto the next row (creating a second row).
buildName.classList.add("block");
- wrap = blockWrap(buildDetails, buildControls);
+ wrap = blockWrap(buildDetails, buildBadges);
indentMultiline(wrap);
- wrap.classList.add("build-details-controls");
- detailsOverflowParams = getElementOverflowParams(buildDetails); // recalculate
- expandLeftWithRight(detailsOverflowParams, controlsOverflowParams);
- setBuildControlWidths();
- fitToControlsHeight(buildDetails);
+ wrap.classList.add("build-details-badges");
+ expandLeftWithRight(detailsOverflowParams, badgesOverflowParams);
+ } else if (
+ !nameOverflowParams.isOverflowed &&
+ nameWidth + detailsOverflowParams.scrollWidth < usableRowWidth
+ ) {
+ // Build name and details can go on one row. Make badges take full row
+ // it goes on separate row
+ indentMultiline(buildBadges);
+ setBuildBadgesWidths();
} else {
// No suitable combo fits on a row. All need to go on rows of their own.
buildName.classList.add("block");
buildDetails.classList.add("block");
- buildControls.classList.add("block");
+ buildBadges.classList.add("block");
indentMultiline(buildDetails);
- indentMultiline(buildControls);
- nameOverflowParams = getElementOverflowParams(buildName); // recalculate
- detailsOverflowParams = getElementOverflowParams(buildDetails); // recalculate
- setBuildControlWidths();
+ indentMultiline(buildBadges);
+ setBuildBadgesWidths();
}
- controlsRepositioned = true;
} else {
- buildName.classList.add("block");
- buildDetails.classList.add("block");
+ // name and details don't fit in one row
indentMultiline(buildDetails);
+ buildName.classList.add("block");
}
}
- if (buildControls && !controlsRepositioned) {
- buildBadge = buildControls.querySelector(".build-badge");
- if (buildBadge) {
- badgeOverflowParams = getElementOverflowParams(buildBadge);
-
- if (badgeOverflowParams.isOverflowed) {
- markMultiline();
- indentMultiline(buildControls);
- buildControls.classList.add("block");
- controlsRepositioned = true;
- setBuildControlWidths();
- }
- }
- }
-
- if (
- !nameOverflowParams.isOverflowed &&
- !detailsOverflowParams.isOverflowed &&
- !controlsRepositioned
- ) {
- fitToControlsHeight(buildName);
- fitToControlsHeight(buildDetails);
- }
-
row.classList.add("overflow-checked");
}
-function checkAllRowCellOverflows() {
+function checkAllRowCellOverflows(recalculate = false) {
if (isRunAsTest) {
return;
}
-
var dataTable = getDataTable(buildHistoryContainer);
- var rows = dataTable.rows;
+ var rows = dataTable.getElementsByClassName("build-row");
for (var i = 0; i < rows.length; i++) {
var row = rows[i];
- checkRowCellOverflows(row);
+ checkRowCellOverflows(row, recalculate);
}
}
@@ -524,6 +508,31 @@ function loadPage(params, focusOnSearch) {
});
}
+const handleResize = function () {
+ checkAllRowCellOverflows(true);
+};
+
+const debouncedResizer = debounce(handleResize, 500);
+
+addEventListener("resize", function () {
+ const newClientWidth = document.body.clientWidth;
+ // the sidepanel has 2 sizes depending on the clientWidth
+ // > tabletBreakpoint: the sidepanel has fixed width
+ // <= tabletBreakpoint: the sidepanel takes the complete width
+ if (
+ lastClientWidth > tabletBreakpoint &&
+ newClientWidth > tabletBreakpoint &&
+ lastClientWidth != newClientWidth
+ ) {
+ // we're in a range of the clientWidth were changes do not affect the layout
+ // or the width hasn't changed.
+ lastClientWidth = newClientWidth;
+ return;
+ }
+ lastClientWidth = newClientWidth;
+ debouncedResizer();
+});
+
const handleFilter = function () {
loadPage({}, true);
};
@@ -531,6 +540,7 @@ const handleFilter = function () {
const debouncedFilter = debounce(handleFilter, 300);
document.addEventListener("DOMContentLoaded", function () {
+ lastClientWidth = document.body.clientWidth;
// Apply correct styling upon filter bar text change, call API after wait
if (pageSearchInput !== null) {
pageSearchInput.addEventListener("input", function () {
diff --git a/war/src/main/scss/components/_side-panel-widgets.scss b/war/src/main/scss/components/_side-panel-widgets.scss
index bd6715cdc586..4aa214cdb297 100644
--- a/war/src/main/scss/components/_side-panel-widgets.scss
+++ b/war/src/main/scss/components/_side-panel-widgets.scss
@@ -116,20 +116,26 @@
width: 50%;
}
-.build-row-cell .pane.build-controls {
+.build-row-cell .pane.build-badges {
width: 18%;
- text-align: right;
+ display: inline-flex !important;
+ justify-content: end;
+ align-items: center;
+ gap: 2px;
+ flex-flow: row-reverse wrap-reverse;
}
.build-row-cell .pane.build-details.block {
width: 100%;
+ display: block;
}
.build-row.multi-line .build-row-cell .pane.build-name.block {
width: 100%;
+ display: block;
}
-.build-row-cell .pane.build-controls.block {
+.build-row-cell .pane.build-badges.block {
width: 100%;
}
@@ -144,37 +150,33 @@
z-index: 1;
}
-.build-row-cell .build-stop {
- display: inline-block;
- width: 30%;
-}
-
-.build-row-cell .build-badge {
- display: inline-block;
- text-align: right;
- width: 70%;
- padding: 2px 0;
-}
-
-.build-row-cell .build-badge > span {
+.build-row-cell .build-badges > span {
display: inline-block;
max-width: 256px;
padding: 0 1px;
overflow: hidden;
}
-.build-row-cell .build-badge > span + span {
+.build-row-cell .build-badges > span + span {
margin: 0 0 0 2px !important;
}
@media (width >= 1170px) {
- .build-row-cell .build-badge > span {
+ .build-row-cell .build-badges > span {
max-width: 296px;
}
}
-.build-row .build-name-controls .pane.build-name,
-.build-row .build-details-controls .pane.build-details {
+.build-row-cell .build-badges > .run-badge {
+ display: inline-flex;
+}
+
+.build-row-cell .build-badges > .run-badge > a {
+ display: inline-flex;
+}
+
+.build-row .build-name-badges .pane.build-name,
+.build-row .build-details-badges .pane.build-details {
width: 70%;
}
@@ -186,7 +188,6 @@
}
.build-row.multi-line .build-row-cell .block {
- display: block;
overflow: auto;
}
diff --git a/war/src/main/webapp/scripts/hudson-behavior.js b/war/src/main/webapp/scripts/hudson-behavior.js
index ca068e55e272..906358d1d15d 100644
--- a/war/src/main/webapp/scripts/hudson-behavior.js
+++ b/war/src/main/webapp/scripts/hudson-behavior.js
@@ -2232,6 +2232,8 @@ function getElementOverflowParams(element) {
// Then we force it to "nowrap", so we can get those dimension.
// We can then compare the two sets, which will indicate if
// wrapping is potentially happening, or not.
+ // The scrollWidth is calculated based on the content and not the actual
+ // width of the element
// Force it to wrap.
element.classList.add("force-wrap");
@@ -2241,6 +2243,8 @@ function getElementOverflowParams(element) {
// Force it to nowrap. Return the comparisons.
element.classList.add("force-nowrap");
+ const oldWidth = element.style.width;
+ element.style.width = "fit-content";
var nowrapClientHeight = element.clientHeight;
try {
var overflowParams = {
@@ -2252,6 +2256,7 @@ function getElementOverflowParams(element) {
return overflowParams;
} finally {
element.classList.remove("force-nowrap");
+ element.style.width = oldWidth;
}
}