You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ambari.apache.org by yu...@apache.org on 2014/09/10 06:25:28 UTC

git commit: AMBARI-7235. Admin View: it is difficult and error-prone to be typing users and groups in textarea for membership and permission changes (implement type-ahead for user + group input fields). (yusaku)

Repository: ambari
Updated Branches:
  refs/heads/trunk 364417494 -> 9cf27bf86


AMBARI-7235. Admin View: it is difficult and error-prone to be typing users and groups in textarea for membership and permission changes (implement type-ahead for user + group input fields). (yusaku)


Project: http://git-wip-us.apache.org/repos/asf/ambari/repo
Commit: http://git-wip-us.apache.org/repos/asf/ambari/commit/9cf27bf8
Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/9cf27bf8
Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/9cf27bf8

Branch: refs/heads/trunk
Commit: 9cf27bf86046b0061f30e5c29e16b8b7aa7ab921
Parents: 3644174
Author: Yusaku Sako <yu...@hortonworks.com>
Authored: Tue Sep 9 18:32:26 2014 -0700
Committer: Yusaku Sako <yu...@hortonworks.com>
Committed: Tue Sep 9 18:32:43 2014 -0700

----------------------------------------------------------------------
 .../main/resources/ui/admin-web/app/index.html  |   2 +
 .../resources/ui/admin-web/app/scripts/app.js   |   1 +
 .../controllers/ambariViews/ViewsEditCtrl.js    |   8 +
 .../clusters/ClustersManageAccessCtrl.js        |   8 +
 .../controllers/groups/GroupsEditCtrl.js        |   9 +
 .../scripts/controllers/users/UsersShowCtrl.js  |  13 ++
 .../app/scripts/directives/editableList.js      | 199 +++++++++++++++++++
 .../ui/admin-web/app/scripts/services/Group.js  |   6 +
 .../ui/admin-web/app/scripts/services/User.js   |   7 +
 .../resources/ui/admin-web/app/styles/main.css  | 182 +++++++++++++++++
 .../admin-web/app/views/ambariViews/edit.html   |  29 +--
 .../app/views/clusters/manageAccess.html        |  31 +--
 .../app/views/directives/editableList.html      |  44 ++++
 .../ui/admin-web/app/views/groups/edit.html     |  23 +--
 .../ui/admin-web/app/views/users/show.html      |  22 +-
 .../src/main/resources/ui/admin-web/bower.json  |   3 +-
 16 files changed, 490 insertions(+), 97 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/ambari/blob/9cf27bf8/ambari-admin/src/main/resources/ui/admin-web/app/index.html
----------------------------------------------------------------------
diff --git a/ambari-admin/src/main/resources/ui/admin-web/app/index.html b/ambari-admin/src/main/resources/ui/admin-web/app/index.html
index 2bc520f..e7a8371 100644
--- a/ambari-admin/src/main/resources/ui/admin-web/app/index.html
+++ b/ambari-admin/src/main/resources/ui/admin-web/app/index.html
@@ -85,6 +85,7 @@
     <script src="bower_components/jquery/dist/jquery.js"></script>
     <script src="bower_components/bootstrap/dist/js/bootstrap.js"></script>
     <script src="bower_components/angular/angular.js"></script>
+    <script src="bower_components/angular-animate/angular-animate.js"></script>
     <script src="bower_components/angular-route/angular-route.js"></script>
     <script src="bower_components/angular-bootstrap/ui-bootstrap-tpls.js"></script>
     <script src="bower_components/lodash/dist/lodash.compat.js"></script>
@@ -126,6 +127,7 @@
     <script src="scripts/directives/linkToDir.js"></script>
     <script src="scripts/directives/PasswordVerify.js"></script>
     <script src="scripts/directives/disabledTooltip.js"></script>
+    <script src="scripts/directives/editableList.js"></script>
     <script src="scripts/services/User.js"></script>
     <script src="scripts/services/Group.js"></script>
     <script src="scripts/services/View.js"></script>

