server/resources/editFeature.jsp (561 lines of code) (raw):
<%@ include file="/include-internal.jsp" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="jetbrains.buildServer.sharedResources.SharedResourcesPluginConstants" %>
<%@ page import="jetbrains.buildServer.sharedResources.server.feature.FeatureParams" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="props" tagdir="/WEB-INF/tags/props" %>
<jsp:useBean id="keys" class="jetbrains.buildServer.sharedResources.SharedResourcesPluginConstants"/>
<jsp:useBean id="propertiesBean" scope="request" type="jetbrains.buildServer.controllers.BasePropertiesBean"/>
<jsp:useBean id="locks" scope="request" type="java.util.Map<java.lang.String, jetbrains.buildServer.sharedResources.model.Lock>"/>
<jsp:useBean id="bean" scope="request" type="jetbrains.buildServer.sharedResources.pages.beans.EditFeatureBean"/>
<jsp:useBean id="inherited" scope="request" type="java.lang.Boolean"/>
<c:set var="locksFeatureParamKey" value="<%=FeatureParams.LOCKS_FEATURE_PARAM_KEY%>"/>
<c:set var="PARAM_RESOURCE_NAME" value="<%=SharedResourcesPluginConstants.WEB.PARAM_RESOURCE_NAME%>"/>
<c:set var="PARAM_PROJECT_ID" value="<%=SharedResourcesPluginConstants.WEB.PARAM_PROJECT_ID%>"/>
<c:set var="PARAM_RESOURCE_TYPE" value="<%=SharedResourcesPluginConstants.WEB.PARAM_RESOURCE_TYPE%>"/>
<c:set var="PARAM_RESOURCE_QUOTA" value="<%=SharedResourcesPluginConstants.WEB.PARAM_RESOURCE_QUOTA%>"/>
<c:set var="project" value="${bean.project}"/>
<c:set var="canEdit" value="${not project.readOnly and afn:permissionGrantedForProject(project, 'EDIT_PROJECT')}"/>
<c:set var="templateLink">
<c:choose>
<%--@elvariable id="template" type="jetbrains.buildServer.serverSide.BuildTypeTemplate"--%>
<c:when test="${inherited && not empty template}">
<admin:editBuildTypeNavSteps settings="${template}"/>
<jsp:useBean id="buildConfigSteps"
scope="request"
type="java.util.ArrayList<jetbrains.buildServer.controllers.admin.projects.ConfigurationStep>"/>
<admin:editTemplateLink templateId="${template.externalId}"
step="${buildConfigSteps[5].stepId}"
withoutLink="true"/>
</c:when>
</c:choose>
</c:set>
<style type="text/css">
.locksTable td {
position:static;
border: 1px solid #CCCCCC;
}
</style>
<script type="text/javascript">
BS.LocksUtil = {
locksDisplay: {
readLock: "Read Lock",
writeLock: "Write Lock"
},
lockToString: function (lock) {
return lock.name + " " + lock.type + " " + (lock.value ? lock.value : "") + "\n";
},
lockToTableRow: function (lock) {
var resource = BS.SharedResourcesFeatureDialog.resources[lock.name];
var result = {};
result.name = lock.name;
if (resource.type === 'CUSTOM') {
if (lock.type === 'writeLock') {
result.description = "All Values";
} else {
if (lock.value) {
result.description = "Specific Value: " + lock.value;
} else {
result.description = "Any Value";
}
}
} else {
result.description = this.locksDisplay[lock.type];
}
result.parameter = "teamcity.locks." + lock.type + "." + lock.name;
return result;
}
};
/**
* Data container for resources and locks
*
* Deals with table of locks, calls dialog for add/edit
*
* @type {{resources: {}, locks: {}}}
*/
BS.SharedResourcesFeatureDialog = {
resources: {}, // map of resources: <resource_name, Resource>
locks: {}, // map of locks: <lock_name, Lock>
invalid: {}, // map of invalid locks <lock_name, Lock>
canEdit: true,
inherited: false,
refreshUI: function () {
var tableBody = $j('#locksTaken tbody:last');
var parametersAnchor = $j('#paramsList');
var textArea = $j('#${locksFeatureParamKey}');
tableBody.children().remove();
parametersAnchor.children().remove();
var locks = this.sortObject(this.locks);
var textAreaContent = "";
var needRendering = false;
for (var key in locks) {
if (locks.hasOwnProperty(key)) {
textAreaContent += BS.LocksUtil.lockToString(locks[key]);
// if we have invalid lock - do not render it in the table
if (!this.invalid[key]) {
this.renderSingleValidRow(tableBody, parametersAnchor, key);
needRendering = true;
}
}
}
if (needRendering) { // we have some locks
this.reHighlight();
BS.Util.show('locksTaken');
BS.Util.show('paramsDiv');
BS.Util.hide('noLocksTaken');
} else { // no locks are taken
BS.Util.hide('locksTaken');
BS.Util.hide('paramsDiv');
BS.Util.show('noLocksTaken');
}
this.renderInvalidLocks();
textArea.val($j.trim(textAreaContent));
if (this.inherited) {
BS.Util.hide('addNewLock');
} else {
BS.Util.show('addNewLock');
}
BS.MultilineProperties.updateVisible();
},
renderInvalidLocks: function() {
// render invalid locks
var invalidLocks = this.sortObject(this.invalid);
//noinspection JSUnresolvedVariable
var size = _.size(invalidLocks);
if (size > 0) {
var arr = [];
for (var key in invalidLocks) {
if (invalidLocks.hasOwnProperty(key)) {
arr.push('<strong>' + key + '</strong>');
}
}
var text = "Build feature contains invalid lock" + (size > 1 ? "s" : "") + ": " + arr.join(', ') + ". ";
var messageElement = $j('#invalidLocksMessage');
var templateLink = '${templateLink}';
if (this.inherited && templateLink !== '') {
text += (size > 1 ? "They" : "It") + " can be removed in ";
text += "<a href=\"${templateLink}\">corresponding template</a>";
}
messageElement.html(text);
if (!this.inherited && this.canEdit) {
messageElement.append($j('<a>').attr('href', '#').attr('style', 'float: right').attr('onclick', 'BS.SharedResourcesFeatureDialog.removeInvalid(); return false;').text('Remove'));
}
BS.Util.show('invalidLocksRow');
} else {
BS.Util.hide('invalidLocksRow');
}
},
renderSingleValidRow: function(tableBody, parametersAnchor, key) {
var od, deleteCell;
var oc, editCell;
var hClass;
if (this.inherited) {
oc = '';
hClass = '';
editCell = $j('<td>').attr('class', 'edit').append($j('<span>').attr('style', 'white-space: nowrap;').text('cannot be edited'));
deleteCell = $j('<td>').attr('class', 'edit').text('undeletable');
} else if (!this.canEdit) {
oc = '';
hClass = '';
editCell = '';
deleteCell = '';
} else {
oc = 'BS.LocksDialog.showEdit(\"' + key + '\", ' + this.canEdit + '); return false;';
od = 'BS.SharedResourcesFeatureDialog.deleteLock(\"' + key + '\"); return false;';
hClass = 'highlight';
editCell = $j('<td>').attr('class', 'edit ' + hClass).attr('style', 'width: 10%').attr('onclick', oc).append($j('<a>').attr('href', '#').attr('onclick', oc).text('edit'));
deleteCell = $j('<td>').attr('class', 'edit').attr('style', 'width: 10%').append($j('<a>').attr('href', '#').attr('onclick', od).text('delete'));
}
var tableRow = BS.LocksUtil.lockToTableRow(this.locks[key]);
//noinspection JSCheckFunctionSignatures
var row = $j('<tr>')
.append($j('<td>').attr('class', hClass).text(tableRow.name).attr('onclick', oc))
.append($j('<td>').attr('class', hClass).text(tableRow.description).attr('onclick', oc));
if (editCell !== '') {
row.append(editCell)
}
if (deleteCell !== '') {
row.append(deleteCell);
}
tableBody.append(row);
var paramRow = $j('<li>').append($j('<code>').text(tableRow.parameter));
parametersAnchor.append(paramRow);
},
removeInvalid: function() {
var invalidLocks = this.invalid;
var locks = this.locks;
for (var key in invalidLocks) {
if (invalidLocks.hasOwnProperty(key) && locks.hasOwnProperty(key)) {
delete invalidLocks[key];
delete locks[key];
}
}
this.refreshUI();
},
reHighlight: function () {
var hElements = $j("#locksTaken td.highlight");
hElements.each(function (i, element) {
BS.TableHighlighting.createInitElementFunction.call(this, element, 'Click to edit lock');
});
},
/**
* Keeps collection of lock objects sorted by name
*/
sortObject: function(map) {
var keys = _.sortBy(_.keys(map), function(a) { return a.toLowerCase(); });
var newmap = {};
_.each(keys, function(k) {
newmap[k] = map[k];
});
return newmap;
},
deleteLock: function (lockName) {
delete this.locks[lockName];
this.refreshUI();
}
};
//noinspection JSUnusedGlobalSymbols
/**
* Dialog for adding/editing locks
* @type {*}
*/
BS.LocksDialog = OO.extend(BS.AbstractModalDialog, {
attachedToRoot: false,
availableResources: {},
currentLockName: "",
canEdit: true,
getContainer: function () {
return $('locksDialog');
},
showDialog: function () {
this.editMode = false;
this.canEdit = true;
$j('#locksDialogTitle').html('Add Lock');
// filter available resources
this.fillAvailableResources();
this.fillAvailableResourcesDropdown();
// sync state (resources / no resources)
this.displayResourceChooser();
// sync state (resource type => locks type (quoted => read/write; custom=>ALL/ANY/SPECIFIC))
this.showCentered();
this.bindCtrlEnterHandler(this.submit.bind(this));
},
showEdit: function (lockName, canEdit) {
this.editMode = true;
this.currentLockName = lockName;
this.canEdit = canEdit;
// set proper title
$j('#locksDialogTitle').html('Edit Lock');
// select resource
var currentResource = BS.SharedResourcesFeatureDialog.resources[this.currentLockName];
// select lock
var currentLock = BS.SharedResourcesFeatureDialog.locks[this.currentLockName];
// filter available resources
this.fillAvailableResources();
// add current resource to available
this.availableResources[this.currentLockName] = currentResource;
// fill dropdown
this.fillAvailableResourcesDropdown();
// restore selection
$j('#lockFromResources option').each(function () {
var self = $j(this);
self.prop("selected", self.val() === lockName);
});
this.displayResourceChooser();
this.chooseResource();
// set values
if (currentResource.type === 'CUSTOM') {
var customLockType;
if (currentLock.type === 'readLock') {
if (currentLock.value) {
customLockType = 'SPECIFIC';
} else {
customLockType = 'ANY';
}
} else {
customLockType = 'ALL';
}
$j('#newCustomLockType option').each(function () {
var self = $j(this);
self.prop("selected", self.val() === customLockType);
}); // restore lock type
this.chooseCustomLockType();
if (customLockType === 'SPECIFIC') {
// restore selection
$j('#newCustomLockType_Values option').each(function () {
var self = $j(this);
self.prop("selected", self.val() === currentLock.value);
});
}
} else { // quoted resource. simply select lock type
$j('#newLockType option').each(function () {
var self = $j(this);
self.prop("selected", self.val() === currentLock.type);
}); // restore lock type
}
this.showCentered();
this.bindCtrlEnterHandler(this.submit.bind(this));
},
fillAvailableResourcesDropdown: function () {
var resourceDropdown = $j('#lockFromResources');
resourceDropdown.children().remove();
for (var key in this.availableResources) {
if (this.availableResources.hasOwnProperty(key)) {
//noinspection JSCheckFunctionSignatures
resourceDropdown.append(
$j("<option>").attr('value', this.availableResources[key].name).text(this.availableResources[key].name)
);
}
}
},
/**
* Filters resources that will be available for resource chooser
*/
fillAvailableResources: function () {
this.availableResources = {};
var resources = BS.SharedResourcesFeatureDialog.resources;
var locks = BS.SharedResourcesFeatureDialog.locks;
for (var key in resources) {
if (resources.hasOwnProperty(key) && !locks[key]) { // resource exists but is not used
this.availableResources[key] = resources[key];
}
}
this.availableResources = BS.SharedResourcesFeatureDialog.sortObject(this.availableResources);
},
displayResourceChooser: function () {
//noinspection JSUnresolvedVariable
if (_.size(this.availableResources) > 0) {
BS.Util.show('lockFromResources_Yes');
BS.Util.hide('lockFromResources_No');
BS.Util.show('locksDialogSubmit');
this.chooseResource();
} else {
BS.Util.show('lockFromResources_No');
BS.Util.hide('lockFromResources_Yes');
BS.Util.hide('row_CustomResource_Type');
BS.Util.hide('row_QuotedResource_Type');
BS.Util.hide('row_CustomResource_Value');
BS.Util.hide('locksDialogSubmit');
}
},
chooseResource: function () {
// get value of chooser
var resourceName = $j('#lockFromResources option:selected').val();
// get resource for value
var resource = BS.SharedResourcesFeatureDialog.resources[resourceName];
// get resource type
if (resource.type === 'QUOTED') {
BS.Util.show('row_QuotedResource_Type');
BS.Util.hide('row_CustomResource_Type');
BS.Util.hide('row_CustomResource_Value');
} else {
BS.Util.show('row_CustomResource_Type');
BS.Util.hide('row_QuotedResource_Type');
this.chooseCustomLockType();
}
},
chooseCustomLockType: function () {
var customType = $j('#newCustomLockType option:selected').val();
if ('SPECIFIC' === customType) {
BS.Util.show('row_CustomResource_Value');
this.fillResourceValues();
} else {
BS.Util.hide('row_CustomResource_Value');
}
},
fillResourceValues: function () {
// get value of chooser
var resourceName = $j('#lockFromResources option:selected').val();
// get resource for value
var resource = BS.SharedResourcesFeatureDialog.resources[resourceName];
var valuesDropdown = $j('#newCustomLockType_Values');
valuesDropdown.children().remove();
for (var key in resource.values) {
if (resource.values.hasOwnProperty(key)) {
//noinspection JSCheckFunctionSignatures
valuesDropdown.append($j("<option>").attr('value', resource.values[key]).text(resource.values[key]));
}
}
},
submit: function () {
// construct lock
var lock = {};
/// get selected resource name
/// get selected resource
// get value of chooser
var resourceName = $j('#lockFromResources option:selected').val();
// get resource for value
var resource = BS.SharedResourcesFeatureDialog.resources[resourceName];
lock.name = resourceName;
///
if (resource.type === 'QUOTED') {
lock.type = $j('#newLockType option:selected').val();
} else { // CUSTOM
var typeName = $j('#newCustomLockType option:selected').val();
if (typeName === 'ANY') {
lock.type = "readLock";
} else if (typeName === 'SPECIFIC') {
lock.type = "readLock";
lock.value = $j('#newCustomLockType_Values option:selected').val();
} else {
lock.type = "writeLock";
}
}
if (this.editMode) {
delete BS.SharedResourcesFeatureDialog.locks[this.currentLockName];
}
// add to locks
BS.SharedResourcesFeatureDialog.locks[lock.name] = lock;
// refresh ui
BS.SharedResourcesFeatureDialog.refreshUI();
this.close();
return false;
}
});
</script>
<script type="text/javascript">
var self = BS.SharedResourcesFeatureDialog;
/* load resources into javaScript */
self.canEdit = ${canEdit};
var rs = self.resources;
var rc;
<c:set var="resourcesMap" value="${bean.allResources}"/>
<c:forEach var="item" items="${resourcesMap}">
rc = {};
rc.name = '<bs:escapeForJs text="${item.name}"/>';
rc.type = '${item.type}';
<c:choose>
<c:when test="${item.type == 'CUSTOM'}">
rc.values = [];
<c:forEach var="cr" items="${item.values}">
rc.values.push('<bs:escapeForJs text="${cr}"/>');
</c:forEach>
</c:when>
<c:when test="${item.type == 'QUOTED'}">
rc.quota = ${item.quota};
</c:when>
</c:choose>
rs['<bs:escapeForJs text="${item.name}"/>'] = rc; // push resource to map
</c:forEach>
/* load locks into javascript */
var locks = self.locks;
var lc;
<c:forEach var="item" items="${locks}">
lc = {};
lc.name = '<bs:escapeForJs text="${item.value.name}"/>';
lc.type = '${item.value.type.name}';
lc.value = '<bs:escapeForJs text="${item.value.value}"/>';
locks['<bs:escapeForJs text="${item.value.name}"/>'] = lc;
</c:forEach>
self.inherited = ${inherited};
var invalid = self.invalid;
<jsp:useBean id="invalidLocks" scope="request" type="java.util.Map<java.lang.String, jetbrains.buildServer.sharedResources.model.Lock>"/>
<c:forEach var="item" items="${invalidLocks}">
lc = {};
lc.name = '<bs:escapeForJs text="${item.value.name}"/>';
lc.type = '${item.value.type.name}';
lc.value = '<bs:escapeForJs text="${item.value.value}"/>';
invalid['<bs:escapeForJs text="${item.value.name}"/>'] = lc;
</c:forEach>
BS.SharedResourcesFeatureDialog.refreshUI();
</script>
<tr>
<td colspan="2"><em>Allows limiting the number of concurrently running builds which use a shared resource.</em> <bs:help file="Shared+Resources"/></td>
</tr>
<tr>
<td class="noBorder" colspan="2">
<c:if test="${not inherited && canEdit}">
<forms:addButton id="addNewLock" onclick="BS.LocksDialog.showDialog(); return false">Add lock</forms:addButton>
</c:if>
<bs:dialog dialogId="locksDialog" title="Add Lock" titleId="locksDialogTitle" closeCommand="BS.LocksDialog.close()">
<div id="lockFromResources_Yes">
<table class="runnerFormTable">
<tr id="row_resourceChoose">
<th style="white-space: nowrap"><label for="lockFromResources">Resource name:</label></th>
<td>
<forms:select name="lockFromResources" id="lockFromResources" style="width: 90%"
onchange="BS.LocksDialog.chooseResource();"/>
<span class="smallNote">Select the resource you want to lock</span>
</td>
</tr>
<tr id="row_QuotedResource_Type">
<th><label for="newLockType">Lock type:</label></th>
<td>
<forms:select name="newLockType" id="newLockType" style="width: 90%">
<forms:option value="readLock">Read Lock</forms:option>
<forms:option value="writeLock">Write Lock</forms:option>
</forms:select>
<span class="smallNote">Select a type of lock: read lock (shared), or write lock (exclusive)</span>
</td>
</tr>
<tr id="row_CustomResource_Type">
<th>Lock type:</th>
<td>
<forms:select name="newCustomLockType" id="newCustomLockType" style="width: 90%"
onchange="BS.LocksDialog.chooseCustomLockType(); ">
<forms:option value="ANY">Lock any value</forms:option>
<forms:option value="ALL">Lock all values</forms:option>
<forms:option value="SPECIFIC">Lock specific value</forms:option>
</forms:select>
<span class="smallNote">Select a type of lock on the custom resource: any available value, all values or specify the value you want to lock</span>
</td>
</tr>
<tr id="row_CustomResource_Value">
<th>Value to lock:</th>
<td>
<forms:select name="newCustomLockType_Values" id="newCustomLockType_Values" style="width: 90%"/>
<span class="smallNote">Select value of custom resource to lock</span>
</td>
</tr>
</table>
</div>
<div id="lockFromResources_No">
<bs:out value="No resources available. Please add resource in the project settings."/>
</div>
<div class="popupSaveButtonsBlock">
<forms:submit id="locksDialogSubmit" type="button" label="Save" onclick="BS.LocksDialog.submit();"/>
<forms:cancel onclick="BS.LocksDialog.close()" showdiscardchangesmessage="false"/>
</div>
</bs:dialog>
<c:if test="${canEdit}">
<div style="float:right">
<c:url var="url" value="/admin/editProject.html?projectId=${project.externalId}&tab=JetBrains.SharedResources"/>
<a href="${url}">Configure resources</a>
</div>
</c:if>
</td>
</tr>
<tr>
<td colspan="2" style="padding-right: 8px; border: none">
<table id="locksTaken" class="parametersTable locksTable">
<thead>
<tr>
<th style="width: 25%">Resource Name</th>
<th <c:if test="${canEdit}"> colspan="3" </c:if> style="width: 75%">Lock Details</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<div id="paramsDiv">
<p>Parameters, provided to builds<bs:help file="sharedRes-locks"/>:</p>
<ul id="paramsList">
</ul>
</div>
<span class="smallNote" id="inheritedNote" style="display: none;">This feature is inherited. Locks can be edited in template this feature is inherited from.</span>
<div id="noLocksTaken" style="display: none">
No locks are currently defined
</div>
</td>
</tr>
<tr style="display: none">
<th>Locks</th>
<td>
<props:multilineProperty name="${locksFeatureParamKey}" linkTitle="names" cols="49" rows="5" expanded="${false}"/>
</td>
</tr>
<tr style="display: none" id="invalidLocksRow">
<td colspan="2">
<div class="attentionComment" id="invalidLocksMessage" style="width: 97%">
</div>
</td>
</tr>