keycloak-aplcache

KEYCLOAK-6622

2/26/2018 5:34:51 PM

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