http://git-wip-us.apache.org/repos/asf/ambari/blob/9cf27bf8/ambari-admin/src/main/resources/ui/admin-web/app/scripts/app.js
----------------------------------------------------------------------
diff --git a/ambari-admin/src/main/resources/ui/admin-web/app/scripts/app.js b/ambari-admin/src/main/resources/ui/admin-web/app/scripts/app.js
index b9fcce2..b580da7 100644
--- a/ambari-admin/src/main/resources/ui/admin-web/app/scripts/app.js
+++ b/ambari-admin/src/main/resources/ui/admin-web/app/scripts/app.js
@@ -19,6 +19,7 @@
 
 angular.module('ambariAdminConsole', [
   'ngRoute',
+  'ngAnimate',
   'ui.bootstrap',
   'restangular',
   'angularAlert',

http://git-wip-us.apache.org/repos/asf/ambari/blob/9cf27bf8/ambari-admin/src/main/resources/ui/admin-web/app/scripts/controllers/ambariViews/ViewsEditCtrl.js
----------------------------------------------------------------------
diff --git a/ambari-admin/src/main/resources/ui/admin-web/app/scripts/controllers/ambariViews/ViewsEditCtrl.js b/ambari-admin/src/main/resources/ui/admin-web/app/scripts/controllers/ambariViews/ViewsEditCtrl.js
index d860904..4355f63 100644
--- a/ambari-admin/src/main/resources/ui/admin-web/app/scripts/controllers/ambariViews/ViewsEditCtrl.js
+++ b/ambari-admin/src/main/resources/ui/admin-web/app/scripts/controllers/ambariViews/ViewsEditCtrl.js
@@ -160,6 +160,14 @@ angular.module('ambariAdminConsole')
     $scope.editPermissionDisabled = true;
   };
 
+  $scope.$watch(function() {
+    return $scope.permissionsEdit;
+  }, function(newValue) {
+    if(newValue){
+      $scope.savePermissions();
+    }
+  }, true);  
+
   $scope.deleteInstance = function(instance) {
     ConfirmationModal.show('Delete View Instance', 'Are you sure you want to delete View Instance '+ instance.ViewInstanceInfo.label +'?').then(function() {
       View.deleteInstance(instance.ViewInstanceInfo.view_name, instance.ViewInstanceInfo.version, instance.ViewInstanceInfo.instance_name)

http://git-wip-us.apache.org/repos/asf/ambari/blob/9cf27bf8/ambari-admin/src/main/resources/ui/admin-web/app/scripts/controllers/clusters/ClustersManageAccessCtrl.js
----------------------------------------------------------------------
diff --git a/ambari-admin/src/main/resources/ui/admin-web/app/scripts/controllers/clusters/ClustersManageAccessCtrl.js b/ambari-admin/src/main/resources/ui/admin-web/app/scripts/controllers/clusters/ClustersManageAccessCtrl.js
index ff61f60..8999594 100644
--- a/ambari-admin/src/main/resources/ui/admin-web/app/scripts/controllers/clusters/ClustersManageAccessCtrl.js
+++ b/ambari-admin/src/main/resources/ui/admin-web/app/scripts/controllers/clusters/ClustersManageAccessCtrl.js
@@ -61,4 +61,12 @@ angular.module('ambariAdminConsole')
     });
     $scope.isEditMode = false;
   };
+
+  $scope.$watch(function() {
+    return $scope.permissionsEdit;
+  }, function(newValue) {
+    if(newValue){
+      $scope.save();
+    }
+  }, true);
 }]);
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ambari/blob/9cf27bf8/ambari-admin/src/main/resources/ui/admin-web/app/scripts/controllers/groups/GroupsEditCtrl.js
----------------------------------------------------------------------
diff --git a/ambari-admin/src/main/resources/ui/admin-web/app/scripts/controllers/groups/GroupsEditCtrl.js b/ambari-admin/src/main/resources/ui/admin-web/app/scripts/controllers/groups/GroupsEditCtrl.js
index 0896de1..a322c1d 100644
--- a/ambari-admin/src/main/resources/ui/admin-web/app/scripts/controllers/groups/GroupsEditCtrl.js
+++ b/ambari-admin/src/main/resources/ui/admin-web/app/scripts/controllers/groups/GroupsEditCtrl.js
@@ -26,6 +26,14 @@ angular.module('ambariAdminConsole')
   $scope.dataLoaded = false;
   
   $scope.isMembersEditing = false;
+
+  $scope.$watch(function() {
+    return $scope.group.editingUsers;
+  }, function(newValue) {
+    if(newValue && !angular.equals(newValue, $scope.groupMembers)){
+      $scope.updateMembers();  
+    }
+  }, true);
   
   $scope.enableMembersEditing = function() {
     $scope.isMembersEditing = true;
@@ -49,6 +57,7 @@ angular.module('ambariAdminConsole')
   function loadMembers(){
     $scope.group.getMembers().then(function(members) {
       $scope.groupMembers = members;
+      $scope.group.editingUsers = angular.copy($scope.groupMembers);
     });
   }    
   

http://git-wip-us.apache.org/repos/asf/ambari/blob/9cf27bf8/ambari-admin/src/main/resources/ui/admin-web/app/scripts/controllers/users/UsersShowCtrl.js
----------------------------------------------------------------------
diff --git a/ambari-admin/src/main/resources/ui/admin-web/app/scripts/controllers/users/UsersShowCtrl.js b/ambari-admin/src/main/resources/ui/admin-web/app/scripts/controllers/users/UsersShowCtrl.js
index 97a7234..f3e4240 100644
--- a/ambari-admin/src/main/resources/ui/admin-web/app/scripts/controllers/users/UsersShowCtrl.js
+++ b/ambari-admin/src/main/resources/ui/admin-web/app/scripts/controllers/users/UsersShowCtrl.js
@@ -24,6 +24,7 @@ angular.module('ambariAdminConsole')
     User.get($routeParams.id).then(function(data) {
       $scope.user = data.Users;
       $scope.isCurrentUser = $scope.user.user_name === Auth.getCurrentUser();
+      $scope.editingGroupsList = angular.copy($scope.user.groups);
     });
   }
 
