Details
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 ba0412f..f2077e2 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
@@ -744,6 +744,12 @@ configure=Configure
select-realm=Select realm
add=Add
+client-storage=Client Storage
+no-client-storage-providers-configured=No client storage providers configured
+client-stores.tooltip=Keycloak can retrieve clients and their details from external stores.
+
+
+
client-template.name.tooltip=Name of the client template. Must be unique in the realm
client-template.description.tooltip=Description of the client template
client-template.protocol.tooltip=Which SSO protocol configuration is being supplied by this client template
@@ -1335,7 +1341,7 @@ userStorage.cachePolicy.option.EVICT_WEEKLY=EVICT_WEEKLY
userStorage.cachePolicy.option.EVICT_DAILY=EVICT_DAILY
userStorage.cachePolicy.option.MAX_LIFESPAN=MAX_LIFESPAN
userStorage.cachePolicy.option.NO_CACHE=NO_CACHE
-userStorage.cachePolicy.tooltip=Cache Policy for this storage provider. 'DEFAULT' is whatever the default settings are for the global user cache. 'EVICT_DAILY' is a time of day every day that the user cache will be invalidated. 'EVICT_WEEKLY' is a day of the week and time the cache will be invalidated. 'MAX-LIFESPAN' is the time in milliseconds that will be the lifespan of a cache entry.
+userStorage.cachePolicy.tooltip=Cache Policy for this storage provider. 'DEFAULT' is whatever the default settings are for the global cache. 'EVICT_DAILY' is a time of day every day that the cache will be invalidated. 'EVICT_WEEKLY' is a day of the week and time the cache will be invalidated. 'MAX-LIFESPAN' is the time in milliseconds that will be the lifespan of a cache entry.
userStorage.cachePolicy.evictionDay=Eviction Day
userStorage.cachePolicy.evictionDay.tooltip=Day of the week the entry will become invalid on
userStorage.cachePolicy.evictionHour=Eviction Hour
@@ -1343,13 +1349,31 @@ userStorage.cachePolicy.evictionHour.tooltip=Hour of day the entry will become i
userStorage.cachePolicy.evictionMinute=Eviction Minute
userStorage.cachePolicy.evictionMinute.tooltip=Minute of day the entry will become invalid on.
userStorage.cachePolicy.maxLifespan=Max Lifespan
-userStorage.cachePolicy.maxLifespan.tooltip=Max lifespan of a user cache entry in milliseconds.
+userStorage.cachePolicy.maxLifespan.tooltip=Max lifespan of cache entry in milliseconds.
user-origin-link=Storage Origin
user-origin.tooltip=UserStorageProvider the user was loaded from
user-link.tooltip=UserStorageProvider this locally stored user was imported from.
client-origin-link=Storage Origin
client-origin.tooltip=Provider the client was loaded from
+client-storage-cache-policy=Cache Settings
+clientStorage.cachePolicy=Cache Policy
+clientStorage.cachePolicy.option.DEFAULT=DEFAULT
+clientStorage.cachePolicy.option.EVICT_WEEKLY=EVICT_WEEKLY
+clientStorage.cachePolicy.option.EVICT_DAILY=EVICT_DAILY
+clientStorage.cachePolicy.option.MAX_LIFESPAN=MAX_LIFESPAN
+clientStorage.cachePolicy.option.NO_CACHE=NO_CACHE
+clientStorage.cachePolicy.tooltip=Cache Policy for this storage provider. 'DEFAULT' is whatever the default settings are for the global cache. 'EVICT_DAILY' is a time of day every day that the cache will be invalidated. 'EVICT_WEEKLY' is a day of the week and time the cache will be invalidated. 'MAX-LIFESPAN' is the time in milliseconds that will be the lifespan of a cache entry.
+clientStorage.cachePolicy.evictionDay=Eviction Day
+clientStorage.cachePolicy.evictionDay.tooltip=Day of the week the entry will become invalid on
+clientStorage.cachePolicy.evictionHour=Eviction Hour
+clientStorage.cachePolicy.evictionHour.tooltip=Hour of day the entry will become invalid on.
+clientStorage.cachePolicy.evictionMinute=Eviction Minute
+clientStorage.cachePolicy.evictionMinute.tooltip=Minute of day the entry will become invalid on.
+clientStorage.cachePolicy.maxLifespan=Max Lifespan
+clientStorage.cachePolicy.maxLifespan.tooltip=Max lifespan of cache entry in milliseconds.
+
+
disable=Disable
disableable-credential-types=Disableable Types
credentials.disableable.tooltip=List of credential types that you can disable
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 bc71ae8..3c1f086 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
@@ -1451,7 +1451,57 @@ module.config([ '$routeProvider', function($routeProvider) {
},
controller : 'ClientImportCtrl'
})
- .when('/', {
+ .when('/realms/:realm/client-stores', {
+ templateUrl : resourceUrl + '/partials/client-storage-list.html',
+ resolve : {
+ realm : function(RealmLoader) {
+ return RealmLoader();
+ },
+ serverInfo : function(ServerInfoLoader) {
+ return ServerInfoLoader();
+ }
+ },
+ controller : 'ClientStoresCtrl'
+ })
+ .when('/realms/:realm/client-storage/providers/:provider/:componentId', {
+ templateUrl : resourceUrl + '/partials/client-storage-generic.html',
+ resolve : {
+ realm : function(RealmLoader) {
+ return RealmLoader();
+ },
+ instance : function(ComponentLoader) {
+ return ComponentLoader();
+ },
+ providerId : function($route) {
+ return $route.current.params.provider;
+ },
+ serverInfo : function(ServerInfoLoader) {
+ return ServerInfoLoader();
+ }
+ },
+ controller : 'GenericClientStorageCtrl'
+ })
+ .when('/create/client-storage/:realm/providers/:provider', {
+ templateUrl : resourceUrl + '/partials/client-storage-generic.html',
+ resolve : {
+ realm : function(RealmLoader) {
+ return RealmLoader();
+ },
+ instance : function() {
+ return {
+
+ };
+ },
+ providerId : function($route) {
+ return $route.current.params.provider;
+ },
+ serverInfo : function(ServerInfoLoader) {
+ return ServerInfoLoader();
+ }
+ },
+ controller : 'GenericClientStorageCtrl'
+ })
+ .when('/', {
templateUrl : resourceUrl + '/partials/home.html',
controller : 'HomeCtrl'
})
@@ -2409,6 +2459,15 @@ module.directive('kcTabsUsers', function () {
}
});
+module.directive('kcTabsClients', function () {
+ return {
+ scope: true,
+ restrict: 'E',
+ replace: true,
+ templateUrl: resourceUrl + '/templates/kc-tabs-clients.html'
+ }
+});
+
module.directive('kcTabsGroup', function () {
return {
scope: true,
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
index 950345c..04f2a48 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
@@ -2388,3 +2388,210 @@ module.controller('ClientTemplateScopeMappingCtrl', function($scope, $http, real
updateTemplateRealmRoles();
});
+
+module.controller('ClientStoresCtrl', function($scope, $location, $route, realm, serverInfo, Components, Notifications, Dialog) {
+ console.log('ClientStoresCtrl ++++****');
+ $scope.realm = realm;
+ $scope.providers = serverInfo.componentTypes['org.keycloak.storage.client.ClientStorageProvider'];
+ $scope.instancesLoaded = false;
+
+ if (!$scope.providers) $scope.providers = [];
+
+ $scope.addProvider = function(provider) {
+ console.log('Add provider: ' + provider.id);
+ $location.url("/create/client-storage/" + realm.realm + "/providers/" + provider.id);
+ };
+
+ $scope.getInstanceLink = function(instance) {
+ return "/realms/" + realm.realm + "/client-storage/providers/" + instance.providerId + "/" + instance.id;
+ }
+
+ $scope.getInstanceName = function(instance) {
+ return instance.name;
+ }
+ $scope.getInstanceProvider = function(instance) {
+ return instance.providerId;
+ }
+
+ $scope.isProviderEnabled = function(instance) {
+ return !instance.config['enabled'] || instance.config['enabled'][0] == 'true';
+ }
+
+ $scope.getInstancePriority = function(instance) {
+ if (!instance.config['priority']) {
+ return "0";
+ }
+ return instance.config['priority'][0];
+ }
+
+ Components.query({realm: realm.realm,
+ parent: realm.id,
+ type: 'org.keycloak.storage.client.ClientStorageProvider'
+ }, function(data) {
+ $scope.instances = data;
+ $scope.instancesLoaded = true;
+ });
+
+ $scope.removeInstance = function(instance) {
+ Dialog.confirmDelete(instance.name, 'client storage provider', function() {
+ Components.remove({
+ realm : realm.realm,
+ componentId : instance.id
+ }, function() {
+ $route.reload();
+ Notifications.success("The provider has been deleted.");
+ });
+ });
+ };
+});
+
+module.controller('GenericClientStorageCtrl', function($scope, $location, Notifications, $route, Dialog, realm,
+ serverInfo, instance, providerId, Components) {
+ console.log('GenericClientStorageCtrl');
+ console.log('providerId: ' + providerId);
+ $scope.create = !instance.providerId;
+ console.log('create: ' + $scope.create);
+ var providers = serverInfo.componentTypes['org.keycloak.storage.client.ClientStorageProvider'];
+ console.log('providers length ' + providers.length);
+ var providerFactory = null;
+ for (var i = 0; i < providers.length; i++) {
+ var p = providers[i];
+ console.log('provider: ' + p.id);
+ if (p.id == providerId) {
+ $scope.providerFactory = p;
+ providerFactory = p;
+ break;
+ }
+
+ }
+ $scope.changed = false;
+
+ console.log("providerFactory: " + providerFactory.id);
+
+ function initClientStorageSettings() {
+ if ($scope.create) {
+ $scope.changed = true;
+ instance.name = providerFactory.id;
+ instance.providerId = providerFactory.id;
+ instance.providerType = 'org.keycloak.storage.client.ClientStorageProvider';
+ instance.parentId = realm.id;
+ instance.config = {
+
+ };
+ instance.config['priority'] = ["0"];
+ instance.config['enabled'] = ["true"];
+
+ $scope.fullSyncEnabled = false;
+ $scope.changedSyncEnabled = false;
+ instance.config['cachePolicy'] = ['DEFAULT'];
+ instance.config['evictionDay'] = [''];
+ instance.config['evictionHour'] = [''];
+ instance.config['evictionMinute'] = [''];
+ instance.config['maxLifespan'] = [''];
+ if (providerFactory.properties) {
+
+ for (var i = 0; i < providerFactory.properties.length; i++) {
+ var configProperty = providerFactory.properties[i];
+ if (configProperty.defaultValue) {
+ instance.config[configProperty.name] = [configProperty.defaultValue];
+ } else {
+ instance.config[configProperty.name] = [''];
+ }
+
+ }
+ }
+
+ } else {
+ $scope.changed = false;
+ if (!instance.config['enabled']) {
+ instance.config['enabled'] = ['true'];
+ }
+ if (!instance.config['cachePolicy']) {
+ instance.config['cachePolicy'] = ['DEFAULT'];
+
+ }
+ if (!instance.config['evictionDay']) {
+ instance.config['evictionDay'] = [''];
+
+ }
+ if (!instance.config['evictionHour']) {
+ instance.config['evictionHour'] = [''];
+
+ }
+ if (!instance.config['evictionMinute']) {
+ instance.config['evictionMinute'] = [''];
+
+ }
+ if (!instance.config['maxLifespan']) {
+ instance.config['maxLifespan'] = [''];
+
+ }
+ if (!instance.config['priority']) {
+ instance.config['priority'] = ['0'];
+ }
+
+ if (providerFactory.properties) {
+ for (var i = 0; i < providerFactory.properties.length; i++) {
+ var configProperty = providerFactory.properties[i];
+ if (!instance.config[configProperty.name]) {
+ instance.config[configProperty.name] = [''];
+ }
+ }
+ }
+
+ }
+ }
+
+ initClientStorageSettings();
+ $scope.instance = angular.copy(instance);
+ $scope.realm = realm;
+
+ $scope.$watch('instance', function() {
+ if (!angular.equals($scope.instance, instance)) {
+ $scope.changed = true;
+ }
+
+ }, true);
+
+ $scope.save = function() {
+ console.log('save provider');
+ $scope.changed = false;
+ if ($scope.create) {
+ console.log('saving new provider');
+ Components.save({realm: realm.realm}, $scope.instance, function (data, headers) {
+ var l = headers().location;
+ var id = l.substring(l.lastIndexOf("/") + 1);
+
+ $location.url("/realms/" + realm.realm + "/client-storage/providers/" + $scope.instance.providerId + "/" + id);
+ Notifications.success("The provider has been created.");
+ });
+ } else {
+ console.log('update existing provider');
+ Components.update({realm: realm.realm,
+ componentId: instance.id
+ },
+ $scope.instance, function () {
+ $route.reload();
+ Notifications.success("The provider has been updated.");
+ });
+ }
+ };
+
+ $scope.reset = function() {
+ $route.reload();
+ };
+
+ $scope.cancel = function() {
+ console.log('cancel');
+ if ($scope.create) {
+ $location.url("/realms/" + realm.realm + "/client-stores");
+ } else {
+ $route.reload();
+ }
+ };
+
+
+
+});
+
+
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/client-list.html b/themes/src/main/resources/theme/base/admin/resources/partials/client-list.html
index 744e3c4..1773fec 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/client-list.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/client-list.html
@@ -1,8 +1,5 @@
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
- <h1>
- <span>{{:: 'clients' | translate}}</span>
- <kc-tooltip>{{:: 'clients.tooltip' | translate}}</kc-tooltip>
- </h1>
+ <kc-tabs-clients></kc-tabs-clients>
<table class="datatable table table-striped table-bordered dataTable no-footer">
<thead>
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/client-storage-generic.html b/themes/src/main/resources/theme/base/admin/resources/partials/client-storage-generic.html
new file mode 100755
index 0000000..90ed088
--- /dev/null
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/client-storage-generic.html
@@ -0,0 +1,207 @@
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+ <ol class="breadcrumb">
+ <li><a href="#/realms/{{realm.realm}}/client-stores">{{:: 'client-storage' | translate}}</a></li>
+ <li data-ng-hide="create">{{instance.name|capitalize}}</li>
+ <li data-ng-show="create">{{:: 'add-client-storage-provider' | translate}}</li>
+ </ol>
+
+ <form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
+ <fieldset>
+ <legend><span class="text">{{:: 'required-settings' | translate}}</span></legend>
+ <div class="form-group clearfix" data-ng-show="!create">
+ <label class="col-md-2 control-label" for="providerId">{{:: 'provider-id' | translate}} </label>
+ <div class="col-md-6">
+ <input class="form-control" id="providerId" type="text" ng-model="instance.id" readonly>
+ </div>
+ </div>
+ <div class="form-group clearfix block">
+ <label class="col-md-2 control-label" for="enabled">{{:: 'enabled' | translate}}</label>
+ <div class="col-md-6">
+ <input ng-model="instance.config['enabled'][0]" name="enabled" id="enabled" onoffswitchvalue on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
+ </div>
+ <kc-tooltip>{{:: 'client-storage.enabled.tooltip' | translate}}</kc-tooltip>
+ </div>
+ <div class="form-group clearfix">
+ <label class="col-md-2 control-label" for="consoleDisplayName">{{:: 'console-display-name' | translate}} </label>
+ <div class="col-md-6">
+ <input class="form-control" id="consoleDisplayName" type="text" ng-model="instance.name" placeholder="{{:: 'defaults-to-id' | translate}}">
+ </div>
+ <kc-tooltip>{{:: 'console-display-name.tooltip' | translate}}</kc-tooltip>
+ </div>
+ <div class="form-group clearfix">
+ <label class="col-md-2 control-label" for="priority">{{:: 'priority' | translate}} </label>
+ <div class="col-md-6">
+ <input class="form-control" id="priority" type="text" ng-model="instance.config['priority'][0]">
+ </div>
+ <kc-tooltip>{{:: 'priority.tooltip' | translate}}</kc-tooltip>
+ </div>
+
+ <kc-component-config realm="realm" config="instance.config" properties="providerFactory.properties"></kc-component-config>
+
+ </fieldset>
+
+ <fieldset>
+ <legend><span class="text">{{:: 'client-storage-cache-policy' | translate}}</span></legend>
+ <div class="form-group">
+ <label for="cachePolicy" class="col-md-2 control-label">{{:: 'clientStorage.cachePolicy' | translate}}</label>
+ <div class="col-md-2">
+ <div>
+ <select id="cachePolicy" ng-model="instance.config['cachePolicy'][0]" class="form-control">
+ <option value="DEFAULT">{{:: 'clientStorage.cachePolicy.option.DEFAULT' | translate}}</option>
+ <option value="EVICT_DAILY">{{:: 'clientStorage.cachePolicy.option.EVICT_DAILY' | translate}}</option>
+ <option value="EVICT_WEEKLY">{{:: 'clientStorage.cachePolicy.option.EVICT_WEEKLY' | translate}}</option>
+ <option value="MAX_LIFESPAN">{{:: 'clientStorage.cachePolicy.option.MAX_LIFESPAN' | translate}}</option>
+ <option value="NO_CACHE">{{:: 'clientStorage.cachePolicy.option.NO_CACHE' | translate}}</option>
+ </select>
+ </div>
+ </div>
+ <kc-tooltip>{{:: 'clientStorage.cachePolicy.tooltip' | translate}}</kc-tooltip>
+ </div>
+ <div class="form-group" data-ng-show="instance.config['cachePolicy'][0] == 'EVICT_WEEKLY'">
+ <label for="evictionDay" class="col-md-2 control-label">{{:: 'clientStorage.cachePolicy.evictionDay' | translate}}</label>
+ <div class="col-md-2">
+ <div>
+ <select id="evictionDay" ng-model="instance.config['evictionDay'][0]" class="form-control">
+ <option value="1">{{:: 'Sunday' | translate}}</option>
+ <option value="2">{{:: 'Monday' | translate}}</option>
+ <option value="3">{{:: 'Tuesday' | translate}}</option>
+ <option value="4">{{:: 'Wednesday' | translate}}</option>
+ <option value="5">{{:: 'Thursday' | translate}}</option>
+ <option value="6">{{:: 'Friday' | translate}}</option>
+ <option value="7">{{:: 'Saturday' | translate}}</option>
+ </select>
+ </div>
+ </div>
+ <kc-tooltip>{{:: 'clientStorage.cachePolicy.evictionDay.tooltip' | translate}}</kc-tooltip>
+ </div>
+ <div class="form-group clearfix" data-ng-show="instance.config['cachePolicy'][0] == 'EVICT_WEEKLY' || instance.config['cachePolicy'][0] == 'EVICT_DAILY'">
+ <label class="col-md-2 control-label" for="evictionHour">{{:: 'clientStorage.cachePolicy.evictionHour' | translate}}</label>
+ <div class="col-md-2">
+ <div>
+ <select id="evictionHour" ng-model="instance.config['evictionHour'][0]" class="form-control">
+ <option value="0">00</option>
+ <option value="1">01</option>
+ <option value="2">02</option>
+ <option value="3">03</option>
+ <option value="4">04</option>
+ <option value="5">05</option>
+ <option value="6">06</option>
+ <option value="7">07</option>
+ <option value="8">08</option>
+ <option value="9">09</option>
+ <option value="10">10</option>
+ <option value="11">11</option>
+ <option value="12">12</option>
+ <option value="13">13</option>
+ <option value="14">14</option>
+ <option value="15">15</option>
+ <option value="16">16</option>
+ <option value="17">17</option>
+ <option value="18">18</option>
+ <option value="19">19</option>
+ <option value="20">20</option>
+ <option value="21">21</option>
+ <option value="22">22</option>
+ <option value="23">23</option>
+ </select>
+ </div>
+ </div>
+ <kc-tooltip>{{:: 'clientStorage.cachePolicy.evictionHour.tooltip' | translate}}</kc-tooltip>
+ </div>
+ <div class="form-group clearfix" data-ng-show="instance.config['cachePolicy'][0] == 'EVICT_WEEKLY' || instance.config['cachePolicy'][0] == 'EVICT_DAILY'">
+ <label class="col-md-2 control-label" for="evictionMinute">{{:: 'clientStorage.cachePolicy.evictionMinute' | translate}}</label>
+ <div class="col-md-2">
+ <div>
+ <select id="evictionMinute" ng-model="instance.config['evictionMinute'][0]" class="form-control">
+ <option value="0">00</option>
+ <option value="1">01</option>
+ <option value="2">02</option>
+ <option value="3">03</option>
+ <option value="4">04</option>
+ <option value="5">05</option>
+ <option value="6">06</option>
+ <option value="7">07</option>
+ <option value="8">08</option>
+ <option value="9">09</option>
+ <option value="10">10</option>
+ <option value="11">11</option>
+ <option value="12">12</option>
+ <option value="13">13</option>
+ <option value="14">14</option>
+ <option value="15">15</option>
+ <option value="16">16</option>
+ <option value="17">17</option>
+ <option value="18">18</option>
+ <option value="19">19</option>
+ <option value="20">20</option>
+ <option value="21">21</option>
+ <option value="22">22</option>
+ <option value="23">23</option>
+ <option value="24">24</option>
+ <option value="25">25</option>
+ <option value="26">26</option>
+ <option value="27">27</option>
+ <option value="28">28</option>
+ <option value="29">29</option>
+ <option value="30">30</option>
+ <option value="31">31</option>
+ <option value="32">32</option>
+ <option value="33">33</option>
+ <option value="34">34</option>
+ <option value="35">35</option>
+ <option value="36">36</option>
+ <option value="37">37</option>
+ <option value="38">38</option>
+ <option value="39">39</option>
+ <option value="40">40</option>
+ <option value="41">41</option>
+ <option value="42">42</option>
+ <option value="43">43</option>
+ <option value="44">44</option>
+ <option value="45">45</option>
+ <option value="46">46</option>
+ <option value="47">47</option>
+ <option value="48">48</option>
+ <option value="49">49</option>
+ <option value="50">50</option>
+ <option value="51">51</option>
+ <option value="52">52</option>
+ <option value="53">53</option>
+ <option value="54">54</option>
+ <option value="55">55</option>
+ <option value="56">56</option>
+ <option value="57">57</option>
+ <option value="58">58</option>
+ <option value="59">59</option>
+ </select>
+ </div>
+ </div>
+ <kc-tooltip>{{:: 'clientStorage.cachePolicy.evictionMinute.tooltip' | translate}}</kc-tooltip>
+ </div>
+ <div class="form-group clearfix" data-ng-show="instance.config['cachePolicy'][0] == 'MAX_LIFESPAN'">
+ <label class="col-md-2 control-label" for="maxLifespan">{{:: 'clientStorage.cachePolicy.maxLifespan' | translate}}</label>
+ <div class="col-md-6">
+ <input class="form-control" type="text" ng-model="instance.config['maxLifespan'][0]" id="maxLifespan" />
+ </div>
+ <kc-tooltip>{{:: 'clientStorage.cachePolicy.maxLifespan.tooltip' | translate}}</kc-tooltip>
+ </div>
+ </fieldset>
+
+
+ <div class="form-group">
+ <div class="col-md-10 col-md-offset-2" data-ng-show="create && access.manageRealm">
+ <button kc-save>{{:: 'save' | translate}}</button>
+ <button kc-cancel data-ng-click="cancel()">{{:: 'cancel' | translate}}</button>
+ </div>
+ </div>
+
+ <div class="form-group">
+ <div class="col-md-10 col-md-offset-2" data-ng-show="!create && access.manageRealm">
+ <button kc-save data-ng-disabled="!changed">{{:: 'save' | translate}}</button>
+ <button kc-reset data-ng-disabled="!changed">{{:: 'cancel' | translate}}</button>
+ </div>
+ </div>
+ </form>
+</div>
+
+<kc-menu></kc-menu>
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/client-storage-list.html b/themes/src/main/resources/theme/base/admin/resources/partials/client-storage-list.html
new file mode 100755
index 0000000..6db5467
--- /dev/null
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/client-storage-list.html
@@ -0,0 +1,68 @@
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+
+ <kc-tabs-clients></kc-tabs-clients>
+
+ <div class="blank-slate-pf" data-ng-hide="!instancesLoaded || (instances && instances.length > 0)">
+ <div class="blank-slate-pf-icon">
+ <span class="fa fa-database"></span>
+ </div>
+ <h1>
+ {{:: 'client-storage' | translate}}
+ </h1>
+ <p>Keycloak can federate external client databases. Out of the box we have support for Openshift OAuth clients and service accounts.</p>
+ <p>To get started select a provider from the dropdown below:</p>
+ <div class="blank-slate-pf-main-action">
+ <div class="row" data-ng-show="access.manageRealm">
+ <div class="col-sm-4 col-sm-offset-4">
+ <div class="form-group">
+ <select class="form-control" ng-model="selectedProvider"
+ ng-options="p.id for p in providers"
+ data-ng-change="addProvider(selectedProvider); selectedProvider = null">
+ <option value="" disabled selected>{{:: 'add-provider.placeholder' | translate}}</option>
+ </select>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <table class="table table-striped table-bordered" data-ng-show="instances && instances.length > 0">
+ <thead>
+ <tr ng-show="providers.length > 0 && access.manageRealm">
+ <th colspan="5" class="kc-table-actions">
+ <div class="pull-right">
+ <div>
+ <select class="form-control" ng-model="selectedProvider"
+ ng-options="p.id for p in providers"
+ data-ng-change="addProvider(selectedProvider); selectedProvider = null">
+ <option value="" disabled selected>{{:: 'add-provider.placeholder' | translate}}</option>
+ </select>
+ </div>
+ </div>
+ </th>
+ </tr>
+ <tr data-ng-show="instances && instances.length > 0">
+ <th>{{:: 'id' | translate}}</th>
+ <th>{{:: 'enabled' | translate}}</th>
+ <th>{{:: 'provider-name' | translate}}</th>
+ <th>{{:: 'priority' | translate}}</th>
+ <th colspan="2">{{:: 'actions' | translate}}</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr ng-repeat="instance in instances">
+ <td><a href="#{{getInstanceLink(instance)}}">{{getInstanceName(instance)}}</a></td>
+ <td>{{isProviderEnabled(instance)}}</td>
+ <td>{{getInstanceProvider(instance) | capitalize}}</td>
+ <td>{{getInstancePriority(instance)}}</td>
+ <td class="kc-action-cell" kc-open="{{getInstanceLink(instance)}}">{{:: 'edit' | translate}}</td>
+ <td class="kc-action-cell" data-ng-click="removeInstance(instance)">{{:: 'delete' | translate}}</td>
+ </tr>
+ <tr data-ng-show="!instances || instances.length == 0">
+ <td class="text-muted">{{:: 'no-client-storage-providers-configured' | translate}}</td>
+ </tr>
+ </tbody>
+ </table>
+</div>
+
+<kc-menu></kc-menu>
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-clients.html b/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-clients.html
new file mode 100755
index 0000000..cf5a332
--- /dev/null
+++ b/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-clients.html
@@ -0,0 +1,16 @@
+<div >
+ <h1>
+ <span>{{:: 'clients' | translate}}</span>
+ </h1>
+
+ <ul class="nav nav-tabs">
+ <li ng-class="{active: path[2] == 'clients'}">
+ <a href="#/realms/{{realm.realm}}/clients">{{:: 'lookup' | translate}}</a>
+ <kc-tooltip>{{:: 'clients.tooltip' | translate}}</kc-tooltip>
+ </li>
+ <li ng-class="{active: path[2] == 'client-stores'}">
+ <a href="#/realms/{{realm.realm}}/client-stores">{{:: 'client-storage' | translate}}</a>
+ <kc-tooltip>{{:: 'client-stores.tooltip' | translate}}</kc-tooltip>
+ </li>
+ </ul>
+</div>
\ No newline at end of file