提交 cb9036c4 编写于 作者: T tfennelly

Single/Multiline Build History

上级 6bf6e7f5
......@@ -33,30 +33,38 @@ THE SOFTWARE.
<j:if test="${!pending.isEmpty()}">
<j:forEach var="item" items="${queuedItems}" indexVar="i">
<j:set var="id" value="${h.generateId()}"/>
<tr class="build-row transitive" id="${id}">
<td nowrap="nowrap">
<l:icon class="icon-grey icon-sm"/>
<st:nbsp/>
<!-- Don't use math unless needed, in case nextBuildNumber is not numeric -->
#${queuedItems.size()==1 ? it.owner.nextBuildNumber
: it.owner.nextBuildNumber+queuedItems.size()-i-1}
</td>
<td style="white-space:normal;" colspan="2">
<div style="float:right">
<j:if test="${item.hasCancelPermission()}">
<l:stopButton href="${rootURL}/queue/cancelItem?id=${item.id}" alt="${%cancel this build}"/>
</j:if>
<tr class="build-row build-pending transitive single-line" id="${id}">
<td class="build-row-cell">
<div class="pane build-name">
<div class="build-icon">
<l:icon class="icon-grey icon-sm"/>
</div>
<!-- Don't use math unless needed, in case nextBuildNumber is not numeric -->
<div class="display-name">
#${queuedItems.size()==1 ? it.owner.nextBuildNumber
: it.owner.nextBuildNumber+queuedItems.size()-i-1}
</div>
</div>
<div class="pane build-details indent-multiline">
<j:set var="cause" value="${item.getCauseOfBlockage()}"/>
<j:choose>
<j:when test="${cause!=null}">
(${%pending}—<st:include it="${cause}" page="summary.jelly"/>)
</j:when>
<j:otherwise>
(${%pending})
</j:otherwise>
</j:choose>
${item.params}
</div>
<div class="pane build-controls">
<div class="build-stop">
<j:if test="${item.hasCancelPermission()}">
<l:stopButton href="${rootURL}/queue/cancelItem?id=${item.id}" alt="${%cancel this build}"/>
</j:if>
</div>
</div>
<j:set var="cause" value="${item.getCauseOfBlockage()}"/>
<j:choose>
<j:when test="${cause!=null}">
(${%pending}—<st:include it="${cause}" page="summary.jelly"/>)
</j:when>
<j:otherwise>
(${%pending})
</j:otherwise>
</j:choose>
${item.params}
<div class="left-bar"></div>
</td>
</tr>
</j:forEach>
......
......@@ -29,41 +29,48 @@ THE SOFTWARE.
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:i="jelly:fmt">
<j:set var="link" value="${it.baseUrl}/${build.number}/" />
<j:set var="transitive" value="${(it.firstTransientBuildKey!=null and (it.adapter.compare(build,it.firstTransientBuildKey) ge 0)) ? 'transitive' : null}" />
<tr class="build-row ${transitive}">
<td class="pane build-name">
<a class="build-status-link" href="${link}console"><l:icon alt="${build.iconColor.description} &gt; ${%Console Output}" class="${build.buildStatusIconClassName} icon-sm" tooltip="${build.iconColor.description} &gt; ${%Console Output}"/></a><st:nbsp/>
${build.displayName}
</td>
<td class="pane build-details">
<a class="tip model-link inside" href="${link}">
<i:formatDate value="${build.timestamp.time}" type="both" dateStyle="medium" timeStyle="medium"/>
</a>
<j:if test="${build.building}">
<j:set target="${it}" property="nextBuildNumberToFetch" value="${build.number}"/>
<t:buildProgressBar build="${build}"/>
</j:if>
<tr class="build-row ${transitive} single-line">
<td class="build-row-cell">
<div class="pane build-name">
<div class="build-icon">
<a class="build-status-link" href="${link}console"><l:icon alt="${build.iconColor.description} &gt; ${%Console Output}" class="${build.buildStatusIconClassName} icon-sm" tooltip="${build.iconColor.description} &gt; ${%Console Output}"/></a>
</div>
<a class="display-name" href="${link}">${build.displayName}</a>
</div>
<div class="pane build-details" time="${build.timestamp.time.time}">
<a class="tip model-link inside build-link" href="${link}">
<i:formatDate value="${build.timestamp.time}" type="both" dateStyle="medium" timeStyle="short" />
</a>
<j:if test="${build.building}">
<j:set target="${it}" property="nextBuildNumberToFetch" value="${build.number}"/>
<t:buildProgressBar build="${build}"/>
</j:if>
</div>
<div class="pane build-controls">
<div class="middle-align build-badge">
<j:set var="badges" value="${build.badgeActions}"/>
<j:if test="${!empty(badges)}">
<st:nbsp/>
<j:forEach var="badge" items="${badges}">
<st:include it="${badge}" page="badge.jelly" />
</j:forEach>
</j:if>
</div>
<j:if test="${build.building}">
<div class="build-stop">
<!-- Check ABORT permission for Project, Admin permission otherwise -->
<j:if test="${empty(it.owner.ABORT) ? h.hasPermission(app.ADMINISTER) : it.owner.hasPermission(it.owner.ABORT)}">
<l:stopButton href="${link}stop" alt="[cancel]"/>
</j:if>
</div>
</j:if>
</div>
<j:if test="${!empty build.description}">
<div class="desc">
<div class="pane desc indent-multiline">
<j:out value="${app.markupFormatter.translate(build.truncatedDescription)}"/>
</div>
</j:if>
</td>
<td class="pane build-stop">
<j:if test="${build.building}">
<!-- Check ABORT permission for Project, Admin permission otherwise -->
<j:if test="${empty(it.owner.ABORT) ? h.hasPermission(app.ADMINISTER) : it.owner.hasPermission(it.owner.ABORT)}">
<l:stopButton href="${link}stop" alt="[cancel]"/>
</j:if>
</j:if>
</td>
<td class="pane middle-align build-badge">
<j:set var="badges" value="${build.badgeActions}"/>
<j:if test="${!empty(badges)}">
<st:nbsp/>
<j:forEach var="badge" items="${badges}">
<st:include it="${badge}" page="badge.jelly" />
</j:forEach>
</j:if>
<div class="left-bar" />
</td>
</tr>
</j:jelly>
......@@ -88,6 +88,7 @@ THE SOFTWARE.
newhist.headers = hist.headers
replace(hist, newhist);
fireBuildHistoryChanged();
}
});
}
......
......@@ -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 ========================= */
......
......@@ -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)
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册