diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/GroupsResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/GroupsResource.java index 78070941ad..6c92204cca 100755 --- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/GroupsResource.java +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/GroupsResource.java @@ -20,26 +20,89 @@ package org.keycloak.admin.client.resource; import org.jboss.resteasy.annotations.cache.NoCache; import org.keycloak.representations.idm.GroupRepresentation; -import javax.ws.rs.Consumes; -import javax.ws.rs.GET; -import javax.ws.rs.POST; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.Produces; +import javax.ws.rs.*; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import java.util.List; +import java.util.Map; /** * @author Bill Burke * @version $Revision: 1 $ */ public interface GroupsResource { + + /** + * Get all groups. + * @return A list containing all groups. + */ @GET @NoCache @Produces(MediaType.APPLICATION_JSON) List groups(); + /** + * Get groups by pagination params. + * @param first index of the first element + * @param max max number of occurrences + * @return A list containing the slice of all groups. + */ + @GET + @NoCache + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + List groups(@QueryParam("first") Integer first, @QueryParam("max") Integer max); + + /** + * Get groups by pagination params. + * @param search max number of occurrences + * @param first index of the first element + * @param max max number of occurrences + * @return A list containing the slice of all groups. + */ + @GET + @NoCache + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + List groups(@QueryParam("search") String search, + @QueryParam("first") Integer first, + @QueryParam("max") Integer max); + + /** + * Counts all groups. + * @return A map containing key "count" with number of groups as value. + */ + @GET + @NoCache + @Path("count") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + Map count(); + + /** + * Counts groups by name search. + * @param search max number of occurrences + * @return A map containing key "count" with number of groups as value which matching with search. + */ + @GET + @NoCache + @Path("count") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + Map count(@QueryParam("search") String search); + + /** + * Counts groups by name search. + * @param onlyTopGroups true or false for filter only top level groups count + * @return A map containing key "count" with number of top level groups. + */ + @GET + @NoCache + @Path("count") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + Map count(@QueryParam("top") @DefaultValue("true") boolean 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. diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java index 9925a690a0..7144c3b474 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java @@ -20,32 +20,12 @@ package org.keycloak.models.cache.infinispan; import org.keycloak.Config; import org.keycloak.common.enums.SslRequired; import org.keycloak.component.ComponentModel; -import org.keycloak.models.AuthenticationExecutionModel; -import org.keycloak.models.AuthenticationFlowModel; -import org.keycloak.models.AuthenticatorConfigModel; -import org.keycloak.models.ClientModel; -import org.keycloak.models.ClientTemplateModel; -import org.keycloak.models.GroupModel; -import org.keycloak.models.IdentityProviderMapperModel; -import org.keycloak.models.IdentityProviderModel; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.OTPPolicy; -import org.keycloak.models.PasswordPolicy; -import org.keycloak.models.RealmModel; -import org.keycloak.models.RequiredActionProviderModel; -import org.keycloak.models.RequiredCredentialModel; -import org.keycloak.models.RoleModel; +import org.keycloak.models.*; import org.keycloak.models.cache.CachedRealmModel; import org.keycloak.models.cache.infinispan.entities.CachedRealm; import org.keycloak.storage.UserStorageProvider; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; /** @@ -328,7 +308,7 @@ public class RealmAdapter implements CachedRealmModel { getDelegateForUpdate(); updated.setLoginWithEmailAllowed(loginWithEmailAllowed); } - + @Override public boolean isDuplicateEmailsAllowed() { if (isUpdated()) return updated.isDuplicateEmailsAllowed(); @@ -797,9 +777,9 @@ public class RealmAdapter implements CachedRealmModel { @Override public void setEnabledEventTypes(Set enabledEventTypes) { getDelegateForUpdate(); - updated.setEnabledEventTypes(enabledEventTypes); + updated.setEnabledEventTypes(enabledEventTypes); } - + @Override public boolean isAdminEventsEnabled() { if (isUpdated()) return updated.isAdminEventsEnabled(); @@ -823,7 +803,7 @@ public class RealmAdapter implements CachedRealmModel { getDelegateForUpdate(); updated.setAdminEventsDetailsEnabled(enabled); } - + @Override public ClientModel getMasterAdminClient() { return cached.getMasterAdminClient()==null ? null : cacheSession.getRealm(Config.getAdminRealm()).getClientById(cached.getMasterAdminClient()); @@ -1232,11 +1212,31 @@ public class RealmAdapter implements CachedRealmModel { return cacheSession.getGroups(this); } + @Override + public Long getGroupsCount(Boolean onlyTopGroups) { + return cacheSession.getGroupsCount(this, onlyTopGroups); + } + + @Override + public Long getGroupsCountByNameContaining(String search) { + return cacheSession.getGroupsCountByNameContaining(this, search); + } + @Override public List getTopLevelGroups() { return cacheSession.getTopLevelGroups(this); } + @Override + public List getTopLevelGroups(Integer first, Integer max) { + return cacheSession.getTopLevelGroups(this, first, max); + } + + @Override + public List searchForGroupByName(String search, Integer first, Integer max) { + return cacheSession.searchForGroupByName(this, search, first, max); + } + @Override public boolean removeGroup(GroupModel group) { return cacheSession.removeGroup(this, group); diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java index fdd5cce38b..318bb539c5 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java @@ -22,48 +22,14 @@ import org.keycloak.cluster.ClusterProvider; import org.keycloak.models.ClientInitialAccessModel; import org.keycloak.models.cache.infinispan.events.InvalidationEvent; import org.keycloak.migration.MigrationModel; -import org.keycloak.models.ClientModel; -import org.keycloak.models.ClientTemplateModel; -import org.keycloak.models.GroupModel; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.KeycloakTransaction; -import org.keycloak.models.RealmModel; -import org.keycloak.models.RealmProvider; -import org.keycloak.models.RoleModel; +import org.keycloak.models.*; import org.keycloak.models.cache.CacheRealmProvider; import org.keycloak.models.cache.CachedRealmModel; -import org.keycloak.models.cache.infinispan.entities.CachedClient; -import org.keycloak.models.cache.infinispan.entities.CachedClientRole; -import org.keycloak.models.cache.infinispan.entities.CachedClientTemplate; -import org.keycloak.models.cache.infinispan.entities.CachedGroup; -import org.keycloak.models.cache.infinispan.entities.CachedRealm; -import org.keycloak.models.cache.infinispan.entities.CachedRealmRole; -import org.keycloak.models.cache.infinispan.entities.CachedRole; -import org.keycloak.models.cache.infinispan.entities.ClientListQuery; -import org.keycloak.models.cache.infinispan.entities.GroupListQuery; -import org.keycloak.models.cache.infinispan.entities.RealmListQuery; -import org.keycloak.models.cache.infinispan.entities.RoleListQuery; -import org.keycloak.models.cache.infinispan.events.ClientAddedEvent; -import org.keycloak.models.cache.infinispan.events.ClientRemovedEvent; -import org.keycloak.models.cache.infinispan.events.ClientTemplateEvent; -import org.keycloak.models.cache.infinispan.events.ClientUpdatedEvent; -import org.keycloak.models.cache.infinispan.events.GroupAddedEvent; -import org.keycloak.models.cache.infinispan.events.GroupMovedEvent; -import org.keycloak.models.cache.infinispan.events.GroupRemovedEvent; -import org.keycloak.models.cache.infinispan.events.GroupUpdatedEvent; -import org.keycloak.models.cache.infinispan.events.RealmRemovedEvent; -import org.keycloak.models.cache.infinispan.events.RealmUpdatedEvent; -import org.keycloak.models.cache.infinispan.events.RoleAddedEvent; -import org.keycloak.models.cache.infinispan.events.RoleRemovedEvent; -import org.keycloak.models.cache.infinispan.events.RoleUpdatedEvent; +import org.keycloak.models.cache.infinispan.entities.*; +import org.keycloak.models.cache.infinispan.events.*; import org.keycloak.models.utils.KeycloakModelUtils; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; /** @@ -128,7 +94,6 @@ public class RealmCacheSession implements CacheRealmProvider { protected static final Logger logger = Logger.getLogger(RealmCacheSession.class); public static final String REALM_CLIENTS_QUERY_SUFFIX = ".realm.clients"; public static final String ROLES_QUERY_SUFFIX = ".roles"; - public static final String ROLE_BY_NAME_QUERY_SUFFIX = ".role.by-name"; protected RealmCacheManager cache; protected KeycloakSession session; protected RealmProvider delegate; @@ -873,9 +838,22 @@ public class RealmCacheSession implements CacheRealmProvider { } list.add(group); } + + list.sort(Comparator.comparing(GroupModel::getName)); + return list; } + @Override + public Long getGroupsCount(RealmModel realm, Boolean onlyTopGroups) { + return getDelegate().getGroupsCount(realm, onlyTopGroups); + } + + @Override + public Long getGroupsCountByNameContaining(RealmModel realm, String search) { + return getDelegate().getGroupsCountByNameContaining(realm, search); + } + @Override public List getTopLevelGroups(RealmModel realm) { String cacheKey = getTopGroupsQueryCacheKey(realm.getId()); @@ -909,9 +887,56 @@ public class RealmCacheSession implements CacheRealmProvider { } list.add(group); } + + list.sort(Comparator.comparing(GroupModel::getName)); + return list; } + @Override + public List getTopLevelGroups(RealmModel realm, Integer first, Integer max) { + String cacheKey = getTopGroupsQueryCacheKey(realm.getId() + first + max); + boolean queryDB = invalidations.contains(cacheKey) || listInvalidations.contains(realm.getId() + first + max); + if (queryDB) { + return getDelegate().getTopLevelGroups(realm, first, max); + } + + GroupListQuery query = cache.get(cacheKey, GroupListQuery.class); + if (Objects.nonNull(query)) { + logger.tracev("getTopLevelGroups cache hit: {0}", realm.getName()); + } + + if (Objects.isNull(query)) { + Long loaded = cache.getCurrentRevision(cacheKey); + List model = getDelegate().getTopLevelGroups(realm, first, max); + if (model == null) return null; + Set ids = new HashSet<>(); + for (GroupModel client : model) ids.add(client.getId()); + query = new GroupListQuery(loaded, cacheKey, realm, ids); + logger.tracev("adding realm getTopLevelGroups cache miss: realm {0} key {1}", realm.getName(), cacheKey); + cache.addRevisioned(query, startupRevision); + return model; + } + List list = new LinkedList<>(); + for (String id : query.getGroups()) { + GroupModel group = session.realms().getGroupById(id, realm); + if (Objects.isNull(group)) { + invalidations.add(cacheKey); + return getDelegate().getTopLevelGroups(realm); + } + list.add(group); + } + + list.sort(Comparator.comparing(GroupModel::getName)); + + return list; + } + + @Override + public List searchForGroupByName(RealmModel realm, String search, Integer first, Integer max) { + return getDelegate().searchForGroupByName(realm, search, first, max); + } + @Override public boolean removeGroup(RealmModel realm, GroupModel group) { invalidateGroup(group.getId(), realm.getId(), true); @@ -963,11 +988,9 @@ public class RealmCacheSession implements CacheRealmProvider { String groupId = eventToAdd.getId(); // Check if we have existing event with bigger priority - boolean eventAlreadyExists = invalidationEvents.stream().filter((InvalidationEvent event) -> { - - return (event.getId().equals(groupId)) && (event instanceof GroupAddedEvent || event instanceof GroupMovedEvent || event instanceof GroupRemovedEvent); - - }).findFirst().isPresent(); + boolean eventAlreadyExists = invalidationEvents.stream() + .anyMatch((InvalidationEvent event) -> (event.getId().equals(groupId)) && + (event instanceof GroupAddedEvent || event instanceof GroupMovedEvent || event instanceof GroupRemovedEvent)); if (!eventAlreadyExists) { invalidationEvents.add(eventToAdd); diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java index 3a85825e9d..3592a56c0d 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java @@ -17,6 +17,8 @@ package org.keycloak.models.jpa; +import com.sun.org.apache.xpath.internal.operations.Bool; +import org.apache.commons.codec.binary.StringUtils; import org.jboss.logging.Logger; import org.keycloak.common.util.Time; import org.keycloak.connections.jpa.util.JpaUtils; @@ -40,12 +42,7 @@ import org.keycloak.models.utils.KeycloakModelUtils; import javax.persistence.EntityManager; import javax.persistence.TypedQuery; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Set; +import java.util.*; import java.util.stream.Collectors; /** @@ -343,10 +340,29 @@ public class JpaRealmProvider implements RealmProvider { return ref.getGroups().stream() .map(g -> session.realms().getGroupById(g.getId(), realm)) + .sorted(Comparator.comparing(GroupModel::getName)) .collect(Collectors.collectingAndThen( Collectors.toList(), Collections::unmodifiableList)); } + @Override + 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(); + + return count; + } + + @Override + public Long getGroupsCountByNameContaining(RealmModel realm, String search) { + return (long) searchForGroupByName(realm, search, null, null).size(); + } + @Override public List getTopLevelGroups(RealmModel realm) { RealmEntity ref = em.getReference(RealmEntity.class, realm.getId()); @@ -354,10 +370,31 @@ public class JpaRealmProvider implements RealmProvider { return ref.getGroups().stream() .filter(g -> g.getParent() == null) .map(g -> session.realms().getGroupById(g.getId(), realm)) + .sorted(Comparator.comparing(GroupModel::getName)) .collect(Collectors.collectingAndThen( Collectors.toList(), Collections::unmodifiableList)); } + @Override + public List getTopLevelGroups(RealmModel realm, Integer first, Integer max) { + List groupIds = em.createNamedQuery("getTopLevelGroupIds", String.class) + .setParameter("realm", realm.getId()) + .setFirstResult(first) + .setMaxResults(max) + .getResultList(); + List list = new ArrayList<>(); + if(Objects.nonNull(groupIds) && !groupIds.isEmpty()) { + for (String id : groupIds) { + GroupModel group = getGroupById(id, realm); + list.add(group); + } + } + + list.sort(Comparator.comparing(GroupModel::getName)); + + return Collections.unmodifiableList(list); + } + @Override public boolean removeGroup(RealmModel realm, GroupModel group) { if (group == null) { @@ -547,6 +584,31 @@ public class JpaRealmProvider implements RealmProvider { return adapter; } + @Override + public List searchForGroupByName(RealmModel realm, String search, Integer first, Integer max) { + TypedQuery query = em.createNamedQuery("getGroupIdsByNameContaining", String.class) + .setParameter("realm", realm.getId()) + .setParameter("search", search); + if(Objects.nonNull(first) && Objects.nonNull(max)) { + query= query.setFirstResult(first).setMaxResults(max); + } + List groups = query.getResultList(); + if (Objects.isNull(groups)) return Collections.EMPTY_LIST; + List list = new ArrayList<>(); + for (String id : groups) { + GroupModel groupById = session.realms().getGroupById(id, realm); + while(Objects.nonNull(groupById.getParentId())) { + groupById = session.realms().getGroupById(groupById.getParentId(), realm); + } + if(!list.contains(groupById)) { + list.add(groupById); + } + } + list.sort(Comparator.comparing(GroupModel::getName)); + + return Collections.unmodifiableList(list); + } + @Override public ClientInitialAccessModel createClientInitialAccessModel(RealmModel realm, int expiration, int count) { RealmEntity realmEntity = em.find(RealmEntity.class, realm.getId()); @@ -595,7 +657,7 @@ public class JpaRealmProvider implements RealmProvider { List entities = query.getResultList(); return entities.stream() - .map(entity -> entityToModel(entity)) + .map(this::entityToModel) .collect(Collectors.toList()); } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java index cd814f4605..3b07a73e10 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java @@ -22,53 +22,13 @@ import org.keycloak.common.enums.SslRequired; import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.component.ComponentFactory; import org.keycloak.component.ComponentModel; -import org.keycloak.models.AuthenticationExecutionModel; -import org.keycloak.models.AuthenticationFlowModel; -import org.keycloak.models.AuthenticatorConfigModel; -import org.keycloak.models.ClientModel; -import org.keycloak.models.ClientTemplateModel; -import org.keycloak.models.GroupModel; -import org.keycloak.models.IdentityProviderMapperModel; -import org.keycloak.models.IdentityProviderModel; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.ModelException; -import org.keycloak.models.OTPPolicy; -import org.keycloak.models.PasswordPolicy; -import org.keycloak.models.RealmModel; -import org.keycloak.models.RequiredActionProviderModel; -import org.keycloak.models.RequiredCredentialModel; -import org.keycloak.models.RoleModel; -import org.keycloak.models.jpa.entities.AuthenticationExecutionEntity; -import org.keycloak.models.jpa.entities.AuthenticationFlowEntity; -import org.keycloak.models.jpa.entities.AuthenticatorConfigEntity; -import org.keycloak.models.jpa.entities.ClientEntity; -import org.keycloak.models.jpa.entities.ClientTemplateEntity; -import org.keycloak.models.jpa.entities.ComponentConfigEntity; -import org.keycloak.models.jpa.entities.ComponentEntity; -import org.keycloak.models.jpa.entities.GroupEntity; -import org.keycloak.models.jpa.entities.IdentityProviderEntity; -import org.keycloak.models.jpa.entities.IdentityProviderMapperEntity; -import org.keycloak.models.jpa.entities.RealmAttributeEntity; -import org.keycloak.models.jpa.entities.RealmAttributes; -import org.keycloak.models.jpa.entities.RealmEntity; -import org.keycloak.models.jpa.entities.RequiredActionProviderEntity; -import org.keycloak.models.jpa.entities.RequiredCredentialEntity; -import org.keycloak.models.jpa.entities.RoleEntity; +import org.keycloak.models.*; +import org.keycloak.models.jpa.entities.*; import org.keycloak.models.utils.ComponentUtil; import org.keycloak.models.utils.KeycloakModelUtils; import javax.persistence.EntityManager; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; +import java.util.*; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -360,7 +320,7 @@ public class RealmAdapter implements RealmModel, JpaModel { realm.setVerifyEmail(verifyEmail); em.flush(); } - + @Override public boolean isLoginWithEmailAllowed() { return realm.isLoginWithEmailAllowed(); @@ -372,7 +332,7 @@ public class RealmAdapter implements RealmModel, JpaModel { if (loginWithEmailAllowed) realm.setDuplicateEmailsAllowed(false); em.flush(); } - + @Override public boolean isDuplicateEmailsAllowed() { return realm.isDuplicateEmailsAllowed(); @@ -1726,11 +1686,31 @@ public class RealmAdapter implements RealmModel, JpaModel { return session.realms().getGroups(this); } + @Override + public Long getGroupsCount(Boolean onlyTopGroups) { + return session.realms().getGroupsCount(this, onlyTopGroups); + } + + @Override + public Long getGroupsCountByNameContaining(String search) { + return session.realms().getGroupsCountByNameContaining(this, search); + } + @Override public List getTopLevelGroups() { return session.realms().getTopLevelGroups(this); } + @Override + public List getTopLevelGroups(Integer first, Integer max) { + return session.realms().getTopLevelGroups(this, first, max); + } + + @Override + public List searchForGroupByName(String search, Integer first, Integer max) { + return session.realms().searchForGroupByName(this, search, first, max); + } + @Override public boolean removeGroup(GroupModel group) { return session.realms().removeGroup(this, group); diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/GroupEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/GroupEntity.java index 98b130a8cf..e01ad0c11e 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/GroupEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/GroupEntity.java @@ -17,19 +17,7 @@ package org.keycloak.models.jpa.entities; -import javax.persistence.Access; -import javax.persistence.AccessType; -import javax.persistence.CascadeType; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.FetchType; -import javax.persistence.Id; -import javax.persistence.JoinColumn; -import javax.persistence.ManyToOne; -import javax.persistence.NamedQueries; -import javax.persistence.NamedQuery; -import javax.persistence.OneToMany; -import javax.persistence.Table; +import javax.persistence.*; import java.util.ArrayList; import java.util.Collection; @@ -39,6 +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="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") }) @Entity @Table(name="KEYCLOAK_GROUP") diff --git a/server-spi-private/src/main/java/org/keycloak/models/cache/CachedRealmModel.java b/server-spi-private/src/main/java/org/keycloak/models/cache/CachedRealmModel.java index 16a965f79f..09111cc4bd 100644 --- a/server-spi-private/src/main/java/org/keycloak/models/cache/CachedRealmModel.java +++ b/server-spi-private/src/main/java/org/keycloak/models/cache/CachedRealmModel.java @@ -18,7 +18,6 @@ package org.keycloak.models.cache; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; -import org.keycloak.models.UserModel; import org.keycloak.provider.ProviderEvent; import java.util.concurrent.ConcurrentHashMap; diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java index 172147a20f..82089a1cc9 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java +++ b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java @@ -17,17 +17,6 @@ package org.keycloak.models.utils; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; - import org.keycloak.authorization.AuthorizationProvider; import org.keycloak.authorization.model.Policy; import org.keycloak.authorization.model.Resource; @@ -42,60 +31,15 @@ import org.keycloak.credential.CredentialModel; import org.keycloak.events.Event; import org.keycloak.events.admin.AdminEvent; import org.keycloak.events.admin.AuthDetails; -import org.keycloak.models.AuthenticatedClientSessionModel; -import org.keycloak.models.AuthenticationExecutionModel; -import org.keycloak.models.AuthenticationFlowModel; -import org.keycloak.models.AuthenticatorConfigModel; -import org.keycloak.models.ClientModel; -import org.keycloak.models.ClientTemplateModel; -import org.keycloak.models.FederatedIdentityModel; -import org.keycloak.models.GroupModel; -import org.keycloak.models.IdentityProviderMapperModel; -import org.keycloak.models.IdentityProviderModel; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.ModelException; -import org.keycloak.models.OTPPolicy; -import org.keycloak.models.ProtocolMapperModel; -import org.keycloak.models.RealmModel; -import org.keycloak.models.RequiredActionProviderModel; -import org.keycloak.models.RequiredCredentialModel; -import org.keycloak.models.RoleModel; -import org.keycloak.models.UserConsentModel; -import org.keycloak.models.UserCredentialModel; -import org.keycloak.models.UserModel; -import org.keycloak.models.UserSessionModel; +import org.keycloak.models.*; import org.keycloak.provider.ProviderConfigProperty; -import org.keycloak.representations.idm.AdminEventRepresentation; -import org.keycloak.representations.idm.AuthDetailsRepresentation; -import org.keycloak.representations.idm.AuthenticationExecutionExportRepresentation; -import org.keycloak.representations.idm.AuthenticationFlowRepresentation; -import org.keycloak.representations.idm.AuthenticatorConfigRepresentation; -import org.keycloak.representations.idm.ClientRepresentation; -import org.keycloak.representations.idm.ClientTemplateRepresentation; -import org.keycloak.representations.idm.ComponentRepresentation; -import org.keycloak.representations.idm.ConfigPropertyRepresentation; -import org.keycloak.representations.idm.CredentialRepresentation; -import org.keycloak.representations.idm.EventRepresentation; -import org.keycloak.representations.idm.FederatedIdentityRepresentation; -import org.keycloak.representations.idm.GroupRepresentation; -import org.keycloak.representations.idm.IdentityProviderMapperRepresentation; -import org.keycloak.representations.idm.IdentityProviderRepresentation; -import org.keycloak.representations.idm.ProtocolMapperRepresentation; -import org.keycloak.representations.idm.RealmEventsConfigRepresentation; -import org.keycloak.representations.idm.RealmRepresentation; -import org.keycloak.representations.idm.RequiredActionProviderRepresentation; -import org.keycloak.representations.idm.RoleRepresentation; -import org.keycloak.representations.idm.UserConsentRepresentation; -import org.keycloak.representations.idm.UserRepresentation; -import org.keycloak.representations.idm.UserSessionRepresentation; -import org.keycloak.representations.idm.authorization.AbstractPolicyRepresentation; -import org.keycloak.representations.idm.authorization.PolicyRepresentation; -import org.keycloak.representations.idm.authorization.ResourceOwnerRepresentation; -import org.keycloak.representations.idm.authorization.ResourceRepresentation; -import org.keycloak.representations.idm.authorization.ResourceServerRepresentation; -import org.keycloak.representations.idm.authorization.ScopeRepresentation; +import org.keycloak.representations.idm.*; +import org.keycloak.representations.idm.authorization.*; import org.keycloak.storage.StorageId; +import java.util.*; +import java.util.stream.Collectors; + /** * @author Bill Burke * @version $Revision: 1 $ @@ -131,12 +75,7 @@ public class ModelToRepresentation { } else { ClientModel client = (ClientModel)role.getContainer(); String clientId = client.getClientId(); - List currentClientRoles = clientRoleNames.get(clientId); - if (currentClientRoles == null) { - currentClientRoles = new ArrayList<>(); - clientRoleNames.put(clientId, currentClientRoles); - } - + List currentClientRoles = clientRoleNames.computeIfAbsent(clientId, k -> new ArrayList<>()); currentClientRoles.add(role.getName()); } } @@ -147,10 +86,32 @@ public class ModelToRepresentation { return rep; } + public static List searchForGroupByName(RealmModel realm, String search, Integer first, Integer max) { + List result = new LinkedList<>(); + List groups = realm.searchForGroupByName(search, first, max); + if (Objects.isNull(groups)) return result; + for (GroupModel group : groups) { + GroupRepresentation rep = toGroupHierarchy(group, false); + result.add(rep); + } + return result; + } + + public static List toGroupHierarchy(RealmModel realm, boolean full, Integer first, Integer max) { + List hierarchy = new LinkedList<>(); + List groups = realm.getTopLevelGroups(first, max); + if (Objects.isNull(groups)) return hierarchy; + for (GroupModel group : groups) { + GroupRepresentation rep = toGroupHierarchy(group, full); + hierarchy.add(rep); + } + return hierarchy; + } + public static List toGroupHierarchy(RealmModel realm, boolean full) { List hierarchy = new LinkedList<>(); List groups = realm.getTopLevelGroups(); - if (groups == null) return hierarchy; + if (Objects.isNull(groups)) return hierarchy; for (GroupModel group : groups) { GroupRepresentation rep = toGroupHierarchy(group, full); hierarchy.add(rep); @@ -188,9 +149,7 @@ public class ModelToRepresentation { List reqActions = new ArrayList(); Set requiredActions = user.getRequiredActions(); - for (String ra : requiredActions){ - reqActions.add(ra); - } + reqActions.addAll(requiredActions); rep.setRequiredActions(reqActions); @@ -644,11 +603,7 @@ public class ModelToRepresentation { Map> grantedProtocolMappers = new HashMap>(); for (ProtocolMapperModel protocolMapper : model.getGrantedProtocolMappers()) { String protocol = protocolMapper.getProtocol(); - List currentProtocolMappers = grantedProtocolMappers.get(protocol); - if (currentProtocolMappers == null) { - currentProtocolMappers = new LinkedList(); - grantedProtocolMappers.put(protocol, currentProtocolMappers); - } + List currentProtocolMappers = grantedProtocolMappers.computeIfAbsent(protocol, k -> new LinkedList()); currentProtocolMappers.add(protocolMapper.getName()); } @@ -661,11 +616,7 @@ public class ModelToRepresentation { ClientModel client2 = (ClientModel) role.getContainer(); String clientId2 = client2.getClientId(); - List currentClientRoles = grantedClientRoles.get(clientId2); - if (currentClientRoles == null) { - currentClientRoles = new LinkedList(); - grantedClientRoles.put(clientId2, currentClientRoles); - } + List currentClientRoles = grantedClientRoles.computeIfAbsent(clientId2, k -> new LinkedList()); currentClientRoles.add(role.getName()); } } diff --git a/server-spi/src/main/java/org/keycloak/models/RealmModel.java b/server-spi/src/main/java/org/keycloak/models/RealmModel.java index ff1cfd78bb..e88c59ff37 100755 --- a/server-spi/src/main/java/org/keycloak/models/RealmModel.java +++ b/server-spi/src/main/java/org/keycloak/models/RealmModel.java @@ -23,11 +23,7 @@ import org.keycloak.provider.ProviderEvent; import org.keycloak.storage.UserStorageProvider; import org.keycloak.storage.UserStorageProviderModel; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; /** * @author Bill Burke @@ -147,11 +143,11 @@ public interface RealmModel extends RoleContainerModel { boolean isVerifyEmail(); void setVerifyEmail(boolean verifyEmail); - + boolean isLoginWithEmailAllowed(); void setLoginWithEmailAllowed(boolean loginWithEmailAllowed); - + boolean isDuplicateEmailsAllowed(); void setDuplicateEmailsAllowed(boolean duplicateEmailsAllowed); @@ -404,7 +400,11 @@ public interface RealmModel extends RoleContainerModel { GroupModel getGroupById(String id); List getGroups(); + Long getGroupsCount(Boolean onlyTopGroups); + Long getGroupsCountByNameContaining(String search); List getTopLevelGroups(); + List getTopLevelGroups(Integer first, Integer max); + List searchForGroupByName(String search, Integer first, Integer max); boolean removeGroup(GroupModel group); void moveGroup(GroupModel group, GroupModel toParent); diff --git a/server-spi/src/main/java/org/keycloak/models/RealmProvider.java b/server-spi/src/main/java/org/keycloak/models/RealmProvider.java index f3a26f1158..d14f2d6256 100755 --- a/server-spi/src/main/java/org/keycloak/models/RealmProvider.java +++ b/server-spi/src/main/java/org/keycloak/models/RealmProvider.java @@ -40,8 +40,16 @@ public interface RealmProvider extends Provider { List getGroups(RealmModel realm); + Long getGroupsCount(RealmModel realm, Boolean onlyTopGroups); + + Long getGroupsCountByNameContaining(RealmModel realm, String search); + List getTopLevelGroups(RealmModel realm); + List getTopLevelGroups(RealmModel realm, Integer first, Integer max); + + List searchForGroupByName(RealmModel realm, String search, Integer first, Integer max); + boolean removeGroup(RealmModel realm, GroupModel group); GroupModel createGroup(RealmModel realm, String name); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/GroupsResource.java b/services/src/main/java/org/keycloak/services/resources/admin/GroupsResource.java index 2a1909b383..15be7ae029 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/GroupsResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/GroupsResource.java @@ -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; @@ -26,21 +27,22 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.representations.idm.GroupRepresentation; +import org.keycloak.services.ErrorResponse; -import javax.ws.rs.Consumes; -import javax.ws.rs.GET; -import javax.ws.rs.POST; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.Produces; +import javax.ws.rs.*; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; import java.net.URI; +import java.util.HashMap; import java.util.List; +import java.util.Map; +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 @@ -71,10 +73,22 @@ public class GroupsResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) - public List getGroups() { + public List getGroups(@QueryParam("search") String search, + @QueryParam("first") Integer firstResult, + @QueryParam("max") Integer maxResults) { auth.groups().requireList(); - return ModelToRepresentation.toGroupHierarchy(realm, false); + List results; + + if (Objects.nonNull(search)) { + results = ModelToRepresentation.searchForGroupByName(realm, search.trim(), firstResult, maxResults); + } else if(Objects.nonNull(firstResult) && Objects.nonNull(maxResults)) { + results = ModelToRepresentation.toGroupHierarchy(realm, false, firstResult, maxResults); + } else { + results = ModelToRepresentation.toGroupHierarchy(realm, false); + } + + return results; } /** @@ -94,6 +108,28 @@ public class GroupsResource { return resource; } + /** + * Returns the groups counts. + * + * @return + */ + @GET + @NoCache + @Path("count") + @Produces(MediaType.APPLICATION_JSON) + public Map getGroupCount(@QueryParam("search") String search, + @QueryParam("top") @DefaultValue("false") boolean onlyTopGroups) { + Long results; + Map map = new HashMap<>(); + if (Objects.nonNull(search)) { + results = realm.getGroupsCountByNameContaining(search); + } else { + results = realm.getGroupsCount(onlyTopGroups); + } + map.put("count", results); + return map; + } + /** * 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. @@ -105,13 +141,12 @@ public class GroupsResource { public Response addTopLevelGroup(GroupRepresentation rep) { auth.groups().requireManage(); - for (GroupModel group : realm.getGroups()) { - if (group.getName().equals(rep.getName())) { - return ErrorResponse.exists("Top level group named '" + rep.getName() + "' already exists."); - } + List search = ModelToRepresentation.searchForGroupByName(realm, rep.getName(), 0, 1); + if (search != null && !search.isEmpty() && Objects.equals(search.get(0).getName(), rep.getName())) { + return ErrorResponse.exists("Top level group named '" + rep.getName() + "' already exists."); } - - GroupModel child = null; + + GroupModel child; Response.ResponseBuilder builder = Response.status(204); if (rep.getId() != null) { child = realm.getGroupById(rep.getId()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupTest.java index b7274ecbe3..fd58f7590a 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupTest.java @@ -92,7 +92,6 @@ public class GroupTest extends AbstractGroupTest { user.setCredentials(credentials); users.add(user); - List clients = testRealmRep.getClients(); ClientRepresentation client = new ClientRepresentation(); @@ -155,16 +154,16 @@ public class GroupTest extends AbstractGroupTest { @Test public void doNotAllowSameGroupNameAtSameLevel() throws Exception { RealmResource realm = adminClient.realms().realm("test"); - + GroupRepresentation topGroup = new GroupRepresentation(); topGroup.setName("top"); topGroup = createGroup(realm, topGroup); - + GroupRepresentation anotherTopGroup = new GroupRepresentation(); anotherTopGroup.setName("top"); Response response = realm.groups().add(anotherTopGroup); assertEquals(409, response.getStatus()); // conflict status 409 - same name not allowed - + GroupRepresentation level2Group = new GroupRepresentation(); level2Group.setName("level2"); response = realm.groups().group(topGroup.getId()).subGroup(level2Group); @@ -177,7 +176,7 @@ public class GroupTest extends AbstractGroupTest { response.close(); assertEquals(409, response.getStatus()); // conflict status 409 - same name not allowed } - + @Test public void createAndTestGroups() throws Exception { RealmResource realm = adminClient.realms().realm("test"); @@ -206,7 +205,7 @@ public class GroupTest extends AbstractGroupTest { GroupRepresentation topGroup = new GroupRepresentation(); topGroup.setName("top"); topGroup = createGroup(realm, topGroup); - + List roles = new LinkedList<>(); roles.add(topRole); realm.groups().group(topGroup.getId()).roles().realmLevel().add(roles); @@ -634,4 +633,59 @@ public class GroupTest extends AbstractGroupTest { assertEquals(110, group.members(0, 1000).size()); assertEquals(110, group.members(-1, -2).size()); } + + @Test + public void searchAndCountGroups() throws Exception { + String firstGroupId = ""; + + RealmResource realm = adminClient.realms().realm("test"); + + // Clean up all test groups + for (GroupRepresentation group : realm.groups().groups()) { + GroupResource resource = realm.groups().group(group.getId()); + resource.remove(); + assertAdminEvents.assertEvent("test", OperationType.DELETE, AdminEventPaths.groupPath(group.getId()), ResourceType.GROUP); + } + + // Add 20 new groups with known names + for (int i=0;i<20;i++) { + GroupRepresentation group = new GroupRepresentation(); + group.setName("group"+i); + group = createGroup(realm, group); + if(i== 0) { + firstGroupId = group.getId(); + } + } + + // Get groups by search and pagination + List allGroups = realm.groups().groups(); + assertEquals(20, allGroups.size()); + + List slice = realm.groups().groups(5, 7); + assertEquals(7, slice.size()); + + List search = realm.groups().groups("group1",0,20); + assertEquals(11, search.size()); + for(GroupRepresentation group : search) { + assertTrue(group.getName().contains("group1")); + } + + List noResultSearch = realm.groups().groups("abcd",0,20); + assertEquals(0, noResultSearch.size()); + + // Count + assertEquals(new Long(allGroups.size()), realm.groups().count().get("count")); + assertEquals(new Long(search.size()), realm.groups().count("group1").get("count")); + assertEquals(new Long(noResultSearch.size()), realm.groups().count("abcd").get("count")); + + // Add a subgroup for onlyTopLevel flag testing + GroupRepresentation level2Group = new GroupRepresentation(); + level2Group.setName("group1111"); + Response response = realm.groups().group(firstGroupId).subGroup(level2Group); + response.close(); + assertAdminEvents.assertEvent("test", OperationType.CREATE, AdminEventPaths.groupSubgroupsPath(firstGroupId), level2Group, ResourceType.GROUP); + + assertEquals(new Long(allGroups.size()), realm.groups().count(true).get("count")); + assertEquals(new Long(allGroups.size() + 1), realm.groups().count(false).get("count")); + } } diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties index 1620996305..6cbba48c90 100644 --- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties +++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties @@ -1036,6 +1036,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 diff --git a/themes/src/main/resources/theme/base/admin/resources/js/app.js b/themes/src/main/resources/theme/base/admin/resources/js/app.js index 4e6de53a92..01ce2a5f4c 100755 --- a/themes/src/main/resources/theme/base/admin/resources/js/app.js +++ b/themes/src/main/resources/theme/base/admin/resources/js/app.js @@ -802,6 +802,9 @@ module.config([ '$routeProvider', function($routeProvider) { }, groups : function(GroupListLoader) { return GroupListLoader(); + }, + groupsCount : function(GroupCountLoader) { + return GroupCountLoader(); } }, controller : 'GroupListCtrl' @@ -1936,7 +1939,7 @@ module.factory('spinnerInterceptor', function($q, $window, $rootScope, $location $('#loading').hide(); } return response; - }, + }, responseError: function(response) { resourceRequests--; if (resourceRequests == 0) { @@ -1956,7 +1959,7 @@ module.factory('errorInterceptor', function($q, $window, $rootScope, $location, return { response: function(response) { return response; - }, + }, responseError: function(response) { if (response.status == 401) { Auth.authz.logout(); @@ -2198,17 +2201,17 @@ module.directive('kcEnter', function() { module.directive('kcSave', function ($compile, $timeout, Notifications) { var clickDelay = 500; // 500 ms - + return { restrict: 'A', link: function ($scope, elem, attr, ctrl) { elem.addClass("btn btn-primary"); elem.attr("type","submit"); - + var disabled = false; elem.on('click', function(evt) { if ($scope.hasOwnProperty("changed") && !$scope.changed) return; - + // KEYCLOAK-4121: Prevent double form submission if (disabled) { evt.preventDefault(); @@ -2218,7 +2221,7 @@ module.directive('kcSave', function ($compile, $timeout, Notifications) { disabled = true; $timeout(function () { disabled = false; }, clickDelay, false); } - + $scope.$apply(function() { var form = elem.closest('form'); if (form && form.attr('name')) { @@ -2881,35 +2884,35 @@ module.directive('kcOnReadFile', function ($parse) { module.controller('PagingCtrl', function ($scope) { $scope.currentPageInput = 1; - + $scope.firstPage = function() { if (!$scope.hasPrevious()) return; $scope.currentPage = 1; $scope.currentPageInput = 1; }; - + $scope.lastPage = function() { if (!$scope.hasNext()) return; $scope.currentPage = $scope.numberOfPages; $scope.currentPageInput = $scope.numberOfPages; }; - + $scope.previousPage = function() { if (!$scope.hasPrevious()) return; $scope.currentPage--; $scope.currentPageInput = $scope.currentPage; }; - + $scope.nextPage = function() { if (!$scope.hasNext()) return; $scope.currentPage++; $scope.currentPageInput = $scope.currentPage; }; - + $scope.hasNext = function() { return $scope.currentPage < $scope.numberOfPages; }; - + $scope.hasPrevious = function() { return $scope.currentPage > 1; }; @@ -2939,11 +2942,11 @@ module.directive('kcValidPage', function() { if (viewValue >= 1 && viewValue <= scope.numberOfPages) { scope.currentPage = viewValue; } - + return true; } } - } + } }); // filter used for paged tables diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/groups.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/groups.js index aa0cfadd15..bc3691f9df 100755 --- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/groups.js +++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/groups.js @@ -1,31 +1,102 @@ -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 = []; + 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 ' + queryParams); + }); + 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 ' + countParams); + }); + var promiseCountChain = promiseCount.promise.then(function(entry) { + groupsCount = entry; + $scope.numberOfPages = Math.ceil(groupsCount.count/$scope.pageSize); + }); + }; + + $scope.$watch('currentPage', function(newValue, oldValue) { + if(newValue !== oldValue) { + refreshGroups($scope.searchTerms); + } + }); + + $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; + 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 +112,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 +123,7 @@ module.controller('GroupListCtrl', function($scope, $route, realm, groups, Group }); }); - } + }; $scope.createGroup = function(selected) { var parent = 'realm'; @@ -61,13 +132,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 +148,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 +166,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 +191,7 @@ module.controller('GroupCreateCtrl', function($scope, $route, realm, parentId, G } - } + }; $scope.cancel = function() { $location.url("/realms/" + realm.realm + "/groups"); }; @@ -176,8 +247,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 +256,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("##"); } } } @@ -212,8 +281,8 @@ module.controller('GroupDetailCtrl', function(Dialog, $scope, realm, group, Grou }); module.controller('GroupRoleMappingCtrl', function($scope, $http, realm, group, clients, client, Notifications, GroupRealmRoleMapping, - GroupClientRoleMapping, GroupAvailableRealmRoleMapping, GroupAvailableClientRoleMapping, - GroupCompositeRealmRoleMapping, GroupCompositeClientRoleMapping) { + GroupClientRoleMapping, GroupAvailableRealmRoleMapping, GroupAvailableClientRoleMapping, + GroupCompositeRealmRoleMapping, GroupCompositeClientRoleMapping) { $scope.realm = realm; $scope.group = group; $scope.selectedRealmRoles = []; @@ -237,70 +306,70 @@ module.controller('GroupRoleMappingCtrl', function($scope, $http, realm, group, $scope.selectedRealmRoles = []; $http.post(authUrl + '/admin/realms/' + realm.realm + '/groups/' + group.id + '/role-mappings/realm', roles).then(function() { - $scope.realmMappings = GroupRealmRoleMapping.query({realm : realm.realm, groupId : group.id}); - $scope.realmRoles = GroupAvailableRealmRoleMapping.query({realm : realm.realm, groupId : group.id}); - $scope.realmComposite = GroupCompositeRealmRoleMapping.query({realm : realm.realm, groupId : group.id}); - $scope.selectedRealmMappings = []; - $scope.selectRealmRoles = []; - if ($scope.targetClient) { - console.log('load available'); - $scope.clientComposite = GroupCompositeClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id}); - $scope.clientRoles = GroupAvailableClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id}); - $scope.clientMappings = GroupClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id}); - $scope.selectedClientRoles = []; - $scope.selectedClientMappings = []; - } - Notifications.success("Role mappings updated."); + $scope.realmMappings = GroupRealmRoleMapping.query({realm : realm.realm, groupId : group.id}); + $scope.realmRoles = GroupAvailableRealmRoleMapping.query({realm : realm.realm, groupId : group.id}); + $scope.realmComposite = GroupCompositeRealmRoleMapping.query({realm : realm.realm, groupId : group.id}); + $scope.selectedRealmMappings = []; + $scope.selectRealmRoles = []; + if ($scope.targetClient) { + console.log('load available'); + $scope.clientComposite = GroupCompositeClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id}); + $scope.clientRoles = GroupAvailableClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id}); + $scope.clientMappings = GroupClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id}); + $scope.selectedClientRoles = []; + $scope.selectedClientMappings = []; + } + Notifications.success("Role mappings updated."); - }); + }); }; $scope.deleteRealmRole = function() { $http.delete(authUrl + '/admin/realms/' + realm.realm + '/groups/' + group.id + '/role-mappings/realm', {data : $scope.selectedRealmMappings, headers : {"content-type" : "application/json"}}).then(function() { - $scope.realmMappings = GroupRealmRoleMapping.query({realm : realm.realm, groupId : group.id}); - $scope.realmRoles = GroupAvailableRealmRoleMapping.query({realm : realm.realm, groupId : group.id}); - $scope.realmComposite = GroupCompositeRealmRoleMapping.query({realm : realm.realm, groupId : group.id}); - $scope.selectedRealmMappings = []; - $scope.selectRealmRoles = []; - if ($scope.targetClient) { - console.log('load available'); - $scope.clientComposite = GroupCompositeClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id}); - $scope.clientRoles = GroupAvailableClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id}); - $scope.clientMappings = GroupClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id}); - $scope.selectedClientRoles = []; - $scope.selectedClientMappings = []; - } - Notifications.success("Role mappings updated."); - }); + $scope.realmMappings = GroupRealmRoleMapping.query({realm : realm.realm, groupId : group.id}); + $scope.realmRoles = GroupAvailableRealmRoleMapping.query({realm : realm.realm, groupId : group.id}); + $scope.realmComposite = GroupCompositeRealmRoleMapping.query({realm : realm.realm, groupId : group.id}); + $scope.selectedRealmMappings = []; + $scope.selectRealmRoles = []; + if ($scope.targetClient) { + console.log('load available'); + $scope.clientComposite = GroupCompositeClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id}); + $scope.clientRoles = GroupAvailableClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id}); + $scope.clientMappings = GroupClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id}); + $scope.selectedClientRoles = []; + $scope.selectedClientMappings = []; + } + Notifications.success("Role mappings updated."); + }); }; $scope.addClientRole = function() { $http.post(authUrl + '/admin/realms/' + realm.realm + '/groups/' + group.id + '/role-mappings/clients/' + $scope.targetClient.id, $scope.selectedClientRoles).then(function() { - $scope.clientMappings = GroupClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id}); - $scope.clientRoles = GroupAvailableClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id}); - $scope.clientComposite = GroupCompositeClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id}); - $scope.selectedClientRoles = []; - $scope.selectedClientMappings = []; - $scope.realmComposite = GroupCompositeRealmRoleMapping.query({realm : realm.realm, groupId : group.id}); - $scope.realmRoles = GroupAvailableRealmRoleMapping.query({realm : realm.realm, groupId : group.id}); - Notifications.success("Role mappings updated."); - }); + $scope.clientMappings = GroupClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id}); + $scope.clientRoles = GroupAvailableClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id}); + $scope.clientComposite = GroupCompositeClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id}); + $scope.selectedClientRoles = []; + $scope.selectedClientMappings = []; + $scope.realmComposite = GroupCompositeRealmRoleMapping.query({realm : realm.realm, groupId : group.id}); + $scope.realmRoles = GroupAvailableRealmRoleMapping.query({realm : realm.realm, groupId : group.id}); + Notifications.success("Role mappings updated."); + }); }; $scope.deleteClientRole = function() { $http.delete(authUrl + '/admin/realms/' + realm.realm + '/groups/' + group.id + '/role-mappings/clients/' + $scope.targetClient.id, {data : $scope.selectedClientMappings, headers : {"content-type" : "application/json"}}).then(function() { - $scope.clientMappings = GroupClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id}); - $scope.clientRoles = GroupAvailableClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id}); - $scope.clientComposite = GroupCompositeClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id}); - $scope.selectedClientRoles = []; - $scope.selectedClientMappings = []; - $scope.realmComposite = GroupCompositeRealmRoleMapping.query({realm : realm.realm, groupId : group.id}); - $scope.realmRoles = GroupAvailableRealmRoleMapping.query({realm : realm.realm, groupId : group.id}); - Notifications.success("Role mappings updated."); - }); + $scope.clientMappings = GroupClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id}); + $scope.clientRoles = GroupAvailableClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id}); + $scope.clientComposite = GroupCompositeClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id}); + $scope.selectedClientRoles = []; + $scope.selectedClientMappings = []; + $scope.realmComposite = GroupCompositeRealmRoleMapping.query({realm : realm.realm, groupId : group.id}); + $scope.realmRoles = GroupAvailableRealmRoleMapping.query({realm : realm.realm, groupId : group.id}); + Notifications.success("Role mappings updated."); + }); }; @@ -332,13 +401,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 +415,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 +437,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 +452,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 +470,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 +484,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; diff --git a/themes/src/main/resources/theme/base/admin/resources/js/loaders.js b/themes/src/main/resources/theme/base/admin/resources/js/loaders.js index 044414c478..fb49c64f10 100755 --- a/themes/src/main/resources/theme/base/admin/resources/js/loaders.js +++ b/themes/src/main/resources/theme/base/admin/resources/js/loaders.js @@ -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 : 0, + max : 20 + } + }); +}); + +module.factory('GroupCountLoader', function(Loader, GroupsCount, $route, $q) { + return Loader.query(GroupsCount, function() { + return { + realm : $route.current.params.realm, + top : true } }); }); diff --git a/themes/src/main/resources/theme/base/admin/resources/js/services.js b/themes/src/main/resources/theme/base/admin/resources/js/services.js index a9935f4507..b68af10dc7 100755 --- a/themes/src/main/resources/theme/base/admin/resources/js/services.js +++ b/themes/src/main/resources/theme/base/admin/resources/js/services.js @@ -3,7 +3,7 @@ var module = angular.module('keycloak.services', [ 'ngResource', 'ngRoute' ]); module.service('Dialog', function($modal) { - var dialog = {}; + var dialog = {}; var openDialog = function(title, message, btns, template) { var controller = function($scope, $modalInstance, title, message, btns) { @@ -36,15 +36,15 @@ module.service('Dialog', function($modal) { }).result; } - var escapeHtml = function(str) { - var div = document.createElement('div'); - div.appendChild(document.createTextNode(str)); - return div.innerHTML; - }; + var escapeHtml = function(str) { + var div = document.createElement('div'); + div.appendChild(document.createTextNode(str)); + return div.innerHTML; + }; - dialog.confirmDelete = function(name, type, success) { - var title = 'Delete ' + escapeHtml(type.charAt(0).toUpperCase() + type.slice(1)); - var msg = 'Are you sure you want to permanently delete the ' + type + ' ' + name + '?'; + dialog.confirmDelete = function(name, type, success) { + var title = 'Delete ' + escapeHtml(type.charAt(0).toUpperCase() + type.slice(1)); + var msg = 'Are you sure you want to permanently delete the ' + type + ' ' + name + '?'; var btns = { ok: { label: 'Delete', @@ -57,7 +57,7 @@ module.service('Dialog', function($modal) { } openDialog(title, msg, btns, '/templates/kc-modal.html').then(success); - } + } dialog.confirmGenerateKeys = function(name, type, success) { var title = 'Generate new keys for realm'; @@ -138,10 +138,10 @@ module.service('CopyDialog', function($modal) { }); module.factory('Notifications', function($rootScope, $timeout) { - // time (in ms) the notifications are shown - var delay = 5000; + // time (in ms) the notifications are shown + var delay = 5000; - var notifications = {}; + var notifications = {}; notifications.current = { display: false }; notifications.current.remove = function() { if (notifications.scheduled) { @@ -157,9 +157,9 @@ module.factory('Notifications', function($rootScope, $timeout) { $rootScope.notification = notifications.current; - notifications.message = function(type, header, message) { + notifications.message = function(type, header, message) { notifications.current.remove(); - + notifications.current.type = type; notifications.current.header = header; notifications.current.message = message; @@ -170,25 +170,25 @@ module.factory('Notifications', function($rootScope, $timeout) { }, delay); console.debug("Added message"); - } + } - notifications.info = function(message) { - notifications.message("info", "Info!", message); - }; + notifications.info = function(message) { + notifications.message("info", "Info!", message); + }; - notifications.success = function(message) { - notifications.message("success", "Success!", message); - }; + notifications.success = function(message) { + notifications.message("success", "Success!", message); + }; - notifications.error = function(message) { - notifications.message("danger", "Error!", message); - }; + notifications.error = function(message) { + notifications.message("danger", "Error!", message); + }; - notifications.warn = function(message) { - notifications.message("warning", "Warning!", message); - }; + notifications.warn = function(message) { + notifications.message("warning", "Warning!", message); + }; - return notifications; + return notifications; }); @@ -256,12 +256,12 @@ module.factory('ComponentUtils', function() { }); module.factory('Realm', function($resource) { - return $resource(authUrl + '/admin/realms/:id', { - id : '@realm' - }, { - update : { - method : 'PUT' - }, + return $resource(authUrl + '/admin/realms/:id', { + id : '@realm' + }, { + update : { + method : 'PUT' + }, create : { method : 'POST', params : { id : ''} @@ -344,9 +344,9 @@ module.factory('RealmSMTPConnectionTester', function($resource) { realm : '@realm', config : '@config' }, { - send: { - method: 'POST' - } + send: { + method: 'POST' + } }); }); @@ -796,67 +796,67 @@ function roleControl($scope, realm, role, roles, clients, $scope.addRealmRole = function() { $scope.compositeSwitchDisabled=true; $http.post(authUrl + '/admin/realms/' + realm.realm + '/roles-by-id/' + role.id + '/composites', - $scope.selectedRealmRoles).then(function() { - for (var i = 0; i < $scope.selectedRealmRoles.length; i++) { - var role = $scope.selectedRealmRoles[i]; - var idx = $scope.realmRoles.indexOf($scope.selectedRealmRoles[i]); - if (idx != -1) { - $scope.realmRoles.splice(idx, 1); - $scope.realmMappings.push(role); - } + $scope.selectedRealmRoles).then(function() { + for (var i = 0; i < $scope.selectedRealmRoles.length; i++) { + var role = $scope.selectedRealmRoles[i]; + var idx = $scope.realmRoles.indexOf($scope.selectedRealmRoles[i]); + if (idx != -1) { + $scope.realmRoles.splice(idx, 1); + $scope.realmMappings.push(role); } - $scope.selectedRealmRoles = []; - Notifications.success("Role added to composite."); - }); + } + $scope.selectedRealmRoles = []; + Notifications.success("Role added to composite."); + }); }; $scope.deleteRealmRole = function() { $scope.compositeSwitchDisabled=true; $http.delete(authUrl + '/admin/realms/' + realm.realm + '/roles-by-id/' + role.id + '/composites', {data : $scope.selectedRealmMappings, headers : {"content-type" : "application/json"}}).then(function() { - for (var i = 0; i < $scope.selectedRealmMappings.length; i++) { - var role = $scope.selectedRealmMappings[i]; - var idx = $scope.realmMappings.indexOf($scope.selectedRealmMappings[i]); - if (idx != -1) { - $scope.realmMappings.splice(idx, 1); - $scope.realmRoles.push(role); - } + for (var i = 0; i < $scope.selectedRealmMappings.length; i++) { + var role = $scope.selectedRealmMappings[i]; + var idx = $scope.realmMappings.indexOf($scope.selectedRealmMappings[i]); + if (idx != -1) { + $scope.realmMappings.splice(idx, 1); + $scope.realmRoles.push(role); } - $scope.selectedRealmMappings = []; - Notifications.success("Role removed from composite."); - }); + } + $scope.selectedRealmMappings = []; + Notifications.success("Role removed from composite."); + }); }; $scope.addClientRole = function() { $scope.compositeSwitchDisabled=true; $http.post(authUrl + '/admin/realms/' + realm.realm + '/roles-by-id/' + role.id + '/composites', - $scope.selectedClientRoles).then(function() { - for (var i = 0; i < $scope.selectedClientRoles.length; i++) { - var role = $scope.selectedClientRoles[i]; - var idx = $scope.clientRoles.indexOf($scope.selectedClientRoles[i]); - if (idx != -1) { - $scope.clientRoles.splice(idx, 1); - $scope.clientMappings.push(role); - } + $scope.selectedClientRoles).then(function() { + for (var i = 0; i < $scope.selectedClientRoles.length; i++) { + var role = $scope.selectedClientRoles[i]; + var idx = $scope.clientRoles.indexOf($scope.selectedClientRoles[i]); + if (idx != -1) { + $scope.clientRoles.splice(idx, 1); + $scope.clientMappings.push(role); } - $scope.selectedClientRoles = []; - }); + } + $scope.selectedClientRoles = []; + }); }; $scope.deleteClientRole = function() { $scope.compositeSwitchDisabled=true; $http.delete(authUrl + '/admin/realms/' + realm.realm + '/roles-by-id/' + role.id + '/composites', {data : $scope.selectedClientMappings, headers : {"content-type" : "application/json"}}).then(function() { - for (var i = 0; i < $scope.selectedClientMappings.length; i++) { - var role = $scope.selectedClientMappings[i]; - var idx = $scope.clientMappings.indexOf($scope.selectedClientMappings[i]); - if (idx != -1) { - $scope.clientMappings.splice(idx, 1); - $scope.clientRoles.push(role); - } + for (var i = 0; i < $scope.selectedClientMappings.length; i++) { + var role = $scope.selectedClientMappings[i]; + var idx = $scope.clientMappings.indexOf($scope.selectedClientMappings[i]); + if (idx != -1) { + $scope.clientMappings.splice(idx, 1); + $scope.clientRoles.push(role); } - $scope.selectedClientMappings = []; - }); + } + $scope.selectedClientMappings = []; + }); }; @@ -1073,10 +1073,10 @@ module.factory('ClientTestNodesAvailable', function($resource) { module.factory('ClientCertificate', function($resource) { return $resource(authUrl + '/admin/realms/:realm/clients/:client/certificates/:attribute', { - realm : '@realm', - client : "@client", - attribute: "@attribute" - }); + realm : '@realm', + client : "@client", + attribute: "@attribute" + }); }); module.factory('ClientCertificateGenerate', function($resource) { @@ -1094,10 +1094,10 @@ module.factory('ClientCertificateGenerate', function($resource) { module.factory('ClientCertificateDownload', function($resource) { return $resource(authUrl + '/admin/realms/:realm/clients/:client/certificates/:attribute/download', { - realm : '@realm', - client : "@client", - attribute: "@attribute" - }, + realm : '@realm', + client : "@client", + attribute: "@attribute" + }, { download : { method : 'POST', @@ -1161,9 +1161,9 @@ module.factory('ClientInstallationJBoss', function($resource) { var url = authUrl + '/admin/realms/:realm/clients/:client/installation/jboss'; return { url : function(parameters) - { - return url.replace(':realm', parameters.realm).replace(':client', parameters.client); - } + { + return url.replace(':realm', parameters.realm).replace(':client', parameters.client); + } } }); @@ -1625,10 +1625,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) { diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/group-list.html b/themes/src/main/resources/theme/base/admin/resources/partials/group-list.html index efb62bfc9b..7ad6f657b4 100755 --- a/themes/src/main/resources/theme/base/admin/resources/partials/group-list.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/group-list.html @@ -1,37 +1,50 @@ -
- +
+ - - - - - - - - - + + + + + + + - - -
-
-
- - - - - -
-
-
+
+
+
+
+ +
+ +
+
+
+ +
+
+ + + + + +
+
+
+
+
-
-
- -
+
+ + + + +
+ +
+ \ No newline at end of file