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/core/src/main/resources/jenkins/widgets/HistoryPageFilter/queue-items.jelly b/core/src/main/resources/jenkins/widgets/HistoryPageFilter/queue-items.jelly index 74c5535be819..14afdf30a131 100644 --- a/core/src/main/resources/jenkins/widgets/HistoryPageFilter/queue-items.jelly +++ b/core/src/main/resources/jenkins/widgets/HistoryPageFilter/queue-items.jelly @@ -62,12 +62,12 @@ 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; } }