NIFI-2884: - Adding support to selecting multiple users before updating a policy.

NIFI-2533: - Only including a user/group in the search results if they are not currently selected.
NIFI-2286: - Providing a tooltip for the add user and remove policy button.

This closes #1155.

Signed-off-by: Bryan Bende <bbende@apache.org>
This commit is contained in:
Matt Gilman 2016-10-24 10:28:17 -04:00 committed by Bryan Bende
parent 8c09bef4f8
commit 8dc60c72d4
No known key found for this signature in database
GPG Key ID: A0DDA9ED50711C39
8 changed files with 260 additions and 107 deletions

View File

@ -38,7 +38,6 @@ import org.apache.nifi.web.api.dto.RevisionDTO;
import org.apache.nifi.web.api.dto.TenantDTO;
import org.apache.nifi.web.api.dto.UserDTO;
import org.apache.nifi.web.api.dto.UserGroupDTO;
import org.apache.nifi.web.api.entity.ClusterSearchResultsEntity;
import org.apache.nifi.web.api.entity.TenantEntity;
import org.apache.nifi.web.api.entity.TenantsEntity;
import org.apache.nifi.web.api.entity.UserEntity;
@ -166,6 +165,10 @@ public class TenantsResource extends ApplicationResource {
throw new IllegalArgumentException("User ID cannot be specified.");
}
if (StringUtils.isBlank(requestUserEntity.getComponent().getIdentity())) {
throw new IllegalArgumentException("User identity must be specified.");
}
if (isReplicateRequest()) {
return replicate(HttpMethod.POST, requestUserEntity);
}
@ -551,6 +554,10 @@ public class TenantsResource extends ApplicationResource {
throw new IllegalArgumentException("User group ID cannot be specified.");
}
if (StringUtils.isBlank(requestUserGroupEntity.getComponent().getIdentity())) {
throw new IllegalArgumentException("User group identity must be specified.");
}
if (isReplicateRequest()) {
return replicate(HttpMethod.POST, requestUserGroupEntity);
}
@ -864,19 +871,19 @@ public class TenantsResource extends ApplicationResource {
// ------------
/**
* Searches the cluster for a node with a given address.
* Searches for a tenant with a given identity.
*
* @param value Search value that will be matched against a node's address
* @return Nodes that match the specified criteria
* @param value Search value that will be matched against a user/group identity
* @return Tenants match the specified criteria
*/
@GET
@Consumes(MediaType.WILDCARD)
@Produces(MediaType.APPLICATION_JSON)
@Path("search-results")
@ApiOperation(
value = "Searches the cluster for a node with the specified address",
value = "Searches for a tenant with the specified identity",
notes = NON_GUARANTEED_ENDPOINT,
response = ClusterSearchResultsEntity.class,
response = TenantsEntity.class,
authorizations = {
@Authorization(value = "Read - /tenants", type = "")
}
@ -892,7 +899,7 @@ public class TenantsResource extends ApplicationResource {
)
public Response searchCluster(
@ApiParam(
value = "Node address to search for.",
value = "Identity to search for.",
required = true
)
@QueryParam("q") @DefaultValue(StringUtils.EMPTY) String value) {
@ -925,6 +932,7 @@ public class TenantsResource extends ApplicationResource {
final TenantEntity entity = new TenantEntity();
entity.setPermissions(userEntity.getPermissions());
entity.setRevision(userEntity.getRevision());
entity.setId(userEntity.getId());
entity.setComponent(tenant);
@ -942,6 +950,7 @@ public class TenantsResource extends ApplicationResource {
final TenantEntity entity = new TenantEntity();
entity.setPermissions(userGroupEntity.getPermissions());
entity.setRevision(userGroupEntity.getRevision());
entity.setId(userGroupEntity.getId());
entity.setComponent(tenant);

View File

@ -78,8 +78,8 @@
<div id="component-policy-target"></div>
<div class="clear"></div>
</div>
<button id="delete-policy-button" class="fa fa-trash policy-button"></button>
<button id="new-policy-user-button" class="fa fa-user-plus policy-button"></button>
<button id="delete-policy-button" class="fa fa-trash policy-button" title="Delete this policy"></button>
<button id="new-policy-user-button" class="fa fa-user-plus policy-button" title="Add users/groups to this policy"></button>
<div class="clear"></div>
</div>
<div id="policy-table"></div>

View File

@ -17,7 +17,21 @@
<%@ page contentType="text/html" pageEncoding="UTF-8" session="false" %>
<div id="search-users-dialog" class="hidden">
<div class="dialog-content">
<input id="search-users-field" type="text" placeholder="User Identity"/>
<div class="secure-port-setting">
<input id="search-users-field" type="text" placeholder="User Identity"/>
</div>
<div class="secure-port-setting">
<div class="setting-name">Allowed Users</div>
<div class="setting-field allowed-container">
<ul id="allowed-users" class="allowed"></ul>
</div>
</div>
<div class="secure-port-setting">
<div class="setting-name">Allowed Groups</div>
<div class="setting-field allowed-container">
<ul id="allowed-groups" class="allowed"></ul>
</div>
</div>
</div>
</div>
<div id="search-users-results"></div>

View File

@ -536,6 +536,15 @@ div.context-menu-item-text {
color: #262626;
}
/* search */
li.search-no-matches {
padding: 4px;
font-weight: bold;
color: #aaa;
font-style: italic;
}
/* progress bars */
md-progress-linear > div {

View File

@ -170,13 +170,6 @@ input.search-flow {
line-height: 1.4em;
}
#search-flow-results li.search-no-matches {
padding: 4px;
font-weight: bold;
color: #aaa;
font-style: italic;
}
div.search-glass-pane {
position: absolute;
background-image: url(../images/spacer.png);

View File

@ -194,7 +194,7 @@ div.policy-selected-component-type {
#search-users-dialog {
width: 450px;
height: 250px;
height: 450px;
}
#search-users-results .ui-autocomplete {
@ -211,6 +211,69 @@ div.policy-selected-component-type {
border-radius: 0;
}
div.secure-port-setting {
margin-bottom: 15px;
width: 410px;
}
li.search-users-header {
font-weight: bold;
padding-top: 4px;
padding-left: 4px;
padding-right: 4px;
height: 14px;
}
div.search-users-match-header {
font-weight: normal;
margin-left: 10px;
}
li.search-users-no-matches {
padding: 4px;
font-weight: bold;
color: #aaa;
font-style: italic;
}
div.allowed-container {
width: 408px;
height: 100px;
border: 1px solid #aaa;
overflow-x: hidden;
overflow-y: scroll;
}
ul.allowed {
list-style-type: none;
}
ul.allowed li {
height: 16px;
width: 385px;
border-bottom: 1px solid #ddd;
color: #262626;
overflow: hidden;
margin: 2px;
padding: 2px;
line-height: 16px;
font-weight: normal;
}
span.allowed-entity {
float: left;
width: 360px;
}
div.remove-allowed-entity {
float: right;
width: 16px;
height: 16px;
cursor: pointer;
font-size: 14px;
line-height: 16px;
}
/*
admin policy message
*/

View File

@ -50,85 +50,31 @@ nf.PolicyManagement = (function () {
},
handler: {
click: function () {
var tenantSearchTerm = $('#search-users-field').val();
// add to table and update policy
var policyGrid = $('#policy-table').data('gridInstance');
var policyData = policyGrid.getData();
// create the search request
$.ajax({
type: 'GET',
data: {
q: tenantSearchTerm
},
dataType: 'json',
url: config.urls.searchTenants
}).done(function (response) {
var selectedUsers = $.map(response.users, function (user) {
return $.extend({
type: 'user'
}, user);
});
var selectedGroups = $.map(response.userGroups, function (userGroup) {
return $.extend({
type: 'group'
}, userGroup);
});
// begin the update
policyData.beginUpdate();
var selectedUser = [];
selectedUser = selectedUser.concat(selectedUsers, selectedGroups);
// add the user to the policy table
var addUser = function (user) {
// add to table and update policy
var policyGrid = $('#policy-table').data('gridInstance');
var policyData = policyGrid.getData();
// begin the update
policyData.beginUpdate();
// remove the user
policyData.addItem(user);
// end the update
policyData.endUpdate();
// update the policy
updatePolicy();
};
// ensure the search found some results
if (!$.isArray(selectedUser) || selectedUser.length === 0) {
nf.Dialog.showOkDialog({
headerText: 'User Search',
dialogContent: 'No users match \'' + nf.Common.escapeHtml(tenantSearchTerm) + '\'.'
});
} else if (selectedUser.length > 1) {
var exactMatch = false;
// look for an exact match
$.each(selectedUser, function (_, userEntity) {
if (userEntity.component.identity === tenantSearchTerm) {
addUser(userEntity);
exactMatch = true;
return false;
}
});
// if there is an exact match, use it
if (exactMatch) {
// close the dialog
$('#search-users-dialog').modal('hide');
} else {
nf.Dialog.showOkDialog({
headerText: 'User Search',
dialogContent: 'More than one user matches \'' + nf.Common.escapeHtml(tenantSearchTerm) + '\'.'
});
}
} else if (selectedUser.length === 1) {
addUser(selectedUser[0]);
// close the dialog
$('#search-users-dialog').modal('hide');
}
// add all users/groups
$.each(getTenantsToAdd($('#allowed-users')), function (_, user) {
// remove the user
policyData.addItem(user);
});
$.each(getTenantsToAdd($('#allowed-groups')), function (_, group) {
// remove the user
policyData.addItem(group);
});
// end the update
policyData.endUpdate();
// update the policy
updatePolicy();
// close the dialog
$('#search-users-dialog').modal('hide');
}
}
},
@ -150,11 +96,18 @@ nf.PolicyManagement = (function () {
close: function () {
// reset the search fields
$('#search-users-field').userSearchAutocomplete('reset').val('');
$('#selected-user-id').text('');
// clear the selected users/groups
$('#allowed-users, #allowed-groups').empty();
}
}
});
// listen for removal requests
$(document).on('click', 'div.remove-allowed-entity', function () {
$(this).closest('li').remove();
});
// configure the user auto complete
$.widget('nf.userSearchAutocomplete', $.ui.autocomplete, {
reset: function () {
@ -169,18 +122,25 @@ nf.PolicyManagement = (function () {
// results are normalized into a single element array
var searchResults = items[0];
var allowedGroups = getAllAllowedGroups();
var allowedUsers = getAllAllowedUsers();
var self = this;
$.each(searchResults.userGroups, function (_, tenant) {
self._renderGroup(ul, {
label: tenant.component.identity,
value: tenant.component.identity
});
// see if this match is not already selected
if ($.inArray(tenant.id, allowedGroups) === -1) {
self._renderGroup(ul, $.extend({
type: 'group'
}, tenant));
}
});
$.each(searchResults.users, function (_, tenant) {
self._renderUser(ul, {
label: tenant.component.identity,
value: tenant.component.identity
});
// see if this match is not already selected
if ($.inArray(tenant.id, allowedUsers) === -1) {
self._renderUser(ul, $.extend({
type: 'user'
}, tenant));
}
});
// ensure there were some results
@ -190,14 +150,14 @@ nf.PolicyManagement = (function () {
},
_resizeMenu: function () {
var ul = this.menu.element;
ul.width($('#search-users-field').width() + 6);
ul.width($('#search-users-field').outerWidth() - 6);
},
_renderUser: function (ul, match) {
var userContent = $('<a></a>').text(match.label);
var userContent = $('<a></a>').text(match.component.identity);
return $('<li></li>').data('ui-autocomplete-item', match).append(userContent).appendTo(ul);
},
_renderGroup: function (ul, match) {
var groupLabel = $('<span></span>').text(match.label);
var groupLabel = $('<span></span>').text(match.component.identity);
var groupContent = $('<a></a>').append('<div class="fa fa-users" style="margin-right: 5px;"></div>').append(groupLabel);
return $('<li></li>').data('ui-autocomplete-item', match).append(groupContent).appendTo(ul);
}
@ -224,10 +184,110 @@ nf.PolicyManagement = (function () {
}).done(function (searchResponse) {
response(searchResponse);
});
},
select: function (event, ui) {
addAllowedTenant(ui.item);
// reset the search field
$(this).val('');
// stop event propagation
return false;
}
});
};
/**
* Gets all allowed groups including those already in the policy and those selected while searching (not yet saved).
*
* @returns {Array}
*/
var getAllAllowedGroups = function () {
var policyGrid = $('#policy-table').data('gridInstance');
var policyData = policyGrid.getData();
var userGroups = [];
// consider existing groups in the policy table
var items = policyData.getItems();
$.each(items, function (_, item) {
if (item.type === 'group') {
userGroups.push(item.id);
}
});
// also consider groups already selected in the search users dialog
$.each(getTenantsToAdd($('#allowed-groups')), function (_, group) {
userGroups.push(group.id);
});
return userGroups;
};
/**
* Gets the user groups that will be added upon applying the changes.
*
* @param {jQuery} container
* @returns {Array}
*/
var getTenantsToAdd = function (container) {
var tenants = [];
// also consider groups already selected in the search users dialog
container.children('li').each(function (_, allowedTenant) {
var tenant = $(allowedTenant).data('tenant');
if (nf.Common.isDefinedAndNotNull(tenant)) {
tenants.push(tenant);
}
});
return tenants;
};
/**
* Gets all allowed users including those already in the policy and those selected while searching (not yet saved).
*
* @returns {Array}
*/
var getAllAllowedUsers = function () {
var policyGrid = $('#policy-table').data('gridInstance');
var policyData = policyGrid.getData();
var users = [];
// consider existing users in the policy table
var items = policyData.getItems();
$.each(items, function (_, item) {
if (item.type === 'user') {
users.push(item.id);
}
});
// also consider users already selected in the search users dialog
$.each(getTenantsToAdd($('#allowed-users')), function (_, user) {
users.push(user.id);
});
return users;
};
/**
* Added the specified tenant to the listing of users/groups which will be added when applied.
*
* @param allowedTenant user/group to add
*/
var addAllowedTenant = function (allowedTenant) {
var allowedTenants = allowedTenant.type === 'user' ? $('#allowed-users') : $('#allowed-groups');
// append the user
var tenant = $('<span></span>').addClass('allowed-entity ellipsis').text(allowedTenant.component.identity).ellipsis();
var tenantAction = $('<div></div>').addClass('remove-allowed-entity fa fa-trash');
$('<li></li>').data('tenant', allowedTenant).append(tenant).append(tenantAction).appendTo(allowedTenants);
};
/**
* Initializes the policy table.
*/
var initPolicyTable = function () {
$('#override-policy-dialog').modal({
headerText: 'Override Policy',

View File

@ -824,7 +824,12 @@ nf.UsersTable = (function () {
$('#new-user-button').on('click', function () {
buildUsersList();
buildGroupsList();
// show the dialog
$('#user-dialog').modal('show');
// set the focus automatically, only when adding a new user
$('#user-identity-edit-dialog').focus();
});
$('#new-user-button').prop('disabled', false);