@@ -38,6 +39,18 @@ angular.module('ambariAdminConsole')
     $scope.editingGroupsList = angular.copy($scope.user.groups);
   };
 
+  $scope.$watch(function() {
+    return $scope.editingGroupsList;
+  }, function(newValue) {
+    if(newValue){
+      if( !angular.equals(newValue, $scope.user.groups) ){
+        console.log('Update!');
+        $scope.updateGroups();
+      }
+        
+    }
+  }, true);
+
   $scope.updateGroups = function() {
     var groups = $scope.editingGroupsList.toString().split(',').filter(function(item) {return item.trim();}).map(function(item) {return item.trim()});
     var diff = getDifference($scope.user.groups, groups);

http://git-wip-us.apache.org/repos/asf/ambari/blob/9cf27bf8/ambari-admin/src/main/resources/ui/admin-web/app/scripts/directives/editableList.js
----------------------------------------------------------------------
diff --git a/ambari-admin/src/main/resources/ui/admin-web/app/scripts/directives/editableList.js b/ambari-admin/src/main/resources/ui/admin-web/app/scripts/directives/editableList.js
new file mode 100644
index 0000000..5d72078
--- /dev/null
+++ b/ambari-admin/src/main/resources/ui/admin-web/app/scripts/directives/editableList.js
@@ -0,0 +1,199 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+'use strict';
+
+angular.module('ambariAdminConsole')
+.directive('editableList', ['$q', '$document', function($q, $document) {
+  return {
+    restrict: 'E',
+    templateUrl: 'views/directives/editableList.html',
+    scope: {
+      itemsSource: '=',
+      resourceType: '@',
+      editable: '='
+    },
+    link: function($scope, $elem, $attr, $ctrl) {
+      var $editBox = $elem.find('[contenteditable]');
+
+      var readInput = function() {
+        $scope.$apply(function() {
+          $scope.input = $editBox.html();
+        });
+      };
+
+      $scope.$watch(function() {
+        return $scope.input;
+      }, function(newValue) {
+        if(newValue === ''){
+          $scope.clearInput();
+        }
+      });
+
+      $scope.clearInput = function() {
+        $editBox.html('').blur();
+      };
+
+      $scope.focusOnInput = function() {
+        setTimeout(function() {
+          var elem = $editBox[0];
+          var selection = window.getSelection(),
+              range = document.createRange();
+
+          elem.innerHTML = '\u00a0';
+          range.selectNodeContents(elem);
+          selection.removeAllRanges();
+          selection.addRange(range);
+          document.execCommand('delete', false, null);
+        }, 0);
+      };
+
+      $editBox.on('input', readInput);
+      $editBox.on('keydown', function(e) {
+        switch(e.which){
+          case 27: // ESC
+            $editBox.html('').blur();
+            readInput();
+            break;
+          case 13: // Enter
+            $scope.$apply(function() {
+              $scope.addItem();
+            });
+            return false;
+            break;
+          case 40: // Down arrow
+            $scope.downArrowHandler();
+            break;
+          case 38: // Up arrow
+            $scope.upArrowHandler();
+            break;
+        }
+      });
+    },
+    controller: ['$scope', '$injector', function($scope, $injector) {
+      var $resource = $injector.get($scope.resourceType);
+
+      $scope.identity = angular.identity; // Sorting function
+
+      $scope.items = angular.copy($scope.itemsSource);
+      $scope.editMode = false;
+      $scope.input = '';
+      $scope.typeahead = [];
+      $scope.selectedTypeahed = 0;
+
+      // Watch source of items
+      $scope.$watch(function() {
+        return $scope.itemsSource;
+      }, function(newValue) {
+        $scope.items = angular.copy($scope.itemsSource);
+      }, true);
+
+      // When input has changed - load typeahead items
+      $scope.$watch(function() {
+        return $scope.input;
+      }, function(newValue) {
+        if(newValue){
+          var newValue = newValue.split(',').filter(function(i){ 
+            i = i.replace('&nbsp;', ''); // Sanitize from spaces
+            return !!i.trim();
+          });
+          if( newValue.length > 1){
+            // If someone paste coma separated string, then just add all items to list
+            angular.forEach(newValue, function(item) {
+              $scope.addItem(item);
+            });
+            $scope.clearInput();
+            
+          } else {
+            // Load typeahed items based on current input
+            $resource.listByName(newValue).then(function(data) {
+              var items = [];
+              angular.forEach(data.data.items, function(item) {
+                var name;
+                if($scope.resourceType === 'User'){
+                  name = item.Users.user_name;
+                } else if($scope.resourceType === 'Group'){
+                  name = item.Groups.group_name;
+                }
+
+                if($scope.items.indexOf(name) < 0){ // Only if item not in list
+                  items.push(name);
+                }
+                $scope.typeahead = items.slice(0, 5);
+                $scope.selectedTypeahed = 0;
+              });
+            });
+          }
+
+            
+        } else {
+          $scope.typeahead = [];
+          $scope.selectedTypeahed = 0;
+        }
+      });
+
+      $scope.enableEditMode = function() {
+        if( $scope.editable && !$scope.editMode){
+          $scope.editMode = true;
+          if( $scope.items.length === 0){
+            $scope.focusOnInput();
+          }
+        }
+      };
+
+      $scope.cancel = function(event) {
+        $scope.editMode = false;
+        $scope.items = angular.copy($scope.itemsSource);
+        $scope.input = '';
+        event.stopPropagation();
+      };
+      $scope.save = function(event) {
+        $scope.itemsSource = $scope.items;
+        $scope.editMode = false;
+        $scope.input = '';
+        event.stopPropagation();
+      };
+
+
+      $scope.downArrowHandler = function() {
+        $scope.$apply(function() {
+          $scope.selectedTypeahed = ($scope.selectedTypeahed+1) % $scope.typeahead.length;
+        });
+      };
+      $scope.upArrowHandler = function() {
+        $scope.$apply(function() {
+          $scope.selectedTypeahed -= 1;
+          $scope.selectedTypeahed = $scope.selectedTypeahed < 0 ? $scope.typeahead.length-1 : $scope.selectedTypeahed;
+        });
+      };
+
+      $scope.addItem = function(item) {
+        item = item ? item : $scope.typeahead.length ? $scope.typeahead[$scope.selectedTypeahed] : $scope.input;
+        
+        if(item && $scope.items.indexOf(item) < 0){
+          $scope.items.push(item);
+          $scope.input = '';
+        }
+      };
+
+      $scope.removeFromItems = function(item) {
+        $scope.items.splice( $scope.items.indexOf(item), 1);
+      };
+    }]
+  };
+}]);
+

http://git-wip-us.apache.org/repos/asf/ambari/blob/9cf27bf8/ambari-admin/src/main/resources/ui/admin-web/app/scripts/services/Group.js
----------------------------------------------------------------------
diff --git a/ambari-admin/src/main/resources/ui/admin-web/app/scripts/services/Group.js b/ambari-admin/src/main/resources/ui/admin-web/app/scripts/services/Group.js
index efe39a3..87f7831 100644
--- a/ambari-admin/src/main/resources/ui/admin-web/app/scripts/services/Group.js
+++ b/ambari-admin/src/main/resources/ui/admin-web/app/scripts/services/Group.js
@@ -174,6 +174,12 @@ angular.module('ambariAdminConsole')
     return deferred.promise;
   };
 
