KEYCLOAK-18875 UI for managing group of attributes

This commit is contained in:
Joerg Matysiak 2021-07-07 17:35:32 +02:00 committed by Stian Thorgersen
parent ac92e600fc
commit acb2ac1c8d
3 changed files with 280 additions and 28 deletions

View file

@ -1919,6 +1919,7 @@ dialogs.delete.message=Are you sure you want to permanently delete the {{type}}
dialogs.delete.confirm=Delete
dialogs.cancel=Cancel
dialogs.ok=Ok
use=Use
user.profile.attribute=Attribute
user.profile.attribute.name=Name
@ -1947,4 +1948,10 @@ user.profile.attribute.validation.add.validator=Add Validator
user.profile.attribute.validation.add.validator.tooltip=Select a validator to enforce specific constraints to the attribute value.
user.profile.attribute.validation.no.validators=No validators.
user.profile.attribute.annotation=Annotation
use=Use
user.profile.attribute.group=Attribute Group
attribute-groups=Attribute Groups
user.profile.attributegroup.displayHeader=Display header
user.profile.attributegroup.displayDescription=Display description
user.profile.attributegroup=Attribute Group
user.profile.attributegroup.name=Name
user.profile.attributegroup.annotation=Annotation

View file

@ -1415,23 +1415,38 @@ module.controller('RealmUserProfileCtrl', function($scope, Realm, realm, clientS
$scope.validatorProviders = serverInfo.componentTypes['org.keycloak.validate.Validator'];
$scope.isShowAttributes = true;
$scope.isShowAttributeGroups = false;
$scope.isShowJsonEditor = false;
UserProfile.get({realm: realm.realm}, function(config) {
$scope.config = config;
$scope.rawConfig = angular.toJson(config, true);
});
$scope.isShowAttributes = true;
$scope.isShowAttributeGroups = false;
$scope.isShowJsonEditor = false;
$scope.showAttributes = function() {
$route.reload();
delete $scope.currentAttributeGroup;
}
$scope.showAttributeGroups = function() {
$scope.isShowAttributes = false;
$scope.isShowAttributeGroups = true;
$scope.isShowJsonEditor = false;
delete $scope.currentAttribute;
}
$scope.showJsonEditor = function() {
$scope.isShowAttributes = false;
$scope.isShowAttributeGroups = false;
$scope.isShowJsonEditor = true;
delete $scope.currentAttribute;
delete $scope.currentAttributeGroup;
}
$scope.isRequiredRoles = {
minimumInputLength: 0,
delay: 500,
@ -1516,13 +1531,17 @@ module.controller('RealmUserProfileCtrl', function($scope, Realm, realm, clientS
};
$scope.attributeSelected = false;
$scope.showListing = function() {
$scope.showAttributeListing = function() {
return !$scope.attributeSelected && $scope.currentAttribute == null && $scope.isShowAttributes;
}
$scope.create = function() {
$scope.isCreate = true;
$scope.showAttributeGroupListing = function() {
return !$scope.attributeGroupSelected && $scope.currentAttributeGroup == null && $scope.isShowAttributeGroups;
}
$scope.createAttribute = function() {
$scope.isCreateAttribute = true;
$scope.currentAttribute = {
selector: {
scopes: []
@ -1538,6 +1557,11 @@ module.controller('RealmUserProfileCtrl', function($scope, Realm, realm, clientS
};
};
$scope.createAttributeGroup = function() {
$scope.isCreateAttributeGroup = true;
$scope.currentAttributeGroup = {};
};
$scope.isNotUsernameOrEmail = function(attributeName) {
return attributeName != "username" && attributeName != "email";
};
@ -1555,6 +1579,19 @@ module.controller('RealmUserProfileCtrl', function($scope, Realm, realm, clientS
$scope.save();
}
$scope.groupOrderUp = function(index) {
$scope.moveAttributeGroup(index, index - 1);
};
$scope.groupOrderDown = function(index) {
$scope.moveAttributeGroup(index, index + 1);
};
$scope.moveAttributeGroup = function(old_index, new_index){
$scope.config.groups.splice(new_index, 0, $scope.config.groups.splice(old_index, 1)[0]);
$scope.save(false);
}
$scope.removeAttribute = function(attribute) {
Dialog.confirmDelete(attribute.name, 'attribute', function() {
let newAttributes = [];
@ -1570,7 +1607,22 @@ module.controller('RealmUserProfileCtrl', function($scope, Realm, realm, clientS
});
};
$scope.addAnnotation = function() {
$scope.removeAttributeGroup = function(attributeGroup) {
Dialog.confirmDelete(attributeGroup.name, 'group', function() {
let newGroups = [];
for (var v of $scope.config.groups) {
if (v != attributeGroup) {
newGroups.push(v);
}
}
$scope.config.groups = newGroups;
$scope.save();
});
};
$scope.addAttributeAnnotation = function() {
if (!$scope.currentAttribute.annotations) {
$scope.currentAttribute.annotations = {};
}
@ -1578,11 +1630,23 @@ module.controller('RealmUserProfileCtrl', function($scope, Realm, realm, clientS
delete $scope.newAnnotation;
}
$scope.removeAnnotation = function(key) {
$scope.removeAttributeAnnotation = function(key) {
delete $scope.currentAttribute.annotations[key];
}
$scope.edit = function(attribute) {
$scope.addAttributeGroupAnnotation = function() {
if (!$scope.currentAttributeGroup.annotations) {
$scope.currentAttributeGroup.annotations = {};
}
$scope.currentAttributeGroup.annotations[$scope.newAttributeGroupAnnotation.key] = $scope.newAttributeGroupAnnotation.value;
delete $scope.newGroupAnnotation;
}
$scope.removeAttributeGroupAnnotation = function(key) {
delete $scope.currentAttributeGroup.annotations[key];
}
$scope.editAttribute = function(attribute) {
if (attribute.permissions == null) {
attribute.permissions = {
view: [],
@ -1628,6 +1692,20 @@ module.controller('RealmUserProfileCtrl', function($scope, Realm, realm, clientS
$scope.attributeSelected = true;
};
$scope.editAttributeGroup = function(attributeGroup) {
$scope.currentAttributeGroup = attributeGroup;
$scope.attributeGroupSelected = true;
};
$scope.groupIsReferencedInAnyAttribute = function(group) {
for (var currentAttribute of $scope.config.attributes) {
if (currentAttribute.group === group.name) {
return true
}
}
return false;
}
$scope.$watch('isRequired', function() {
if ($scope.isRequired) {
$scope.currentAttribute.required = {
@ -1720,8 +1798,19 @@ module.controller('RealmUserProfileCtrl', function($scope, Realm, realm, clientS
$scope.currentAttribute.validations = newValidators;
};
$scope.reloadConfigurationFromUserProfile = function () {
UserProfile.get({realm: realm.realm}, function(config) {
$scope.config = config;
$scope.rawConfig = angular.toJson(config, true);
});
}
$scope.save = function() {
if (!$scope.isShowAttributes) {
$scope.save(true)
}
$scope.save = function(reload) {
if ($scope.isShowJsonEditor) {
$scope.config = JSON.parse($scope.rawConfig);
}
@ -1740,26 +1829,52 @@ module.controller('RealmUserProfileCtrl', function($scope, Realm, realm, clientS
$scope.currentAttribute.selector.scopes.push($scope.selectorByScope[i].name);
}
if ($scope.isCreate) {
if ($scope.isCreateAttribute) {
$scope.config['attributes'].push($scope.currentAttribute);
}
}
if ($scope.currentAttributeGroup) {
if ($scope.config['groups'] == null) {
$scope.config['groups'] = []
}
if ($scope.isCreateAttributeGroup) {
$scope.config['groups'].push($scope.currentAttributeGroup);
}
}
UserProfile.update({realm: realm.realm},
$scope.config, function () {
$scope.attributeSelected = false;
delete $scope.currentAttribute;
delete $scope.isCreate;
delete $scope.isCreateAttribute
delete $scope.attributeSelected;
delete $scope.currentAttributeGroup;
delete $scope.isCreateAttributeGroup;
delete $scope.attributeGroupSelected;
delete $scope.isRequired;
delete $scope.canUserView;
delete $scope.canAdminView;
delete $scope.canUserEdit;
delete $scope.canAdminEdit;
$route.reload();
if (reload) {
$route.reload();
} else {
$scope.reloadConfigurationFromUserProfile();
}
Notifications.success("User Profile configuration has been saved.");
});
};
$scope.cancelEditAttributeGroup = function() {
delete $scope.currentAttributeGroup;
delete $scope.isCreateAttributeGroup;
delete $scope.attributeGroupSelected;
$scope.reloadConfigurationFromUserProfile();
}
$scope.reset = function() {
$route.reload();
};

View file

@ -3,19 +3,22 @@
<ul class="nav nav-tabs nav-tabs-pf">
<li ng-class="{active: isShowAttributes}"><a href="" data-ng-click="showAttributes()">{{:: 'attributes' | translate}}</a></li>
<li ng-class="{active: !isShowAttributes}"><a href=""
<li ng-class="{active: isShowAttributeGroups}"><a href=""
data-ng-click="showAttributeGroups()">{{:: 'attribute-groups' | translate}}</a>
<li ng-class="{active: isShowJsonEditor}"><a href=""
data-ng-click="showJsonEditor()">{{:: 'client-profiles-json-editor' | translate}}</a>
</li>
</ul>
<div data-ng-show="showListing()">
<div data-ng-show="showAttributeListing()">
<table class="datatable table table-striped table-bordered dataTable no-footer">
<thead>
<tr>
<th class="kc-table-actions" colspan="4">
<th class="kc-table-actions" colspan="5">
<div class="form-inline">
<div class="pull-right" data-ng-show="access.manageClients">
<button class="btn btn-default" data-ng-click="create()">
<button class="btn btn-default" data-ng-click="createAttribute()">
{{:: 'create' | translate}}
</button>
</div>
@ -24,7 +27,8 @@
</tr>
<tr>
<th width="25%">{{:: 'name' | translate}}</th>
<th width="65%">{{:: 'user.profile.attribute.displayName' | translate}}</th>
<th width="40%">{{:: 'user.profile.attribute.displayName' | translate}}</th>
<th width="25%">{{:: 'user.profile.attribute.group' | translate}}</th>
<th colspan="2">{{:: 'actions' | translate}}</th>
</tr>
</thead>
@ -33,18 +37,58 @@
<td class="kc-sorter">
<button data-ng-hide="flow.builtIn" data-ng-disabled="$first" class="btn btn-default btn-sm" data-ng-click="guiOrderUp($index)"><i class="fa fa-angle-up"></i></button>
<button data-ng-hide="flow.builtIn" data-ng-disabled="$last" class="btn btn-default btn-sm" data-ng-click="guiOrderDown($index)"><i class="fa fa-angle-down"></i></button>
<span><a href="" data-ng-click="edit(attribute)">{{attribute.name}}</a></span>
<span><a href="" data-ng-click="editAttribute(attribute)">{{attribute.name}}</a></span>
</td>
<td>{{attribute.displayName}}</td>
<td class="kc-action-cell" data-ng-click="edit(attribute)">{{:: 'edit' | translate}}</td>
<td>{{attribute.group}}</td>
<td class="kc-action-cell" data-ng-click="editAttribute(attribute)">{{:: 'edit' | translate}}</td>
<td class="kc-action-cell" data-ng-click="removeAttribute(attribute)">{{:: 'delete' | translate}}</td>
</tr>
</tbody>
</table>
</div>
<div data-ng-show="showAttributeGroupListing()">
<table class="datatable table table-striped table-bordered dataTable no-footer">
<thead>
<tr>
<th class="kc-table-actions" colspan="5">
<div class="form-inline">
<div class="pull-right" data-ng-show="access.manageClients">
<button class="btn btn-default" data-ng-click="createAttributeGroup()">
{{:: 'create' | translate}}
</button>
</div>
</div>
</th>
</tr>
<tr>
<th width="25%">{{:: 'name' | translate}}</th>
<th width="25%">{{:: 'user.profile.attributegroup.displayHeader' | translate}}</th>
<th width="40%">{{:: 'user.profile.attributegroup.displayDescription' | translate}}</th>
<th colspan="2">{{:: 'actions' | translate}}</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="group in config.groups">
<td class="kc-sorter">
<button data-ng-hide="flow.builtIn" data-ng-disabled="$first" class="btn btn-default btn-sm" data-ng-click="groupOrderUp($index)"><i class="fa fa-angle-up"></i></button>
<button data-ng-hide="flow.builtIn" data-ng-disabled="$last" class="btn btn-default btn-sm" data-ng-click="groupOrderDown($index)"><i class="fa fa-angle-down"></i></button>
<span><a href="" data-ng-click="editAttributeGroup(group)">{{group.name}}</a></span>
</td>
<td>{{group.displayHeader}}</td>
<td>{{group.displayDescription}}</td>
<td class="kc-action-cell" data-ng-click="editAttributeGroup(group)">{{:: 'edit' | translate}}</td>
<!-- show delete button enabled/disabled depending whether it is referenced in any attribute -->
<td class="kc-action-cell" ng-if="!(groupIsReferencedInAnyAttribute(group))" data-ng-click="removeAttributeGroup(group)">{{:: 'delete' | translate}}</td>
<td class="kc-action-cell-disabled" ng-if="(groupIsReferencedInAnyAttribute(group))" align="center" >{{:: 'delete' | translate}}</td>
</tr>
</tbody>
</table>
</div>
<form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm"
data-ng-show="!isShowAttributes">
data-ng-show="isShowJsonEditor">
<filedset>
<div class="form-group">
<div class="col-md-10">
@ -69,7 +113,7 @@
data-ng-show="currentAttribute != null">
<p/>
<legend expanded><span class="text">{{:: 'user.profile.attribute' | translate}} <b
data-ng-show="!isCreate">{{currentAttribute.name}}</b> {{:: 'configuration' | translate}}</span>
data-ng-show="!isCreateAttribute">{{currentAttribute.name}}</b> {{:: 'configuration' | translate}}</span>
</legend>
<div class="form-group">
<label class="col-md-2 control-label" for="currentAttribute.name">{{:: 'user.profile.attribute.name' | translate}}</label>
@ -77,8 +121,8 @@
<div class="col-md-4">
<input name="currentAttribute.name" data-ng-model="currentAttribute.name" id="currentAttribute.name"
type="text" class="form-control"
data-ng-readonly="!isCreate"
required/>
data-ng-readonly="!isCreateAttribute"
data-ng-required="currentAttribute != null"/>
</div>
</div>
<div class="form-group">
@ -89,6 +133,20 @@
type="text" class="form-control"/>
</div>
</div>
<div class="form-group">
<label class="col-md-2 control-label" for="currentAttribute.group">{{:: 'user.profile.attribute.group' | translate}}</label>
<div class="col-md-2">
<div>
<select id="select-attributeGroup" data-ng-model="currentAttribute.group" class="form-control"
data-ng-options="group.name as group.name for group in config.groups" >
<option value="" /> <!-- add the "no group" option -->
</select>
</div>
</div>
<kc-tooltip>{{:: 'user.profile.attribute.group.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group" data-ng-show="isNotUsernameOrEmail(currentAttribute.name)">
<label class="col-md-2 control-label" for="selectorByScopeSelect">{{:: 'user.profile.attribute.selector.scopes' | translate}}</label>
<kc-tooltip>{{:: 'user.profile.attribute.selector.scopes.tooltip' | translate}}</kc-tooltip>
@ -222,7 +280,7 @@
<tr>
<td><input ng-model="newAnnotation.key" class="form-control" type="text" id="newAnnotationKey"/></td>
<td><input ng-model="newAnnotation.value" class="form-control" type="text" id="newAnnotationValue"/></td>
<td class="kc-action-cell" data-ng-click="addAnnotation()"
<td class="kc-action-cell" data-ng-click="addAttributeAnnotation()"
data-ng-disabled="!newAnnotation.key.length || !newAnnotation.value.length">{{:: 'add' | translate}}
</td>
</tr>
@ -238,7 +296,79 @@
</div>
</div>
</form>
<form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm"
data-ng-show="currentAttributeGroup != null">
<p/>
<legend expanded><span class="text">{{:: 'user.profile.attributegroup' | translate}} <b
data-ng-show="!isCreateAttributeGroup">{{currentAttributeGroup.name}}</b> {{:: 'configuration' | translate}}</span>
</legend>
<div class="form-group">
<label class="col-md-2 control-label" for="currentAttributeGroup.name">{{:: 'user.profile.attributegroup.name' | translate}}</label>
<kc-tooltip>{{:: 'user.profile.attributegroup.name.tooltip' | translate}}</kc-tooltip>
<div class="col-md-4">
<input name="currentAttributeGroup.name" data-ng-model="currentAttributeGroup.name" id="currentAttributeGroup.name"
type="text" class="form-control"
data-ng-readonly="!isCreateAttributeGroup"
data-ng-required="currentAttributeGroup != null"/>
</div>
</div>
<div class="form-group">
<label class="col-md-2 control-label" for="currentAttributeGroup.displayHeader">{{:: 'user.profile.attributegroup.displayHeader' | translate}}</label>
<kc-tooltip>{{:: 'user.profile.attributegroup.displayHeader.tooltip' | translate}}</kc-tooltip>
<div class="col-md-4">
<input name="currentAttributeGroup.displayHeader" data-ng-model="currentAttributeGroup.displayHeader" id="currentAttributeGroup.displayHeader"
type="text" class="form-control"/>
</div>
</div>
<div class="form-group">
<label class="col-md-2 control-label" for="currentAttributeGroup.displayDescription">{{:: 'user.profile.attributegroup.displayDescription' | translate}}</label>
<kc-tooltip>{{:: 'user.profile.attributegroup.displayDescription.tooltip' | translate}}</kc-tooltip>
<div class="col-md-4">
<input name="currentAttributeGroup.displayDescription" data-ng-model="currentAttributeGroup.displayDescription" id="currentAttributeGroup.displayDescription"
type="text" class="form-control"/>
</div>
</div>
<fieldset>
<legend collapsed><span class="text">{{:: 'user.profile.attributegroup.annotation' | translate}}</span></legend>
<div class="form-group col-sm-10">
<table class="table table-striped table-bordered">
<thead>
<tr>
<th>{{:: 'key' | translate}}</th>
<th>{{:: 'value' | translate}}</th>
<th>{{:: 'actions' | translate}}</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="(key, value) in currentAttributeGroup.annotations | toOrderedMapSortedByKey">
<td>{{key}}</td>
<td><input ng-model="currentAttributeGroup.annotations[key]" class="form-control" type="text" name="{{key}}"
id="attributegroup-{{key}}"/></td>
<td class="kc-action-cell" data-ng-click="removeAttributeGroupAnnotation(key)">{{:: 'delete' | translate}}</td>
</tr>
<tr>
<td><input ng-model="newAttributeGroupAnnotation.key" class="form-control" type="text" id="newAttributeGroupAnnotationKey"/></td>
<td><input ng-model="newAttributeGroupAnnotation.value" class="form-control" type="text" id="newAttributeGroupAnnotationValue"/></td>
<td class="kc-action-cell" data-ng-click="addAttributeGroupAnnotation()"
data-ng-disabled="!newAttributeGroupAnnotation.key.length || !newAttributeGroupAnnotation.value.length">{{:: 'add' | translate}}
</td>
</tr>
</tbody>
</table>
</div>
</fieldset>
<div class="form-group">
<p/>
<div class="col-md-10 col-md-offset-2" data-ng-show="access.manageRealm">
<button kc-save>{{:: 'save' | translate}}</button>
<button class="btn btn-default" data-ng-click="cancelEditAttributeGroup()">{{:: 'cancel' | translate}}</button>
</div>
</div>
</form>
</div>
<kc-menu></kc-menu>