[KEYCLOAK-14046] Include groups in user creation via Admin Console (#7035)
* [KEYCLOAK-14046] Include groups in user creation via Admin Console Since the POST /users API now supports providing groups membership, here is the UI part! - Added a field in the user creation UI to specify groups the newly created user will be joining - Added associated messages in english language * Added UI integration tests * Fixed UI tests * Flatten nested groups in user creation groups searchbox * Filtering out searched groups * Removed unused injection * Fixed UI tests Co-authored-by: Clement Cureau <clement.cureau@cdiscount.com>
This commit is contained in:
parent
cbb118c013
commit
0b68f24a09
13 changed files with 464 additions and 4 deletions
|
@ -93,7 +93,7 @@ public abstract class AbstractMultipleSelect2<R> {
|
|||
}
|
||||
|
||||
for (WebElement result : result) {
|
||||
if (result.getText().equalsIgnoreCase(id)) {
|
||||
if (match(result.getText(), id)) {
|
||||
clickLink(result);
|
||||
return;
|
||||
}
|
||||
|
@ -102,6 +102,10 @@ public abstract class AbstractMultipleSelect2<R> {
|
|||
|
||||
protected abstract Function<R, String> identity();
|
||||
|
||||
protected boolean match(String result, String search) {
|
||||
return result != null && result.equalsIgnoreCase(search);
|
||||
};
|
||||
|
||||
public Set<R> getSelected() {
|
||||
Set<R> values = new HashSet<>();
|
||||
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
package org.keycloak.testsuite.console.page.groups;
|
||||
|
||||
import org.jboss.arquillian.graphene.page.Page;
|
||||
import org.keycloak.testsuite.console.page.AdminConsoleCreate;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author clementcur
|
||||
*/
|
||||
public class CreateGroup extends AdminConsoleCreate {
|
||||
|
||||
public CreateGroup() {
|
||||
setEntity("group");
|
||||
}
|
||||
|
||||
@Page
|
||||
private CreateGroupForm form;
|
||||
|
||||
public CreateGroupForm form() {
|
||||
return form;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package org.keycloak.testsuite.console.page.groups;
|
||||
|
||||
import org.keycloak.representations.idm.GroupRepresentation;
|
||||
import org.keycloak.testsuite.page.Form;
|
||||
import org.keycloak.testsuite.util.UIUtils;
|
||||
import org.openqa.selenium.WebElement;
|
||||
import org.openqa.selenium.support.FindBy;
|
||||
import org.openqa.selenium.support.ui.Select;
|
||||
|
||||
import static org.keycloak.testsuite.util.UIUtils.getTextFromElement;
|
||||
import static org.keycloak.testsuite.util.UIUtils.getTextInputValue;
|
||||
import static org.keycloak.testsuite.util.WaitUtils.*;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author clementcur
|
||||
*/
|
||||
public class CreateGroupForm extends Form {
|
||||
|
||||
@FindBy(id = "name")
|
||||
private WebElement groupNameInput;
|
||||
|
||||
public void setValues(GroupRepresentation group) {
|
||||
waitUntilElement(groupNameInput).is().present();
|
||||
|
||||
setGroupName(group.getName());
|
||||
}
|
||||
|
||||
public String getGroupName() {
|
||||
return getTextInputValue(groupNameInput);
|
||||
}
|
||||
|
||||
public void setGroupName(String groupName) {
|
||||
UIUtils.setTextInputValue(groupNameInput, groupName);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* JBoss, Home of Professional Open Source
|
||||
*
|
||||
* Copyright 2013 Red Hat, Inc. and/or its affiliates.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.testsuite.console.page.groups;
|
||||
|
||||
import org.jboss.arquillian.drone.api.annotation.Drone;
|
||||
import org.keycloak.testsuite.console.page.AdminConsoleRealm;
|
||||
import org.keycloak.testsuite.console.page.fragment.DataTable;
|
||||
import org.openqa.selenium.WebDriver;
|
||||
import org.openqa.selenium.support.FindBy;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author clementcur
|
||||
*/
|
||||
public class Groups extends AdminConsoleRealm {
|
||||
|
||||
@Override
|
||||
public String getUriFragment() {
|
||||
return super.getUriFragment() + "/groups";
|
||||
}
|
||||
|
||||
public static final String NEW_GROUP = "New";
|
||||
|
||||
@FindBy(id = "group-table")
|
||||
private GroupsTable table;
|
||||
|
||||
public GroupsTable table() {
|
||||
return table;
|
||||
}
|
||||
|
||||
public class GroupsTable extends DataTable {
|
||||
|
||||
@Drone
|
||||
private WebDriver driver;
|
||||
|
||||
public void addGroup() {
|
||||
clickHeaderButton(NEW_GROUP);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -5,12 +5,18 @@ import org.keycloak.testsuite.console.page.fragment.MultipleStringSelect2;
|
|||
import org.keycloak.testsuite.console.page.fragment.OnOffSwitch;
|
||||
import org.keycloak.testsuite.page.Form;
|
||||
import org.keycloak.testsuite.util.UIUtils;
|
||||
import org.openqa.selenium.By;
|
||||
import org.openqa.selenium.WebElement;
|
||||
import org.openqa.selenium.support.FindBy;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.keycloak.testsuite.util.UIUtils.getTextFromElement;
|
||||
import static org.keycloak.testsuite.util.WaitUtils.waitUntilElement;
|
||||
|
||||
/**
|
||||
|
@ -41,6 +47,9 @@ public class UserAttributesForm extends Form {
|
|||
@FindBy(xpath = ".//div[@class='onoffswitch' and ./input[@id='emailVerified']]")
|
||||
private OnOffSwitch emailVerifiedSwitch;
|
||||
|
||||
@FindBy(id = "s2id_groups")
|
||||
private GroupSelect groupsInput;
|
||||
|
||||
@FindBy(id = "s2id_reqActions")
|
||||
private MultipleStringSelect2 requiredUserActionsSelect;
|
||||
|
||||
|
@ -103,6 +112,8 @@ public class UserAttributesForm extends Form {
|
|||
emailVerifiedSwitch.setOn(emailVerified);
|
||||
}
|
||||
|
||||
public void setGroups(Set<String> groups) { groupsInput.update(groups); }
|
||||
|
||||
public void addRequiredAction(String requiredAction) {
|
||||
requiredUserActionsSelect.select(requiredAction);
|
||||
}
|
||||
|
@ -121,8 +132,45 @@ public class UserAttributesForm extends Form {
|
|||
setLastName(user.getLastName());
|
||||
if (user.isEnabled() != null) setEnabled(user.isEnabled());
|
||||
if (user.isEmailVerified() != null) setEmailVerified(user.isEmailVerified());
|
||||
if (user.getGroups() != null && user.getGroups().size() > 0) setGroups(new HashSet<String>(user.getGroups()));
|
||||
if (user.getRequiredActions() != null) setRequiredActions(new HashSet<>(user.getRequiredActions()));
|
||||
}
|
||||
|
||||
// TODO Contact Information section
|
||||
|
||||
public class GroupSelect extends MultipleStringSelect2 {
|
||||
|
||||
@Override
|
||||
protected List<WebElement> getSelectedElements() {
|
||||
return getRoot().findElements(By.xpath("(//table[@id='selected-groups'])/tbody/tr")).stream()
|
||||
.filter(webElement -> webElement.findElements(By.tagName("td")).size() > 1)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BiFunction<WebElement, String, Boolean> deselect() {
|
||||
return (webElement, name) -> {
|
||||
List<WebElement> tds = webElement.findElements(By.tagName("td"));
|
||||
|
||||
if (!getTextFromElement(tds.get(0)).isEmpty()) {
|
||||
if (getTextFromElement(tds.get(0)).equals(name)) {
|
||||
tds.get(1).findElement(By.tagName("button")).click();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Function<WebElement, String> representation() {
|
||||
return webElement -> getTextFromElement(webElement.findElements(By.tagName("td")).get(0));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean match(String result, String search) {
|
||||
return result != null && result.equalsIgnoreCase("/" + search);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
package org.keycloak.testsuite.console.groups;
|
||||
|
||||
import org.jboss.arquillian.graphene.page.Page;
|
||||
import org.junit.Before;
|
||||
import org.keycloak.admin.client.resource.GroupResource;
|
||||
import org.keycloak.admin.client.resource.GroupsResource;
|
||||
import org.keycloak.representations.idm.GroupRepresentation;
|
||||
import org.keycloak.testsuite.console.AbstractConsoleTest;
|
||||
import org.keycloak.testsuite.console.page.groups.CreateGroup;
|
||||
import org.keycloak.testsuite.console.page.groups.Groups;
|
||||
|
||||
import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlEquals;
|
||||
import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author clementcur
|
||||
*/
|
||||
public abstract class AbstractGroupTest extends AbstractConsoleTest {
|
||||
|
||||
@Page
|
||||
protected Groups groupsPage;
|
||||
@Page
|
||||
protected CreateGroup createGroupPage;
|
||||
|
||||
protected GroupRepresentation newTestRealmGroup;
|
||||
|
||||
@Before
|
||||
public void beforeGroupTest() {
|
||||
newTestRealmGroup = new GroupRepresentation();
|
||||
}
|
||||
|
||||
public void createGroup(GroupRepresentation group) {
|
||||
assertCurrentUrlEquals(groupsPage);
|
||||
groupsPage.table().addGroup();
|
||||
assertCurrentUrlStartsWith(createGroupPage);
|
||||
createGroupPage.form().setValues(group);
|
||||
createGroupPage.form().save();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* JBoss, Home of Professional Open Source
|
||||
*
|
||||
* Copyright 2013 Red Hat, Inc. and/or its affiliates.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.testsuite.console.groups;
|
||||
|
||||
import org.jboss.arquillian.graphene.page.Page;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.representations.idm.GroupRepresentation;
|
||||
import org.keycloak.testsuite.console.page.groups.CreateGroup;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author clementcur
|
||||
*/
|
||||
public class GroupTest extends AbstractGroupTest {
|
||||
|
||||
@Page
|
||||
private CreateGroup createGroupPage;
|
||||
|
||||
@Before
|
||||
public void beforeCreateGroupTest() {
|
||||
groupsPage.navigateTo();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createAGroup() {
|
||||
newTestRealmGroup.setName("mygroup");
|
||||
createGroup(newTestRealmGroup);
|
||||
assertAlertSuccess();
|
||||
}
|
||||
}
|
|
@ -22,10 +22,18 @@ import org.junit.Before;
|
|||
import org.junit.Test;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.representations.idm.GroupRepresentation;
|
||||
import org.keycloak.testsuite.console.page.users.UserAttributes;
|
||||
import org.keycloak.testsuite.console.page.groups.CreateGroup;
|
||||
import org.keycloak.testsuite.console.page.groups.Groups;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import static org.keycloak.testsuite.admin.Users.setPasswordFor;
|
||||
import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlEquals;
|
||||
import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith;
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -37,6 +45,12 @@ public class UserAttributesTest extends AbstractUserTest {
|
|||
@Page
|
||||
private UserAttributes userAttributesPage;
|
||||
|
||||
@Page
|
||||
protected Groups groupsPage;
|
||||
|
||||
@Page
|
||||
protected CreateGroup createGroupPage;
|
||||
|
||||
@Before
|
||||
public void beforeUserAttributesTest() {
|
||||
usersPage.navigateTo();
|
||||
|
@ -99,4 +113,31 @@ public class UserAttributesTest extends AbstractUserTest {
|
|||
// TODO try to log in
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createUserWithGroups() {
|
||||
GroupRepresentation newGroup = new GroupRepresentation();
|
||||
newGroup.setName("mygroup");
|
||||
|
||||
// navigate to Groups creation page
|
||||
groupsPage.navigateTo();
|
||||
assertCurrentUrlEquals(groupsPage);
|
||||
groupsPage.table().addGroup();
|
||||
assertCurrentUrlStartsWith(createGroupPage);
|
||||
|
||||
// create the group
|
||||
createGroupPage.form().setValues(newGroup);
|
||||
createGroupPage.form().save();
|
||||
assertAlertSuccess();
|
||||
|
||||
// navigate to Users creation page
|
||||
usersPage.navigateTo();
|
||||
RealmRepresentation representation = testRealmResource().toRepresentation();
|
||||
representation.setRegistrationEmailAsUsername(true);
|
||||
testRealmResource().update(representation);
|
||||
newTestRealmUser.setEmail("test-with-groups@keycloak.org");
|
||||
newTestRealmUser.setGroups(Arrays.asList("mygroup"));
|
||||
createUser(newTestRealmUser);
|
||||
assertAlertSuccess();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1412,6 +1412,11 @@ unlock-user=Unlock user
|
|||
federation-link=Federation Link
|
||||
email-verified=Email Verified
|
||||
email-verified.tooltip=Has the user's email been verified?
|
||||
groups-joining=Groups
|
||||
groups-joining.tooltip=Groups the user will be joining. To add a group, search for any existing one and select it.
|
||||
groups-joining-select.placeholder=Select existing group
|
||||
groups-joining-no-selected=No group selected
|
||||
groups-joining-path=Path
|
||||
required-user-actions=Required User Actions
|
||||
required-user-actions.tooltip=Require an action when the user logs in. 'Verify email' sends an email to the user to verify their email address. 'Update profile' requires user to enter in new personal information. 'Update password' requires user to enter in a new password. 'Configure OTP' requires setup of a mobile password generator.
|
||||
locale=Locale
|
||||
|
|
|
@ -385,14 +385,15 @@ module.controller('UserDetailCtrl', function($scope, realm, user, BruteForceUser
|
|||
Components,
|
||||
UserImpersonation, RequiredActions,
|
||||
UserStorageOperations,
|
||||
$location, $http, Dialog, Notifications, $translate) {
|
||||
$location, $http, Dialog, Notifications, $translate, Groups) {
|
||||
$scope.realm = realm;
|
||||
$scope.create = !user.id;
|
||||
$scope.editUsername = $scope.create || $scope.realm.editUsernameAllowed;
|
||||
$scope.emailAsUsername = $scope.realm.registrationEmailAsUsername;
|
||||
$scope.groupSearch = { selectedGroup : null };
|
||||
|
||||
if ($scope.create) {
|
||||
$scope.user = { enabled: true, attributes: {} }
|
||||
$scope.user = { enabled: true, attributes: {}, groups: [] }
|
||||
} else {
|
||||
if (!user.attributes) {
|
||||
user.attributes = {}
|
||||
|
@ -464,6 +465,8 @@ module.controller('UserDetailCtrl', function($scope, realm, user, BruteForceUser
|
|||
convertAttributeValuesToLists();
|
||||
|
||||
if ($scope.create) {
|
||||
pushSelectedGroupsToUser();
|
||||
|
||||
User.save({
|
||||
realm: realm.realm
|
||||
}, $scope.user, function (data, headers) {
|
||||
|
@ -513,6 +516,51 @@ module.controller('UserDetailCtrl', function($scope, realm, user, BruteForceUser
|
|||
}
|
||||
}
|
||||
|
||||
function pushSelectedGroupsToUser() {
|
||||
var groups = $scope.user.groups;
|
||||
if ($scope.selectedGroups) {
|
||||
for (i = 0; i < $scope.selectedGroups.length; i++) {
|
||||
var groupPath = $scope.selectedGroups[i].path;
|
||||
if (!groups.includes(groupPath)) {
|
||||
groups.push(groupPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function bfs(tree, collection) {
|
||||
if (!tree["subGroups"] || tree["subGroups"].length === 0) return;
|
||||
for (var i=0; i < tree["subGroups"].length; i++) {
|
||||
var child = tree["subGroups"][i]
|
||||
collection.push(child);
|
||||
bfs(child, collection);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
function flattenGroups(groups) {
|
||||
var flattenedGroups = [];
|
||||
if (!groups || groups.length === 0) return groups;
|
||||
for (var i=0; i < groups.length; i++) {
|
||||
flattenedGroups.push(groups[i]);
|
||||
bfs(groups[i], flattenedGroups);
|
||||
}
|
||||
|
||||
return flattenedGroups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Only keep groups that :
|
||||
* - include the search term in their path
|
||||
* - are not already selected
|
||||
*/
|
||||
function filterSearchedGroups(groups, term, selectedGroups) {
|
||||
if (!groups || groups.length === 0) return groups;
|
||||
if (!selectedGroups) selectedGroups = [];
|
||||
|
||||
return groups.filter(group => group.path?.includes(term) && !selectedGroups.some(selGroup => selGroup.id === group.id));
|
||||
}
|
||||
|
||||
$scope.reset = function() {
|
||||
$scope.user = angular.copy(user);
|
||||
$scope.changed = false;
|
||||
|
@ -530,6 +578,67 @@ module.controller('UserDetailCtrl', function($scope, realm, user, BruteForceUser
|
|||
$scope.removeAttribute = function(key) {
|
||||
delete $scope.user.attributes[key];
|
||||
}
|
||||
|
||||
$scope.groupsUiSelect = {
|
||||
minimumInputLength: 1,
|
||||
delay: 500,
|
||||
allowClear: true,
|
||||
query: function (query) {
|
||||
var data = {results: []};
|
||||
if ('' == query.term.trim()) {
|
||||
query.callback(data);
|
||||
return;
|
||||
}
|
||||
$scope.query = {
|
||||
realm: realm.realm,
|
||||
search: query.term.trim(),
|
||||
max : 20,
|
||||
first : 0
|
||||
};
|
||||
Groups.query($scope.query, function(response) {
|
||||
data.results = filterSearchedGroups(flattenGroups(response), query.term.trim(), $scope.selectedGroups);
|
||||
query.callback(data);
|
||||
});
|
||||
},
|
||||
formatResult: function(object, container, query) {
|
||||
object.text = object.path;
|
||||
return object.path;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.removeGroup = function(list, group) {
|
||||
for (i = 0; i < angular.copy(list).length; i++) {
|
||||
if (group.id == list[i].id) {
|
||||
list.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$scope.selectGroup = function(group) {
|
||||
if (!group || !group.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.groupSearch.selectedGroup = group;
|
||||
|
||||
if (!$scope.selectedGroups) {
|
||||
$scope.selectedGroups = [];
|
||||
}
|
||||
|
||||
for (i = 0; i < $scope.selectedGroups.length; i++) {
|
||||
if ($scope.selectedGroups[i].id == group.id) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$scope.selectedGroups.push(group);
|
||||
$scope.groupSearch.selectedGroup = null;
|
||||
}
|
||||
|
||||
$scope.clearGroupSelection = function() {
|
||||
$scope.groupSearch.selectedGroup = null;
|
||||
$('#groups').val(null).trigger('change.select2');
|
||||
}
|
||||
});
|
||||
|
||||
module.controller('UserCredentialsCtrl', function($scope, realm, user, $route, $location, RequiredActions, User, UserExecuteActionsEmail,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<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" style="margin-bottom: 0">
|
||||
<table class="table table-striped table-bordered" id="group-table" style="margin-bottom: 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="kc-table-actions" colspan="5">
|
||||
|
|
|
@ -97,6 +97,42 @@
|
|||
</div>
|
||||
<kc-tooltip>{{:: 'email-verified.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
<div class="form-group clearfix block" data-ng-if="create">
|
||||
<label class="col-md-2 control-label" for="selected-groups">{{:: 'groups-joining' | translate}}</label>
|
||||
<div class="col-sm-6">
|
||||
<table class="table table-striped table-bordered" style="margin-top: 0px" id="selected-groups">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="kc-table-actions" colspan="2">
|
||||
<div class="form-inline col-md-12 select2-container-single">
|
||||
<div class="form-group">
|
||||
<div class="input-group">
|
||||
<input type="hidden" ui-select2="groupsUiSelect" id="groups" data-ng-change="selectGroup(groupSearch.selectedGroup);" data-ng-model="groupSearch.selectedGroup" data-placeholder="{{:: 'groups-joining-select.placeholder' | translate}}..."/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
<tr data-ng-hide="!selectedGroups || selectedGroups.length == 0">
|
||||
<th>{{:: 'groups-joining-path' | translate}}</th>
|
||||
<th width="20%">{{:: 'actions' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="group in selectedGroups">
|
||||
<td>{{group.path}}</td>
|
||||
<td class="kc-action-cell" ng-click="removeGroup(selectedGroups, group);" style="vertical-align: middle">
|
||||
{{:: 'remove' | translate}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr data-ng-show="!selectedGroups || selectedGroups.length == 0">
|
||||
<td class="text-muted" colspan="2">{{:: 'groups-joining-no-selected' | translate}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'groups-joining.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
<div class="form-group clearfix">
|
||||
<label class="col-md-2 control-label" for="reqActions">{{:: 'required-user-actions' | translate}}</label>
|
||||
|
||||
|
|
|
@ -190,6 +190,20 @@ th.w-40 {
|
|||
height: 26px;
|
||||
}
|
||||
|
||||
.select2-container-single {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.select2-container-single .form-group {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.select2-container-single .form-group .input-group {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/*********** html select ********/
|
||||
.overflow-select {
|
||||
overflow: auto;
|
||||
|
|
Loading…
Reference in a new issue