diff --git a/core/src/main/resources/hudson/widgets/BuildHistoryWidget/entries.jelly b/core/src/main/resources/hudson/widgets/BuildHistoryWidget/entries.jelly index 80eaf3cbfd01555371f12e1dbc784533895d7a70..f84e541e08512e16f31213845e9c4a58784fc6bd 100644 --- a/core/src/main/resources/hudson/widgets/BuildHistoryWidget/entries.jelly +++ b/core/src/main/resources/hudson/widgets/BuildHistoryWidget/entries.jelly @@ -33,30 +33,38 @@ THE SOFTWARE. - - - - - - #${queuedItems.size()==1 ? it.owner.nextBuildNumber - : it.owner.nextBuildNumber+queuedItems.size()-i-1} - - -
- - - + + +
+
+ +
+ +
+ #${queuedItems.size()==1 ? it.owner.nextBuildNumber + : it.owner.nextBuildNumber+queuedItems.size()-i-1} +
+
+
+ + + + (${%pending}—) + + + (${%pending}) + + + ${item.params} +
+
+
+ + + +
- - - - (${%pending}—) - - - (${%pending}) - - - ${item.params} +
diff --git a/core/src/main/resources/hudson/widgets/HistoryWidget/entry.jelly b/core/src/main/resources/hudson/widgets/HistoryWidget/entry.jelly index 8ef23a9da0409aa5bb5b22e3125fe5d018ed9ac1..2283a6603a1688bec335825d999c6ec06ddaa9ab 100644 --- a/core/src/main/resources/hudson/widgets/HistoryWidget/entry.jelly +++ b/core/src/main/resources/hudson/widgets/HistoryWidget/entry.jelly @@ -29,41 +29,48 @@ THE SOFTWARE. - - - - ${build.displayName} - - - - - - - - - + + + +
+ + + + + + + +
+
+
+ + + + + + + +
+ +
+ + + + +
+
+
-
+
- - - - - - - - - - - - - - - - - +
diff --git a/core/src/main/resources/hudson/widgets/HistoryWidget/index.jelly b/core/src/main/resources/hudson/widgets/HistoryWidget/index.jelly index e545de38d0acc86fa2bde04460dbb58e3f062e65..f43ba940688401f2bda57cb516e1c844a4497ba6 100644 --- a/core/src/main/resources/hudson/widgets/HistoryWidget/index.jelly +++ b/core/src/main/resources/hudson/widgets/HistoryWidget/index.jelly @@ -88,6 +88,7 @@ THE SOFTWARE. newhist.headers = hist.headers replace(hist, newhist); + fireBuildHistoryChanged(); } }); } diff --git a/war/src/main/webapp/css/style.css b/war/src/main/webapp/css/style.css index 701352dc2488ecd06251f3d8b5af36c94e367fb7..27eb7ff9445cd6cec7dd97e9eaf221afcfacaf9b 100644 --- a/war/src/main/webapp/css/style.css +++ b/war/src/main/webapp/css/style.css @@ -441,10 +441,6 @@ pre.console { width: 480px; } -.build-row { - padding: 3px 4px 3px 4px; -} - .build-keep { font-weight: bold; } @@ -934,18 +930,140 @@ table.parameters > tbody:hover { } #buildHistory .desc { + position: relative; padding: 0; margin-top: 5px; white-space: normal; - max-width: 200px; + opacity: 0.6; + font-style: italic; +} + +#buildHistory .build-row-cell { + position: relative; } #buildHistory .build-rss-links { float: right; } -#buildHistory .build-name { - max-width: 120px; +.build-row { + padding: 3px 4px 3px 4px; +} + +.build-row:hover { + background: #e8e8e8 !important; +} + +.build-row-cell { + font-size: 12px; +} + +.build-row-cell .pane.build-name { + width: 20%; + font-weight: bold; +} +.build-row-cell .pane.build-details { + width: 50%; +} +.build-row-cell .pane.build-controls { + width: 30%; + text-align: right; +} +.build-row-cell .pane.build-details.block { + width: 100%; +} + +.pane.build-name a, +.pane.build-name a:visited { + color: black; + text-decoration: underline; +} + +.pane.build-details a, +.pane.build-details a:visited { + color: black; + opacity: 0.6; + text-decoration: none; +} +.pane.build-details a:hover { + opacity: 1.0; + text-decoration: underline; +} + +.build-row.multi-line .build-row-cell .pane.build-name.block { + width: 100%; +} +.build-row-cell .pane.build-controls.block { + width: 100%; +} + +.build-row-cell .pane.build-name .build-icon, +.build-row-cell .pane.build-name .display-name { + display: inline-block; +} +.build-row-cell .pane.build-name .build-icon { + position: absolute; +} + +.build-row-cell .build-stop { + display: inline-block; + width: 30%; +} +.build-row-cell .build-badge { + display: inline-block; + padding: 2px; + width: 70%; + text-align: right; +} +.build-row .build-name-controls .pane.build-name, +.build-row .build-details-controls .pane.build-details { + width: 70%; +} + +.build-row .build-row-cell .pane { + padding: 0px 2px; /* Sync changes with func expandControlsTo50Percent in hudson-behavior.js */ + display: inline-block; + overflow: hidden; +} + +.build-row.multi-line .build-row-cell .block { + display: block; + overflow: auto; +} + +.build-row.multi-line .build-row-cell .indent-multiline { + margin-top: 5px; +} + +.build-row.multi-line .build-row-cell .left-bar { + position: absolute; + top: 19px; + bottom: 3px; + left: 14px; + border-left: 1px solid #cdcdcd; +} + +.build-row-cell .pane.build-name .display-name, +.build-row-cell .indent-multiline { + padding-left: 20px !important; /* Sync changes with func expandControlsTo50Percent in hudson-behavior.js */ +} + +.build-row .build-row-cell { + visibility: hidden; +} +.build-row.overflow-checked .build-row-cell { + visibility: visible; +} + +/* ================ Element overflow calculation helper styles ================ */ + +.force-wrap, .force-wrap a { + white-space: normal !important; + overflow: visible !important; +} +.force-nowrap, .force-nowrap a { + white-space: nowrap !important; + overflow: hidden !important; } /* ========================= editable combobox style ========================= */ diff --git a/war/src/main/webapp/scripts/hudson-behavior.js b/war/src/main/webapp/scripts/hudson-behavior.js index 1bf29852a860a9cb0fc407a5616c1f2446a802f0..b62f360da2e4b63a2ba989f3f9bc7aee62b19e36 100644 --- a/war/src/main/webapp/scripts/hudson-behavior.js +++ b/war/src/main/webapp/scripts/hudson-behavior.js @@ -1552,21 +1552,324 @@ Form.findMatchingInput = function(base, name) { return null; // not found } +function onBuildHistoryChange(handler) { + Event.observe(window, 'jenkins:buildHistoryChanged', handler); +} +function fireBuildHistoryChanged() { + Event.fire(window, 'jenkins:buildHistoryChanged'); +} + function updateBuildHistory(ajaxUrl,nBuild) { if(isRunAsTest) return; - $('buildHistory').headers = ["n",nBuild]; + var bh = $('buildHistory'); + + bh.headers = ["n",nBuild]; + + function getDataTable(buildHistoryDiv) { + return $(buildHistoryDiv).getElementsBySelector('table.pane')[0]; + } + + var leftRightPadding = 4; + function checkRowCellOverflows(row) { + if (!row) { + return; + } + + if (Element.hasClassName(row, "overflow-checked")) { + // already done. + return; + } + + function markSingleline() { + Element.addClassName(row, "single-line"); + Element.removeClassName(row, "multi-line"); + } + function markMultiline() { + Element.removeClassName(row, "single-line"); + Element.addClassName(row, "multi-line"); + } + function indentMultiline(element) { + Element.addClassName(element, "indent-multiline"); + } + + function blockWrap(el1, el2) { + var div = document.createElement('div'); + + Element.addClassName(div, "block"); + Element.addClassName(div, "wrap"); + Element.addClassName(el1, "wrapped"); + Element.addClassName(el2, "wrapped"); + + el1.parentNode.insertBefore(div, el1); + el1.parentNode.removeChild(el1); + el2.parentNode.removeChild(el2); + div.appendChild(el1); + div.appendChild(el2); + + return div; + } + function blockUnwrap(element) { + var wrapped = $(element).getElementsBySelector('.wrapped'); + for (var i = 0; i < wrapped.length; i++) { + var wrappedEl = wrapped[i]; + wrappedEl.parentNode.removeChild(wrappedEl); + element.parentNode.insertBefore(wrappedEl, element); + Element.removeClassName(wrappedEl, "wrapped"); + } + element.parentNode.removeChild(element); + } + + var buildName = $(row).getElementsBySelector('.build-name')[0]; + var buildDetails = $(row).getElementsBySelector('.build-details')[0]; + + if (!buildName || !buildDetails) { + return; + } + + var displayName = $(buildName).getElementsBySelector('.display-name')[0]; + var buildControls = $(row).getElementsBySelector('.build-controls')[0]; + var desc; + + var descElements = $(row).getElementsBySelector('.desc'); + if (descElements.length > 0) { + desc = descElements[0]; + } + + function resetCellOverflows() { + markSingleline(); + + // undo block wraps + var blockWraps = $(row).getElementsBySelector('.block.wrap'); + for (var i = 0; i < blockWraps.length; i++) { + blockUnwrap(blockWraps[i]); + } + removeZeroWidthSpaces(displayName); + removeZeroWidthSpaces(desc); + Element.removeClassName(buildName, "block"); + buildName.removeAttribute('style'); + Element.removeClassName(buildDetails, "block"); + buildDetails.removeAttribute('style'); + if (buildControls) { + Element.removeClassName(buildControls, "block"); + buildDetails.removeAttribute('style'); + } + } + + // Undo everything from the previous poll. + resetCellOverflows(); + + // Insert zero-width spaces so as to allow text to wrap, allowing us to get the true clientWidth. + insertZeroWidthSpaces(displayName, 2); + if (desc) { + insertZeroWidthSpaces(desc, 30); + markMultiline(); + } + + var rowWidth = bh.clientWidth; + var usableRowWidth = rowWidth - (leftRightPadding * 2); + var nameOverflowParams = getElementOverflowParams(buildName); + var detailsOverflowParams = getElementOverflowParams(buildDetails); + + var controlsOverflowParams; + if (buildControls) { + controlsOverflowParams = getElementOverflowParams(buildControls); + } + + if (nameOverflowParams.isOverflowed) { + // If the name is overflowed, lets remove the zero-width spaces we added above and + // re-add zero-width spaces with a bigger max word sizes. + removeZeroWidthSpaces(displayName); + insertZeroWidthSpaces(displayName, 20); + } + + function fitToControlsHeight(element) { + if (buildControls) { + if (element.clientHeight < buildControls.clientHeight) { + $(element).setStyle({height: buildControls.clientHeight.toString() + 'px'}); + } + } + } + + function setBuildControlWidths() { + if (buildControls) { + var buildBadge = $(buildControls).getElementsBySelector('.build-badge')[0]; + + if (buildBadge) { + var buildControlsWidth = buildControls.clientWidth; + var buildBadgeWidth; + + var buildStop = $(buildControls).getElementsBySelector('.build-stop')[0]; + if (buildStop) { + $(buildStop).setStyle({width: '24px'}); + // Minus 24 for the buildStop width, + // minus 4 for left+right padding in the controls container + buildBadgeWidth = (buildControlsWidth - 24 - leftRightPadding); + if (Element.hasClassName(buildControls, "indent-multiline")) { + buildBadgeWidth = buildBadgeWidth - 20; + } + $(buildBadge).setStyle({width: (buildBadgeWidth) + 'px'}); + } else { + $(buildBadge).setStyle({width: '100%'}); + } + } + controlsOverflowParams = getElementOverflowParams(buildControls); + } + } + 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. + + markMultiline(); + + if (buildControls) { + + // We have build controls. Lets see can we find a combination that allows the build controls + // to sit beside either the build name or the build details. + + var badgesOverflowing = false; + var nameLessThanHalf = true; + var detailsLessThanHalf = true; + var buildBadge = $(buildControls).getElementsBySelector('.build-badge')[0]; + 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); + } + } + function expandLeftWithRight(leftCellOverFlowParams, rightCellOverflowParams) { + // Float them left and right... + $(leftCellOverFlowParams.element).setStyle({float: 'left'}); + $(rightCellOverflowParams.element).setStyle({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).setStyle({width: leftCellOverFlowParams.scrollWidth + 'px'}); + return; + } + if (!leftCellOverFlowParams.isOverflowed && rightCellOverflowParams.isOverflowed) { + $(rightCellOverflowParams.element).setStyle({width: rightCellOverflowParams.scrollWidth + 'px'}); + return; + } + } + + if ((!badgesOverflowing || nameLessThanHalf) && + (nameOverflowParams.scrollWidth + controlsOverflowParams.scrollWidth <= usableRowWidth)) { + // Build name and controls 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 + // build details. + Element.addClassName(buildDetails, "block"); + buildControls.parentNode.removeChild(buildControls); + buildDetails.parentNode.insertBefore(buildControls, buildDetails); + var wrap = blockWrap(buildName, buildControls); + Element.addClassName(wrap, "build-name-controls"); + indentMultiline(buildDetails); + nameOverflowParams = getElementOverflowParams(buildName); // recalculate + expandLeftWithRight(nameOverflowParams, controlsOverflowParams); + setBuildControlWidths(); + fitToControlsHeight(buildName); + } else if ((!badgesOverflowing || detailsLessThanHalf) && + (detailsOverflowParams.scrollWidth + controlsOverflowParams.scrollWidth <= 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 + // onto the next row (creating a second row). + Element.addClassName(buildName, "block"); + var wrap = blockWrap(buildDetails, buildControls); + indentMultiline(wrap); + Element.addClassName(wrap, "build-details-controls"); + $(displayName).setStyle({width: '100%'}); + detailsOverflowParams = getElementOverflowParams(buildDetails); // recalculate + expandLeftWithRight(detailsOverflowParams, controlsOverflowParams); + setBuildControlWidths(); + fitToControlsHeight(buildDetails); + } else { + // No suitable combo fits on a row. All need to go on rows of their own. + Element.addClassName(buildName, "block"); + Element.addClassName(buildDetails, "block"); + Element.addClassName(buildControls, "block"); + indentMultiline(buildDetails); + indentMultiline(buildControls); + nameOverflowParams = getElementOverflowParams(buildName); // recalculate + detailsOverflowParams = getElementOverflowParams(buildDetails); // recalculate + setBuildControlWidths(); + } + controlsRepositioned = true; + } else { + Element.addClassName(buildName, "block"); + Element.addClassName(buildDetails, "block"); + indentMultiline(buildDetails); + } + } + + if (buildControls && !controlsRepositioned) { + var buildBadge = $(buildControls).getElementsBySelector('.build-badge')[0]; + if (buildBadge) { + var badgeOverflowParams = getElementOverflowParams(buildBadge); + + if (badgeOverflowParams.isOverflowed) { + markMultiline(); + indentMultiline(buildControls); + Element.addClassName(buildControls, "block"); + controlsRepositioned = true; + setBuildControlWidths(); + } + } + } + + if (!nameOverflowParams.isOverflowed && !detailsOverflowParams.isOverflowed && !controlsRepositioned) { + fitToControlsHeight(buildName); + fitToControlsHeight(buildDetails); + } + + Element.addClassName(row, "overflow-checked"); + } + + function checkAllRowCellOverflows() { + if(isRunAsTest) { + return; + } + + var bh = $('buildHistory'); + var dataTable = getDataTable(bh); + var rows = dataTable.rows; + + // Insert zero-width spaces in text that may cause overflow distortions. + var displayNames = $(bh).getElementsBySelector('.display-name'); + for (var i = 0; i < displayNames.length; i++) { + insertZeroWidthSpaces(displayNames[i], 2); + } + var descriptions = $(bh).getElementsBySelector('.desc'); + for (var i = 0; i < descriptions.length; i++) { + insertZeroWidthSpaces(descriptions[i], 30); + } + + for (var i = 0; i < rows.length; i++) { + var row = rows[i]; + checkRowCellOverflows(row); + } + } + + var updateBuildsRefreshInterval = 5000; function updateBuilds() { if(isPageVisible()){ - var bh = $('buildHistory'); if (bh.headers == null) { // Yahoo.log("Missing headers in buildHistory element"); } - function getDataTable(buildHistoryDiv) { - return $(buildHistoryDiv).getElementsBySelector('table.pane')[0]; - } - new Ajax.Request(ajaxUrl, { requestHeaders: bh.headers, onSuccess: function(rsp) { @@ -1598,17 +1901,128 @@ function updateBuildHistory(ajaxUrl,nBuild) { // next update bh.headers = ["n",rsp.getResponseHeader("n")]; - window.setTimeout(updateBuilds, 5000); + window.setTimeout(updateBuilds, updateBuildsRefreshInterval); + + checkAllRowCellOverflows(); } }); } else { // Reschedule again - window.setTimeout(updateBuilds, 5000); + window.setTimeout(updateBuilds, updateBuildsRefreshInterval); } } - window.setTimeout(updateBuilds, 5000); + window.setTimeout(updateBuilds, updateBuildsRefreshInterval); + + onPanelResize(function() { + checkAllRowCellOverflows(); + }); + onBuildHistoryChange(function() { + checkAllRowCellOverflows(); + }); } +function getElementOverflowParams(element) { + // 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. + + // Force it to wrap. + Element.addClassName(element, "force-wrap"); + var wrappedClientWidth = element.clientWidth; + var wrappedClientHeight = element.clientHeight; + Element.removeClassName(element, "force-wrap"); + + // Force it to nowrap. Return the comparisons. + Element.addClassName(element, "force-nowrap"); + var nowrapClientHeight = element.clientHeight; + try { + var overflowParams = { + element: element, + clientWidth: wrappedClientWidth, + scrollWidth: element.scrollWidth, + isOverflowed: wrappedClientHeight > nowrapClientHeight + }; + return overflowParams; + } finally { + Element.removeClassName(element, "force-nowrap"); + } +} + +var zeroWidthSpace = String.fromCharCode(8203); +function insertZeroWidthSpaces(element, maxWordSize) { + if (Element.hasClassName(element, 'zws-inserted')) { + // already done. + return; + } + + var words = element.textContent.split(/\s+/); + var newTextContent = ''; + + var splitRegex = new RegExp('.{1,' + maxWordSize + '}', 'g'); + for (var i = 0; i < words.length; i++) { + var word = words[i]; + var wordTokens = word.match(splitRegex); + if (wordTokens) { + for (var ii = 0; ii < wordTokens.length; ii++) { + if (newTextContent.length === 0) { + newTextContent += wordTokens[ii]; + } else { + newTextContent += zeroWidthSpace + wordTokens[ii]; + } + } + } else { + newTextContent += word; + } + newTextContent += ' '; + } + + element.textContent = newTextContent; + Element.addClassName(element, 'zws-inserted'); +} +function removeZeroWidthSpaces(element) { + if (element) { + element.textContent = element.textContent.replace(zeroWidthSpace, ''); + Element.removeClassName(element, 'zws-inserted'); + } +} + +function onPanelResize(handler) { + Event.observe(window, 'jenkins:panelResized', handler); +} +function firePanelResized() { + Event.fire(window, 'jenkins:panelResized'); +} + +Element.observe(document, 'dom:loaded', function(){ + if(isRunAsTest) { + return; + } + + var fixedSidePanelWidth = 360; + var pageBody = $('page-body'); + var sidePanel = $(pageBody).getElementsBySelector('#side-panel')[0]; + var mainPanel = $(pageBody).getElementsBySelector('#main-panel')[0]; + + function doPanelResize() { + var pageBodyWidth = Element.getWidth(pageBody); + if (pageBodyWidth > 768) { + $(sidePanel).setAttribute('style', 'width: ' + fixedSidePanelWidth + 'px;'); + $(mainPanel).setAttribute('style', 'width: ' + (pageBodyWidth - fixedSidePanelWidth - 5) + 'px;'); + } else { + $(sidePanel).removeAttribute('style'); + $(mainPanel).removeAttribute('style'); + } + + firePanelResized(); + } + doPanelResize(); + + Event.observe(window, 'resize', function() { + doPanelResize(); + }); +}); + // get the cascaded computed style value. 'a' is the style name like 'backgroundColor' function getStyle(e,a){ if(document.defaultView && document.defaultView.getComputedStyle)