+  Group.listByName = function(name) {
+    return $http.get(Settings.baseUrl + '/groups?'
+      + 'Groups/group_name.matches(.*'+name+'.*)'
+    );
+  };
+
   Group.getPrivilegies = function(groupId) {
     return $http.get(Settings.baseUrl + '/privileges', {
         params:{

http://git-wip-us.apache.org/repos/asf/ambari/blob/9cf27bf8/ambari-admin/src/main/resources/ui/admin-web/app/scripts/services/User.js
----------------------------------------------------------------------
diff --git a/ambari-admin/src/main/resources/ui/admin-web/app/scripts/services/User.js b/ambari-admin/src/main/resources/ui/admin-web/app/scripts/services/User.js
index 9c2e36a..40c5683 100644
--- a/ambari-admin/src/main/resources/ui/admin-web/app/scripts/services/User.js
+++ b/ambari-admin/src/main/resources/ui/admin-web/app/scripts/services/User.js
@@ -46,6 +46,13 @@ angular.module('ambariAdminConsole')
         + (params.admin ? '&Users/admin=true' : '')
       );
     },
+    listByName: function(name) {
+      return $http.get(
+        Settings.baseUrl + '/users?'
+        + 'Users/user_name.matches(.*'+name+'.*)'
+        + '&from=0&page_size=20'
+      );
+    },
     get: function(userId) {
       return Restangular.one('users', userId).get();
     },

http://git-wip-us.apache.org/repos/asf/ambari/blob/9cf27bf8/ambari-admin/src/main/resources/ui/admin-web/app/styles/main.css
----------------------------------------------------------------------
diff --git a/ambari-admin/src/main/resources/ui/admin-web/app/styles/main.css b/ambari-admin/src/main/resources/ui/admin-web/app/styles/main.css
index e92fe1a..bfa0032 100644
--- a/ambari-admin/src/main/resources/ui/admin-web/app/styles/main.css
+++ b/ambari-admin/src/main/resources/ui/admin-web/app/styles/main.css
@@ -16,6 +16,182 @@
  * limitations under the License.
  */
 
+
+
+/*
+  ------ START editable-list DIRECTIVE SECTION ------ -
+*/
+.editable-list-container.well{
+  padding: 10px;
+  position: relative;
+  margin-bottom: 30px;
+  cursor: pointer;
+}
+.editable-list-container.well.edit-mode{
+  cursor: default;
+}
+.editable-list-container.well.disabled{
+  background: white;
+}
+
+.editable-list-container .items-box{
+
+}
+.editable-list-container .items-box ul.items-list{
+  list-style-type: none;
+  margin: 0;
+  padding: 0;
+}
+
+.editable-list-container .items-box ul.items-list li.item{
+  display: inline-block;
+  padding: 4px 8px;
+  margin: 0 5px 5px 2px;
+  background: white;
+  border: 1px solid #ebebeb;
+  max-width: 100%;
+  white-space: nowrap;
+  position: relative;
+}
+
+.editable-list-container.edit-mode .items-box ul.items-list li.item{
+  padding-right: 25px;
+}
+
+.editable-list-container .items-box ul.items-list li.item.ng-leave-active{
+  display: none;
+}
+.editable-list-container .items-box ul.items-list li a{
+  text-decoration: none;
+}
+
+.editable-list-container .items-box ul.items-list li.item .close{
+  margin: -2px 0 0 5px;
+  width: 13px;
+  outline: none;
+  position: absolute;
+  display: none;
+}
+.editable-list-container.edit-mode .items-box ul.items-list li.item .close{
+  display: inline-block;
+}
+
+.editable-list-container .actions-panel{
+  position: absolute;
+  right: 5px;
+  bottom: -30px;
+  padding: 2px 5px 5px 5px;
+  background: #f5f5f5;
+  border: 1px solid #e3e3e3;
+  border-top: none;
+  border-radius: 0 0 4px 4px;
+
+  -webkit-transition: all 0.3s;
+  -o-transition: all 0.3s;
+  transition: all 0.3s;
+
+  -ms-transform-origin: 0% 0%; /* IE 9 */
+  -webkit-transform-origin: 0% 0%; /* Chrome, Safari, Opera */
+  transform-origin: 0% 0%;
+
+  -webkit-transform: rotateX(0deg);
+  -ms-transform: rotateX(0deg);
+  -o-transform: rotateX(0deg);
+  transform: rotateX(0deg); 
+}
+.editable-list-container .actions-panel.ng-hide{
+  -webkit-transform: rotateX(90deg);
+  -ms-transform: rotateX(90deg);
+  -o-transform: rotateX(90deg);
+  transform: rotateX(90deg);
+}
+
+.editable-list-container.edit-mode .items-box ul.items-list li.item.add-item-input.ng-hidden{
+  display: none !important;
+}
+.editable-list-container.edit-mode .items-box ul.items-list li.item.add-item-input{
+  display: inline-block!important;
+  outline: none;
+  max-width: 200px;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  position: relative;
+  padding-right: 8px;
+  -webkit-transition: none;
+  -o-transition: none;
+  transition: none;
+}
+
+.add-item-input span{
+  display: block;
+  outline: none;
+  min-width: 30px;
+  position: relative;
+  cursor: pointer;
+}
+.add-item-input span:focus{
+  cursor: default;
+}
+.editable-list-container .items-box ul.items-list li.item.add-item{
+  color: #ddd;
+}
+.add-item-input span:empty:before{
+  content: 'New';
+  position: absolute;
+  left: 0;
+  color: #ddd;
+}
+.add-item-input span:focus:before{
+  display: none;
+}
+.typeahead-box{
+  position: absolute;
+  left: 0;
+  margin-top: 5px;
+  background: white;
+  border: 1px solid #ebebeb;
+  z-index: 1000;
+  min-width: 65px;
+}
+.typeahead-box ul{
+  list-style-type: none;
+  margin: 0;
+  padding: 0;
+}
+.typeahead-box ul li{
+  padding: 3px 5px;
+  display: block;
+  cursor: pointer;
+}
+
+.typeahead-box ul li.selected, .typeahead-box ul li:hover{
+  background: #eee;
+}
+
+.editable-list-container.disabled .pencil-box{
+  display: none;
+}
+.editable-list-container .pencil-box{
+  position: absolute;
+  right: 5px;
+  top: 5px;
+  opacity: 0;
+  -webkit-transition: all 0.3s;
+  -o-transition: all 0.3s;
+  transition: all 0.3s;
+}
+.editable-list-container:hover .pencil-box{
+  opacity: 1;
+}
+.editable-list-container.edit-mode:hover .pencil-box{
+  opacity: 0;
+}
+
+/*
+  ------ END editable-list DIRECTIVE SECTION ------ -
+*/
+
+
  .instances-table{
   table-layout: fixed;
  }
@@ -747,6 +923,12 @@ input[type="submit"].btn.btn-mini {
   *padding-bottom: 1px;
 }
 
+button.btn.btn-xs{
+  padding: 1px 5px;
+  font-size: 12px;
+  line-height: 1.5;
+  border-radius: 3px;
+}
 .alert-info {
   background-color: #E6F1F6;
   border-color: #D2D9DD;

http://git-wip-us.apache.org/repos/asf/ambari/blob/9cf27bf8/ambari-admin/src/main/resources/ui/admin-web/app/views/ambariViews/edit.html
----------------------------------------------------------------------
diff --git a/ambari-admin/src/main/resources/ui/admin-web/app/views/ambariViews/edit.html b/ambari-admin/src/main/resources/ui/admin-web/app/views/ambariViews/edit.html
index dbb8165..02d4f77 100644
--- a/ambari-admin/src/main/resources/ui/admin-web/app/views/ambariViews/edit.html
+++ b/ambari-admin/src/main/resources/ui/admin-web/app/views/ambariViews/edit.html
@@ -93,9 +93,6 @@
 <div class="panel panel-default views-permissions-panel" style="">
   <div class="panel-heading clearfix">
     <h3 class="panel-title pull-left">Permissions</h3>
-    <div class="pull-right" >
-      <a ng-hide="isPermissionsEmpty" href class="permissions-edit-toggle" ng-click="editPermissionDisabled = !editPermissionDisabled" ng-show="editPermissionDisabled"> <span class="glyphicon glyphicon-pencil"></span> Edit</a>
-    </div>
   </div>
   <div class="panel-body">
     
@@ -114,36 +111,14 @@
             <label class="">{{permission.PermissionInfo.permission_name}}</label>
           </td>
           <td>
-            <div class="" ng-switch="editPermissionDisabled">
-              <textarea name="" id="" cols="30" rows="4" class="form-control permission-textarea-user" ng-model="permissionsEdit[permission.PermissionInfo.permission_name].USER" ng-switch-when="false"></textarea>
-              <div class="well" ng-switch-when="true">
-                <span ng-repeat="user in permission.USER | orderBy:identity">
-                  <link-to route="users.show" id="{{user}}">{{user}}</link-to>
-                  {{$last ? '' :', '}}
-                </span>
-              </div>
-            </div>
+            <editable-list items-source="permissionsEdit[permission.PermissionInfo.permission_name].USER" editable="true" resource-type="User"></editable-list>
           </td>
           <td>
-            <div class="" ng-switch="editPermissionDisabled">
-              <textarea name="" id="" cols="30" rows="4" class="form-control permission-textarea-group" ng-model="permissionsEdit[permission.PermissionInfo.permission_name].GROUP" ng-switch-when="false"></textarea>
-              <div class="well" ng-switch-when="true">
-                <span ng-repeat="group in permission.GROUP | orderBy:identity">
-                  <link-to route="groups.edit" id="{{group}}" >{{group}}</link-to>
-                  {{$last ? '' :', '}}
-                </span>
-              </div>
-            </div>
+            <editable-list items-source="permissionsEdit[permission.PermissionInfo.permission_name].GROUP" editable="true" resource-type="Group" ></editable-list>
           </td>
         </tr>
       </tbody>
     </table>
-    <div class="form-group" ng-hide="editPermissionDisabled">
-      <div class="col-sm-offset-2 col-sm-10">
-        <button class="btn btn-primary pull-right left-margin permissions-save" ng-click="savePermissions()">Save</button>
-        <button class="btn btn-default pull-right permissions-cancel" ng-click="cancelPermissions()">Cancel</button>
-      </div>
-    </div>
     <div ng-show="isPermissionsEmpty">
       <div class="alert alert-info">There are no permissions defined for this view.</div>
     </div>

http://git-wip-us.apache.org/repos/asf/ambari/blob/9cf27bf8/ambari-admin/src/main/resources/ui/admin-web/app/views/clusters/manageAccess.html
----------------------------------------------------------------------
diff --git a/ambari-admin/src/main/resources/ui/admin-web/app/views/clusters/manageAccess.html b/ambari-admin/src/main/resources/ui/admin-web/app/views/clusters/manageAccess.html
index fe27492..6de2561 100644
--- a/ambari-admin/src/main/resources/ui/admin-web/app/views/clusters/manageAccess.html
+++ b/ambari-admin/src/main/resources/ui/admin-web/app/views/clusters/manageAccess.html
@@ -21,17 +21,14 @@
     <ol class="breadcrumb pull-left">
       <li class="active">{{clusterName}} Permissions</li>
     </ol>
-    <div class="pull-right top-margin-4">
-      <a href class="btn btn-primary"  ng-hide="isEditMode" ng-click="toggleEditMode()"><span class="glyphicon glyphicon-pencil"></span> Edit</a>
-    </div>
   </div>
   <hr>
   <table class="table">
     <thead>
       <tr>
-        <th class="col-sm-2"><label>Permission</label></th>
-        <th><label>Grant permission to these users</label></th>
-        <th><label>Grant permission to these groups</label></th>
+        <th class="col-sm-2" width="20%"><label>Permission</label></th>
+        <th class="col-sm-5" width="40%"><label>Grant permission to these users</label></th>
+        <th class="col-sm-5" width="40%"><label>Grant permission to these groups</label></th>
       </tr>
     </thead>
     <tbody>
@@ -39,33 +36,15 @@
         <td><label class="">{{permission.PermissionInfo.permission_name}}</label></td>
         <td>
           <div class="" ng-switch="isEditMode">
-            <textarea ng-switch-when="true" name="" id="" cols="30" rows="4" class="form-control permission-user-input" ng-model="permissionsEdit[permission.PermissionInfo.permission_name].USER"></textarea>
-            <div class="well" ng-switch-default>
-              <span ng-repeat="user in permission.USER | orderBy:identity">
-                <link-to route="users.show" id="{{user}}" >{{user}}</link-to>
-                {{$last ? '' :', '}}
-              </span>
-            </div>
+            <editable-list items-source="permissionsEdit[permission.PermissionInfo.permission_name].USER" resource-type="User" editable="true"></editable-list>
           </div>
         </td>
         <td>
           <div class="" ng-switch="isEditMode">
-            <textarea ng-switch-when="true" name="" id="" cols="30" rows="4" class="form-control permission-group-input" ng-model="permissionsEdit[permission.PermissionInfo.permission_name].GROUP | orderBy:identity"></textarea>
-            <div class="well" ng-switch-default>
-              <span ng-repeat="group in permission.GROUP">
-                <link-to route="groups.edit" id="{{group}}">{{group}}</link-to>
-                {{$last ? '' :', '}}
-              </span>
-            </div>
+            <editable-list items-source="permissionsEdit[permission.PermissionInfo.permission_name].GROUP" resource-type="Group" editable="true"></editable-list>
           </div>
         </td>
       </tr>
     </tbody>
   </table>
-  <div class="form-group" ng-show="isEditMode">
-    <div class="col-sm-offset-2 col-sm-10">
-      <button class="btn btn-primary pull-right permission-save left-margin" ng-click="save()">Save</button>
-      <button class="btn btn-default pull-right permissions-cancel" ng-click="cancel()">Cancel</button>
-    </div>
-  </div>
 </div>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ambari/blob/9cf27bf8/ambari-admin/src/main/resources/ui/admin-web/app/views/directives/editableList.html
----------------------------------------------------------------------
diff --git a/ambari-admin/src/main/resources/ui/admin-web/app/views/directives/editableList.html b/ambari-admin/src/main/resources/ui/admin-web/app/views/directives/editableList.html
new file mode 100644
index 0000000..5cdc148
--- /dev/null
+++ b/ambari-admin/src/main/resources/ui/admin-web/app/views/directives/editableList.html
@@ -0,0 +1,44 @@
+<!--
+* Licensed to the Apache Software Foundation (ASF) under one
+* or more contributor license agreements.  See the NOTICE file
+* distributed with this work for additional information
+* regarding copyright ownership.  The ASF licenses this file
+* to you 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.
+-->
+
+<div class="editable-list-container well" ng-class="{'edit-mode' : editMode, 'disabled' : !editable}" ng-click="enableEditMode()">
+  <div class="items-box">
+    <ul class="items-list">
+      <li class="item" ng-repeat="item in items | orderBy:identity"><span><a href>{{item}}</a><button ng-click="removeFromItems(item)" type="button" class="close"><span aria-hidden="true">&times;</span><span class="sr-only">Close</span></button></span></li><li class="item add-item-input" ng-show="editMode">
+        <span contenteditable></span>
+        <div class="typeahead-box" ng-show="typeahead.length != 0">
+          <ul>
+            <li ng-repeat="item in typeahead" ng-click="addItem(item)" ng-class="{'selected' : $index == selectedTypeahed}">{{item}}</li>
+          </ul>
+        </div>
+      </li>
+      <li class="item add-item" ng-show="!editMode && !items.length">Add {{resourceType}}</li>
+    </ul>
+  </div>
+  <div class="actions-panel" ng-show="editMode">
+    <button class="btn btn-default btn-xs cancel" ng-click="cancel($event)">
+      <span class="glyphicon glyphicon-remove cancel"></span>
+    </button>
+    <button class="btn btn-primary btn-xs" ng-click="save($event)">
+      <span class="glyphicon glyphicon-ok"></span>
+    </button>
+  </div>
+  <div class="pencil-box">
+    <span class="glyphicon glyphicon-pencil"></span>
+  </div>
+</div>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ambari/blob/9cf27bf8/ambari-admin/src/main/resources/ui/admin-web/app/views/groups/edit.html
----------------------------------------------------------------------
diff --git a/ambari-admin/src/main/resources/ui/admin-web/app/views/groups/edit.html b/ambari-admin/src/main/resources/ui/admin-web/app/views/groups/edit.html
index d82caf6..5b90f79 100644
--- a/ambari-admin/src/main/resources/ui/admin-web/app/views/groups/edit.html
+++ b/ambari-admin/src/main/resources/ui/admin-web/app/views/groups/edit.html
@@ -40,28 +40,7 @@
   <div class="form-group">
     <label for="members" class="col-sm-2 control-label">{{group.ldap_group ? 'LDAP Members' : 'Local Members'}}</label>
     <div class="col-sm-10">
-      <div class="row" ng-hide="isMembersEditing">
-        <div class="col-sm-10">
-          <div class="well users">
-            <span ng-repeat="member in groupMembers" >
-              <link-to route='users.show' id="{{member}}">
-                {{member}}
-              </link-to>
-              {{$last ? '' : ', '}}
-            </span>
-          </div>
-        </div>
-        <div class="col-sm-2">
-          <a href ng-click="enableMembersEditing()" ng-hide="user.ldap_group"><span class="glyphicon glyphicon-pencil"></span> Edit</a>
-        </div>
-      </div>
-      <div class="row" ng-show="isMembersEditing">
-        <div class="col-sm-12">
-          <textarea name="groups" id="" cols="30" rows="5" class="form-control bottom-margin usergroups" ng-model="group.editingUsers"></textarea>
-          <a href class="btn btn-primary pull-right left-margin updategroups" ng-click="updateMembers()">Save</a>
-          <button class="btn btn-default pull-right cancel-groups-update" ng-click="cancelUpdate()">Cancel</button>
-        </div>
-      </div>
+      <editable-list items-source="group.editingUsers" resource-type="User" editable="!group.ldap_group"></editable-list>
     </div>
   </div>
 

http://git-wip-us.apache.org/repos/asf/ambari/blob/9cf27bf8/ambari-admin/src/main/resources/ui/admin-web/app/views/users/show.html
----------------------------------------------------------------------
diff --git a/ambari-admin/src/main/resources/ui/admin-web/app/views/users/show.html b/ambari-admin/src/main/resources/ui/admin-web/app/views/users/show.html
index 88e48c9..0667200 100644
--- a/ambari-admin/src/main/resources/ui/admin-web/app/views/users/show.html
+++ b/ambari-admin/src/main/resources/ui/admin-web/app/views/users/show.html
@@ -62,27 +62,7 @@
     <div class="form-group">
       <label for="groups" class="col-sm-2 control-label">{{user.ldap_user ? 'LDAP Group Membership' : 'Local Group Membership'}}</label>
       <div class="col-sm-10">
-        <div class="row" ng-hide="isGroupEditing">
-          <div class="col-sm-10">
-            <div class="well">
-              <span ng-repeat="group in user.groups">
-                <a href="#/groups/{{group}}/edit" >{{group}}</a>
-                {{$last ? '' : ', '}}
-              </span>
-            </div>
-          </div>
-          <div class="col-sm-2">
-            <a href ng-click="enableGroupEditing()" ng-hide="user.ldap_user"><span class="glyphicon glyphicon-pencil"></span> Edit</a>
-          </div>
-        </div>
-        <div class="row" ng-show="isGroupEditing">
-          <div class="col-sm-12">
-            <textarea name="groups" id="" cols="30" rows="5" class="form-control bottom-margin usergroups" ng-model="editingGroupsList"></textarea>
-            <a href class="btn btn-primary pull-right left-margin updategroups" ng-click="updateGroups()">Save</a>
-            <button class="btn btn-default pull-right cancel-groups-update" ng-click="cancelUpdate()">Cancel</button>
-          </div>
-            
-        </div>
+        <editable-list items-source="editingGroupsList" resource-type="Group" editable="!user.ldap_user"></editable-list>
       </div>
         
     </div>

http://git-wip-us.apache.org/repos/asf/ambari/blob/9cf27bf8/ambari-admin/src/main/resources/ui/admin-web/bower.json
----------------------------------------------------------------------
diff --git a/ambari-admin/src/main/resources/ui/admin-web/bower.json b/ambari-admin/src/main/resources/ui/admin-web/bower.json
index 0c30117..27429c2 100644
--- a/ambari-admin/src/main/resources/ui/admin-web/bower.json
+++ b/ambari-admin/src/main/resources/ui/admin-web/bower.json
@@ -7,7 +7,8 @@
     "angular-route": "~1.2.18",
     "angular-bootstrap": "~0.11.0",
     "restangular": "~1.4.0",
-    "angular-bootstrap-toggle-switch": "~0.5.1"
+    "angular-bootstrap-toggle-switch": "~0.5.1",
+    "angular-animate": "~1.2.23"
   },
   "devDependencies": {}
 }