KEYCLOAK 2538 - UI group pagination

This commit is contained in:
Levente NAGY 2017-09-07 19:39:06 +02:00
parent c8aa708cff
commit 2c24b39268
15 changed files with 384 additions and 240 deletions

View file

@ -90,6 +90,19 @@ public interface GroupsResource {
@Consumes(MediaType.APPLICATION_JSON)
Response count(@QueryParam("search") String search);
/**
* Counts groups by name search.
* @param search max number of occurrences
* @param onlyTopGroups <code>true</code> or <code>false</code> for filter only top level groups count
* @return The number of group containing search therm.
*/
@GET
@NoCache
@Path("/count")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
Response count(@QueryParam("search") String search, @QueryParam("top") String onlyTopGroups);
/**
* create or add a top level realm groupSet or create child. This will update the group and set the parent if it exists. Create it and set the parent
* if the group doesn't exist.

View file

@ -1213,8 +1213,8 @@ public class RealmAdapter implements CachedRealmModel {
}
@Override
public Long getGroupsCount() {
return cacheSession.getGroupsCount(this);
public Long getGroupsCount(Boolean onlyTopGroups) {
return cacheSession.getGroupsCount(this, onlyTopGroups);
}
@Override

View file

@ -843,8 +843,8 @@ public class RealmCacheSession implements CacheRealmProvider {
}
@Override
public Long getGroupsCount(RealmModel realm) {
return getDelegate().getGroupsCount(realm);
public Long getGroupsCount(RealmModel realm, Boolean onlyTopGroups) {
return getDelegate().getGroupsCount(realm, onlyTopGroups);
}
@Override

View file

@ -17,6 +17,7 @@
package org.keycloak.models.jpa;
import com.sun.org.apache.xpath.internal.operations.Bool;
import org.jboss.logging.Logger;
import org.keycloak.common.util.Time;
import org.keycloak.connections.jpa.util.JpaUtils;
@ -339,8 +340,12 @@ public class JpaRealmProvider implements RealmProvider {
}
@Override
public Long getGroupsCount(RealmModel realm) {
Long count = em.createNamedQuery("getGroupCount", Long.class)
public Long getGroupsCount(RealmModel realm, Boolean onlyTopGroups) {
String query = "getGroupCount";
if(Objects.equals(onlyTopGroups, Boolean.TRUE)) {
query = "getTopLevelGroupCount";
}
Long count = em.createNamedQuery(query, Long.class)
.setParameter("realm", realm.getId())
.getSingleResult();
@ -577,7 +582,7 @@ public class JpaRealmProvider implements RealmProvider {
@Override
public List<GroupModel> searchForGroupByName(RealmModel realm, String search, Integer first, Integer max) {
TypedQuery<String> query = em.createNamedQuery("getGroupIdsByNameContaining", String.class)
TypedQuery<String> query = em.createNamedQuery("getTopLevelGroupIdsByNameContaining", String.class)
.setParameter("realm", realm.getId())
.setParameter("search", search);
if(Objects.nonNull(first) && Objects.nonNull(max)) {

View file

@ -1687,8 +1687,8 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
}
@Override
public Long getGroupsCount() {
return session.realms().getGroupsCount(this);
public Long getGroupsCount(Boolean onlyTopGroups) {
return session.realms().getGroupsCount(this, onlyTopGroups);
}
@Override

View file

@ -27,9 +27,10 @@ import java.util.Collection;
*/
@NamedQueries({
@NamedQuery(name="getGroupIdsByParent", query="select u.id from GroupEntity u where u.parent = :parent"),
@NamedQuery(name="getGroupIdsByNameContaining", query="select u.id from GroupEntity u where u.realm.id = :realm and u.name like concat('%',:search,'%') order by u.name ASC"),
@NamedQuery(name="getTopLevelGroupIdsByNameContaining", query="select u.id from GroupEntity u where u.realm.id = :realm and u.name like concat('%',:search,'%') and u.parent is null order by u.name ASC"),
@NamedQuery(name="getTopLevelGroupIds", query="select u.id from GroupEntity u where u.parent is null and u.realm.id = :realm"),
@NamedQuery(name="getGroupCount", query="select count(u) from GroupEntity u where u.realm.id = :realm"),
@NamedQuery(name="getTopLevelGroupCount", query="select count(u) from GroupEntity u where u.realm.id = :realm and u.parent is null"),
@NamedQuery(name="getGroupCountByNameContaining", query="select count(u) from GroupEntity u where u.realm.id = :realm and u.name like concat('%',:name,'%')"),
})
@Entity

View file

@ -400,7 +400,7 @@ public interface RealmModel extends RoleContainerModel {
GroupModel getGroupById(String id);
List<GroupModel> getGroups();
Long getGroupsCount();
Long getGroupsCount(Boolean onlyTopGroups);
Long getGroupsCountByNameContaining(String search);
List<GroupModel> getTopLevelGroups();
List<GroupModel> getTopLevelGroups(Integer first, Integer max);

View file

@ -40,7 +40,7 @@ public interface RealmProvider extends Provider {
List<GroupModel> getGroups(RealmModel realm);
Long getGroupsCount(RealmModel realm);
Long getGroupsCount(RealmModel realm, Boolean onlyTopGroups);
Long getGroupsCountByNameContaining(RealmModel realm, String search);

View file

@ -16,6 +16,7 @@
*/
package org.keycloak.services.resources.admin;
import org.apache.http.HttpStatus;
import org.jboss.resteasy.annotations.cache.NoCache;
import org.jboss.resteasy.spi.NotFoundException;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
@ -38,6 +39,8 @@ import java.util.List;
import java.util.Objects;
import org.keycloak.services.ErrorResponse;
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
import twitter4j.JSONException;
import twitter4j.JSONObject;
/**
* @resource Groups
@ -111,15 +114,22 @@ public class GroupsResource {
@GET
@NoCache
@Path("/count")
public Response getGroupCount(@QueryParam("search") String search) {
auth.requireView();
@Produces(MediaType.APPLICATION_JSON)
public Response getGroupCount(@QueryParam("search") String search, @QueryParam("top") String onlyTopGroups) {
Long results;
JSONObject response = new JSONObject();
if (Objects.nonNull(search)) {
results = realm.getGroupsCountByNameContaining(search);
} else {
results = realm.getGroupsCount();
results = realm.getGroupsCount(Objects.equals(onlyTopGroups, Boolean.TRUE.toString()));
}
return Response.ok(results).build();
try {
response.put("count", results);
} catch (JSONException e) {
e.printStackTrace();
return ErrorResponse.error("Cannot create response object", Response.Status.INTERNAL_SERVER_ERROR);
}
return Response.ok(response.toString(), MediaType.APPLICATION_JSON).build();
}
/**

View file

@ -1031,6 +1031,7 @@ group-membership.tooltip=Groups user is a member of. Select a listed group and c
membership.available-groups.tooltip=Groups a user can join. Select a group and click the join button.
table-of-realm-users=Table of Realm Users
view-all-users=View all users
view-all-groups=View all groups
unlock-users=Unlock users
no-users-available=No users available
users.instruction=Please enter a search, or click on view all users

View file

@ -790,6 +790,9 @@ module.config([ '$routeProvider', function($routeProvider) {
},
groups : function(GroupListLoader) {
return GroupListLoader();
},
groupsCount : function(GroupCountLoader) {
return GroupCountLoader();
}
},
controller : 'GroupListCtrl'

View file

@ -1,31 +1,104 @@
module.controller('GroupListCtrl', function($scope, $route, realm, groups, Groups, Group, GroupChildren, Notifications, $location, Dialog) {
module.controller('GroupListCtrl', function($scope, $route, $q, realm, groups, groupsCount, Groups, GroupsCount, Group, GroupChildren, Notifications, $location, Dialog) {
$scope.realm = realm;
$scope.groupList = [
{"id" : "realm", "name": "Groups",
"subGroups" : groups}
{
"id" : "realm",
"name": "Groups",
"subGroups" : groups
}
];
$scope.searchTerms = '';
$scope.currentPage = 1;
$scope.currentPageInput = $scope.currentPage;
$scope.pageSize = groups.length;
$scope.numberOfPages = Math.ceil(groupsCount.count/$scope.pageSize);
$scope.tree = [];
$scope.edit = function(selected) {
if (selected.id == 'realm') return;
$location.url("/realms/" + realm.realm + "/groups/" + selected.id);
var refreshGroups = function (search) {
var queryParams = {
realm : realm.id,
first : ($scope.currentPage * $scope.pageSize) - $scope.pageSize,
max : $scope.pageSize
};
var countParams = {
realm : realm.id,
top : 'true'
};
if(angular.isDefined(search) && search !== '') {
queryParams.search = search;
countParams.search = search;
}
var promiseGetGroups = $q.defer();
Groups.query(queryParams, function(entry) {
promiseGetGroups.resolve(entry);
}, function() {
promiseGetGroups.reject('Unable to fetch ' + i);
});
var promiseGetGroupsChain = promiseGetGroups.promise.then(function(entry) {
groups = entry;
$scope.groupList = [
{
"id" : "realm",
"name": "Groups",
"subGroups" : groups
}
];
});
var promiseCount = $q.defer();
GroupsCount.query(countParams, function(entry) {
promiseCount.resolve(entry);
}, function() {
promiseCount.reject('Unable to fetch ' + i);
});
var promiseCountChain = promiseCount.promise.then(function(entry) {
groupsCount = entry;
$scope.numberOfPages = Math.ceil(groupsCount.count/$scope.pageSize);
});
$q.all([promiseGetGroupsChain, promiseCountChain]);
};
$scope.$watch('currentPage', function(newValue, oldValue) {
if(newValue !== oldValue) {
refreshGroups();
}
});
$scope.clearSearch = function() {
$scope.searchTerms = '';
$scope.currentPage = 1;
refreshGroups();
};
$scope.searchGroup = function() {
$scope.currentPage = 1;
refreshGroups($scope.searchTerms);
};
$scope.edit = function(selected) {
if (selected.id === 'realm') return;
$location.url("/realms/" + realm.realm + "/groups/" + selected.id);
};
$scope.cut = function(selected) {
$scope.cutNode = selected;
}
};
$scope.isDisabled = function() {
if (!$scope.tree.currentNode) return true;
return $scope.tree.currentNode.id == 'realm';
}
return $scope.tree.currentNode.id === 'realm';
};
$scope.paste = function(selected) {
if (selected == null) return;
if ($scope.cutNode == null) return;
if (selected.id == $scope.cutNode.id) return;
if (selected.id == 'realm') {
if (selected === null) return;
if ($scope.cutNode === null) return;
if (selected.id === $scope.cutNode.id) return;
if (selected.id === 'realm') {
Groups.save({realm: realm.realm}, {id:$scope.cutNode.id}, function() {
$route.reload();
Notifications.success("Group moved.");
@ -41,10 +114,10 @@ module.controller('GroupListCtrl', function($scope, $route, realm, groups, Group
}
}
};
$scope.remove = function(selected) {
if (selected == null) return;
if (selected === null) return;
Dialog.confirmDelete(selected.name, 'group', function() {
Group.remove({ realm: realm.realm, groupId : selected.id }, function() {
$route.reload();
@ -52,7 +125,7 @@ module.controller('GroupListCtrl', function($scope, $route, realm, groups, Group
});
});
}
};
$scope.createGroup = function(selected) {
var parent = 'realm';
@ -61,13 +134,13 @@ module.controller('GroupListCtrl', function($scope, $route, realm, groups, Group
}
$location.url("/create/group/" + realm.realm + '/parent/' + parent);
}
};
var isLeaf = function(node) {
return node.id != "realm" && (!node.subGroups || node.subGroups.length == 0);
}
return node.id !== "realm" && (!node.subGroups || node.subGroups.length === 0);
};
$scope.getGroupClass = function(node) {
if (node.id == "realm") {
if (node.id === "realm") {
return 'pficon pficon-users';
}
if (isLeaf(node)) {
@ -77,12 +150,12 @@ module.controller('GroupListCtrl', function($scope, $route, realm, groups, Group
if (node.subGroups.length && !node.collapsed) return 'expanded';
return 'collapsed';
}
};
$scope.getSelectedClass = function(node) {
if (node.selected) {
return 'selected';
} else if ($scope.cutNode && $scope.cutNode.id == node.id) {
} else if ($scope.cutNode && $scope.cutNode.id === node.id) {
return 'cut';
}
return undefined;
@ -95,8 +168,8 @@ module.controller('GroupCreateCtrl', function($scope, $route, realm, parentId, G
$scope.group = {};
$scope.save = function() {
console.log('save!!!');
if (parentId == 'realm') {
console.log('realm')
if (parentId === 'realm') {
console.log('realm');
Groups.save({realm: realm.realm}, $scope.group, function(data, headers) {
var l = headers().location;
@ -120,7 +193,7 @@ module.controller('GroupCreateCtrl', function($scope, $route, realm, parentId, G
}
}
};
$scope.cancel = function() {
$location.url("/realms/" + realm.realm + "/groups");
};
@ -176,8 +249,7 @@ module.controller('GroupDetailCtrl', function(Dialog, $scope, realm, group, Grou
var attrs = $scope.group.attributes;
for (var attribute in attrs) {
if (typeof attrs[attribute] === "string") {
var attrVals = attrs[attribute].split("##");
attrs[attribute] = attrVals;
attrs[attribute] = attrs[attribute].split("##");
}
}
}
@ -186,8 +258,7 @@ module.controller('GroupDetailCtrl', function(Dialog, $scope, realm, group, Grou
var attrs = group.attributes;
for (var attribute in attrs) {
if (typeof attrs[attribute] === "object") {
var attrVals = attrs[attribute].join("##");
attrs[attribute] = attrVals;
attrs[attribute] = attrs[attribute].join("##");
}
}
}
@ -332,13 +403,13 @@ module.controller('GroupMembersCtrl', function($scope, realm, group, GroupMember
groupId: group.id,
max : 5,
first : 0
}
};
$scope.firstPage = function() {
$scope.query.first = 0;
$scope.searchQuery();
}
};
$scope.previousPage = function() {
$scope.query.first -= parseInt($scope.query.max);
@ -346,12 +417,12 @@ module.controller('GroupMembersCtrl', function($scope, realm, group, GroupMember
$scope.query.first = 0;
}
$scope.searchQuery();
}
};
$scope.nextPage = function() {
$scope.query.first += parseInt($scope.query.max);
$scope.searchQuery();
}
};
$scope.searchQuery = function() {
console.log("query.search: " + $scope.query.search);
@ -368,7 +439,7 @@ module.controller('GroupMembersCtrl', function($scope, realm, group, GroupMember
});
module.controller('DefaultGroupsCtrl', function($scope, $route, realm, groups, DefaultGroups, Notifications, $location, Dialog) {
module.controller('DefaultGroupsCtrl', function($scope, $route, realm, groups, DefaultGroups, Notifications) {
$scope.realm = realm;
$scope.groupList = groups;
$scope.selectedGroup = null;
@ -383,7 +454,7 @@ module.controller('DefaultGroupsCtrl', function($scope, $route, realm, groups, D
if (!$scope.tree.currentNode) {
Notifications.error('Please select a group to add');
return;
};
}
DefaultGroups.update({realm: realm.realm, groupId: $scope.tree.currentNode.id}, function() {
Notifications.success('Added default group');
@ -401,11 +472,11 @@ module.controller('DefaultGroupsCtrl', function($scope, $route, realm, groups, D
};
var isLeaf = function(node) {
return node.id != "realm" && (!node.subGroups || node.subGroups.length == 0);
return node.id !== "realm" && (!node.subGroups || node.subGroups.length === 0);
};
$scope.getGroupClass = function(node) {
if (node.id == "realm") {
if (node.id === "realm") {
return 'pficon pficon-users';
}
if (isLeaf(node)) {
@ -415,12 +486,12 @@ module.controller('DefaultGroupsCtrl', function($scope, $route, realm, groups, D
if (node.subGroups.length && !node.collapsed) return 'expanded';
return 'collapsed';
}
};
$scope.getSelectedClass = function(node) {
if (node.selected) {
return 'selected';
} else if ($scope.cutNode && $scope.cutNode.id == node.id) {
} else if ($scope.cutNode && $scope.cutNode.id === node.id) {
return 'cut';
}
return undefined;

View file

@ -15,7 +15,7 @@ module.factory('Loader', function($q) {
});
return delay.promise;
};
}
};
loader.query = function(service, id) {
return function() {
var i = id && id();
@ -27,7 +27,7 @@ module.factory('Loader', function($q) {
});
return delay.promise;
};
}
};
return loader;
});
@ -490,7 +490,18 @@ module.factory('AuthenticationConfigLoader', function(Loader, AuthenticationConf
module.factory('GroupListLoader', function(Loader, Groups, $route, $q) {
return Loader.query(Groups, function() {
return {
realm : $route.current.params.realm
realm : $route.current.params.realm,
first : 1,
max : 20
}
});
});
module.factory('GroupCountLoader', function(Loader, GroupsCount, $route, $q) {
return Loader.query(GroupsCount, function() {
return {
realm : $route.current.params.realm,
top : 'true'
}
});
});

View file

@ -1606,10 +1606,26 @@ module.factory('GroupChildren', function($resource) {
});
});
module.factory('GroupsCount', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/groups/count', {
realm : '@realm'
},
{
query: {
isArray: false,
method: 'GET',
params: {},
transformResponse: function (data) {
return angular.fromJson(data)
}
}
});
});
module.factory('Groups', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/groups', {
realm : '@realm'
});
})
});
module.factory('GroupRealmRoleMapping', function($resource) {

View file

@ -1,12 +1,22 @@
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
<kc-tabs-group-list></kc-tabs-group-list>
<table class="table table-striped table-bordered">
<table class="table table-striped table-bordered" style="margin-bottom: 0">
<thead>
<tr>
<th class="kc-table-actions" colspan="5">
<div class="form-inline">
<div class="form-group">
<div class="input-group">
<input type="text" placeholder="{{:: 'search.placeholder' | translate}}" ng-model="searchTerms" class="form-control search" onkeydown="if (event.keyCode == 13) document.getElementById('groupSearch').click()">
<div class="input-group-addon">
<i class="fa fa-search" id="groupSearch" ng-click="searchGroup()"></i>
</div>
</div>
</div>
<button id="viewAllGroups" class="btn btn-default" ng-click="clearSearch()">{{:: 'view-all-groups' | translate}}</button>
<div class="pull-right" data-ng-show="access.manageUsers">
<div class="form-inline">
<button id="createGroup" class="btn btn-default" ng-click="createGroup(tree.currentNode)">{{:: 'new' | translate}}</button>
<button id="editGroup" ng-disabled="isDisabled()" class="btn btn-default" ng-click="edit(tree.currentNode)">{{:: 'edit' | translate}}</button>
<button id="cutGroup" ng-disabled="isDisabled()" class="btn btn-default" ng-click="cut(tree.currentNode)">{{:: 'cut' | translate}}</button>
@ -14,13 +24,14 @@
<button id="removeGroup" ng-disabled="isDisabled()" class="btn btn-default" ng-click="remove(tree.currentNode)">{{:: 'delete' | translate}}</button>
</div>
</div>
</div>
</th>
</tr>
</thead>
<tbody>
<tr>
<td> <div
tree-id="tree"
<td>
<div tree-id="tree"
angular-treeview="true"
tree-model="groupList"
node-id="id"
@ -31,7 +42,9 @@
</tr>
</tbody>
</table>
<div style="margin-bottom: 50px">
<kc-paging current-page="currentPage" number-of-pages="numberOfPages" current-page-input="currentPageInput"></kc-paging>
</div>
</div>
<kc-menu></kc-menu>