ambari-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From alexantone...@apache.org
Subject [3/3] ambari git commit: AMBARI-10063. CapSched View: Add Support for Node labels and versions (alexantonenko)
Date Sat, 14 Mar 2015 20:57:01 GMT
AMBARI-10063. CapSched View: Add Support for Node labels and versions (alexantonenko)


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

Branch: refs/heads/trunk
Commit: 95103a905df36745b8e9f01768a612193626c221
Parents: a5f99f4
Author: Alex Antonenko <hiveww@gmail.com>
Authored: Sat Mar 14 22:05:21 2015 +0200
Committer: Alex Antonenko <hiveww@gmail.com>
Committed: Sat Mar 14 22:56:52 2015 +0200

----------------------------------------------------------------------
 .../capacityscheduler/ConfigurationService.java |  115 +-
 .../src/main/resources/ui/app/adapters.js       |  439 +++----
 .../src/main/resources/ui/app/assets/index.html |    2 -
 .../resources/ui/app/components/capacityBar.js  |   22 +-
 .../ui/app/components/capacityInput.js          |   45 +-
 .../ui/app/components/confirmDelete.js          |    6 +-
 .../ui/app/components/dropdownConfirmation.js   |   11 +-
 .../resources/ui/app/components/escapeAcl.js    |   11 +-
 .../resources/ui/app/components/pathInput.js    |    6 +-
 .../ui/app/components/queueListItem.js          |   67 +-
 .../resources/ui/app/components/radioButton.js  |    6 +-
 .../ui/app/components/totalCapacity.js          |  411 ++++++-
 .../main/resources/ui/app/controllers/queue.js  |  284 +++--
 .../main/resources/ui/app/controllers/queues.js |  287 ++++-
 .../src/main/resources/ui/app/initialize.js     |    6 +
 .../src/main/resources/ui/app/models/queue.js   |  148 ++-
 .../src/main/resources/ui/app/router.js         |   34 +-
 .../src/main/resources/ui/app/serializers.js    |  285 +++++
 .../src/main/resources/ui/app/store.js          |  164 +++
 .../resources/ui/app/styles/application.less    | 1069 +++++++++++-------
 .../src/main/resources/ui/app/templates.js      |    2 +
 .../ui/app/templates/capacityEditForm.hbs       |   87 +-
 .../ui/app/templates/components/capacityBar.hbs |    4 +-
 .../components/dropdownConfirmation.hbs         |   29 +
 .../ui/app/templates/components/pathInput.hbs   |    4 +-
 .../app/templates/components/queueContainer.hbs |   67 ++
 .../app/templates/components/queueListItem.hbs  |   42 +-
 .../app/templates/components/totalCapacity.hbs  |   67 +-
 .../main/resources/ui/app/templates/queue.hbs   |  136 ++-
 .../main/resources/ui/app/templates/queues.hbs  |   47 +-
 .../ui/app/templates/schedulerPanel.hbs         |   84 +-
 .../src/main/resources/ui/app/views/queues.js   |    3 +-
 .../src/main/resources/ui/bower.json            |   14 +-
 .../src/main/resources/ui/config.coffee         |   14 +-
 34 files changed, 2869 insertions(+), 1149 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/ambari/blob/95103a90/contrib/views/capacity-scheduler/src/main/java/org/apache/ambari/view/capacityscheduler/ConfigurationService.java
----------------------------------------------------------------------
diff --git a/contrib/views/capacity-scheduler/src/main/java/org/apache/ambari/view/capacityscheduler/ConfigurationService.java b/contrib/views/capacity-scheduler/src/main/java/org/apache/ambari/view/capacityscheduler/ConfigurationService.java
index e6d81d0..f703f65 100644
--- a/contrib/views/capacity-scheduler/src/main/java/org/apache/ambari/view/capacityscheduler/ConfigurationService.java
+++ b/contrib/views/capacity-scheduler/src/main/java/org/apache/ambari/view/capacityscheduler/ConfigurationService.java
@@ -114,9 +114,12 @@ public class ConfigurationService {
   // ================================================================================
 
   private final String versionTagUrl = "%s?fields=Clusters/desired_configs/capacity-scheduler";
-  private final String configurationUrl = "%%s/configurations?type=capacity-scheduler&tag=%s";
+  private final String configurationUrl = "%s/configurations?type=capacity-scheduler";
+  private final String configurationUrlByTag = "%%s/configurations?type=capacity-scheduler&tag=%s";
   private final String rmHostUrl = "%s/services/YARN/components/RESOURCEMANAGER?fields=host_components/host_name";
 
+  private final String rmGetNodeLabelUrl = "http://%s:8088/ws/v1/cluster/get-node-labels";
+
   // ================================================================================
   // Privilege Reading
   // ================================================================================
@@ -131,7 +134,7 @@ public class ConfigurationService {
    */
   @GET
   @Produces(MediaType.APPLICATION_JSON)
-  public Response readConfiguration() {
+  public Response readLatestConfiguration() {
     Response response = null;
     try {
       validateViewConfiguration();
@@ -149,6 +152,55 @@ public class ConfigurationService {
   }
 
   /**
+   * Gets capacity scheduler configuration by all tags.
+   *
+   * @return scheduler configuration
+   */
+  @GET
+  @Path("all")
+  @Produces(MediaType.APPLICATION_JSON)
+  public Response readAllConfigurations() {
+    Response response = null;
+    try {
+      validateViewConfiguration();
+
+      String url = String.format(configurationUrl, baseUrl);
+      JSONObject configurations = proxy.request(url).get().asJSON();
+      response = Response.ok(configurations).build();
+    } catch (WebApplicationException ex) {
+      throw ex;
+    } catch (Exception ex) {
+      throw new ServiceFormattedException(ex.getMessage(), ex);
+    }
+
+    return response;
+  }
+
+  /**
+   * Gets capacity scheduler configuration by specific tag.
+   *
+   * @return scheduler configuration
+   */
+  @GET
+  @Path("byTag/{tag}")
+  @Produces(MediaType.APPLICATION_JSON)
+  public Response readConfigurationByTag(@PathParam("tag") String tag) {
+    Response response = null;
+    try {
+      validateViewConfiguration();
+
+      JSONObject configurations = getConfigurationFromAmbari(tag);
+      response = Response.ok(configurations).build();
+    } catch (WebApplicationException ex) {
+      throw ex;
+    } catch (Exception ex) {
+      throw new ServiceFormattedException(ex.getMessage(), ex);
+    }
+
+    return response;
+  }
+
+  /**
    * Gets the privilege for this user.
    *
    * @return scheduler configuration
@@ -173,6 +225,31 @@ public class ConfigurationService {
   }
 
   /**
+   * Gets node labels from RM
+   *
+   * @return node labels
+   */
+  @GET
+  @Produces(MediaType.APPLICATION_JSON)
+  @Path("/nodeLabels")
+  public Response getNodeLabels() {
+    Response response;
+
+    try {
+      String nodeLabels = proxy.request(String.format(rmGetNodeLabelUrl, getRMHost())).get().asString();
+
+      response = Response.ok(nodeLabels).build();
+    } catch (WebApplicationException ex) {
+      throw ex;
+    } catch (Exception ex) {
+      throw new ServiceFormattedException(ex.getMessage(), ex);
+    }
+
+    return response;
+  }
+
+
+  /**
    * Checks if the user is an operator.
    *
    * @return    if <code>true</code>, the user is an operator; otherwise <code>false</code>
@@ -183,7 +260,7 @@ public class ConfigurationService {
       // first check if the user is an CLUSTER.OPERATOR
       String url = String.format(clusterOperatorPrivilegeUrl, baseUrl, context.getUsername()); 
       JSONObject json = proxy.request(url).get().asJSON();
-      
+
       if (json == null || json.size() <= 0) {
         // user is not a CLUSTER.OPERATOR but might be an AMBARI.ADMIN
         url = String.format(ambariAdminPrivilegeUrl, serverUrl, context.getUsername()); 
@@ -215,7 +292,7 @@ public class ConfigurationService {
   }
 
   private JSONObject getConfigurationFromAmbari(String versionTag) {
-    String urlTemplate = String.format(configurationUrl, versionTag);
+    String urlTemplate = String.format(configurationUrlByTag, versionTag);
     String url = String.format(urlTemplate, baseUrl);
     return proxy.request(url).get().asJSON();
   }
@@ -267,6 +344,7 @@ public class ConfigurationService {
   @Consumes(MediaType.APPLICATION_JSON)
   @Produces(MediaType.APPLICATION_JSON)
   public Response writeConfiguration(JSONObject request) {
+    JSONObject response;
     try {
       validateViewConfiguration();
 
@@ -274,9 +352,9 @@ public class ConfigurationService {
         return Response.status(401).build();
       }
 
-      proxy.request(baseUrl).
-            setData(makeConfigUpdateData(request)).
-            put();
+      response = proxy.request(baseUrl).
+                    setData(request).
+                    put().asJSON();
 
     } catch (WebApplicationException ex) {
       throw ex;
@@ -284,7 +362,7 @@ public class ConfigurationService {
       throw new ServiceFormattedException(ex.getMessage(), ex);
     }
 
-    return readConfiguration();
+    return Response.ok(response).build();
   }
 
   /**
@@ -302,8 +380,6 @@ public class ConfigurationService {
       if (isOperator() == false) {
         return Response.status(401).build();
       }
-      
-      writeConfiguration(request);
 
       String rmHost = getRMHost();
       JSONObject data = (JSONObject) JSONValue.parse(String.format(refreshRMRequestData, rmHost));
@@ -316,7 +392,7 @@ public class ConfigurationService {
     } catch (Exception ex) {
       throw new ServiceFormattedException(ex.getMessage(), ex);
     }
-    return readConfiguration();
+    return readLatestConfiguration();
   }
 
   /**
@@ -335,8 +411,6 @@ public class ConfigurationService {
         return Response.status(401).build();
       }
 
-      writeConfiguration(request);
-
       String rmHost = getRMHost();
       JSONObject data = (JSONObject) JSONValue.parse(String.format(restartRMRequestData, rmHost, rmHost));
       proxy.request(baseUrl + "/requests/").
@@ -348,7 +422,7 @@ public class ConfigurationService {
     } catch (Exception ex) {
       throw new ServiceFormattedException(ex.getMessage(), ex);
     }
-    return readConfiguration();
+    return readLatestConfiguration();
   }
 
   private String getRMHost() {
@@ -367,17 +441,4 @@ public class ConfigurationService {
     return rmHost;
   }
 
-  private JSONObject makeConfigUpdateData(JSONObject request) {
-    JSONObject desiredConfigs = (JSONObject) request.clone();
-    desiredConfigs.put("type", "capacity-scheduler");
-    desiredConfigs.put("tag", "version" + String.valueOf(System.currentTimeMillis()));
-
-    JSONObject clusters = new JSONObject();
-    clusters.put("desired_configs", desiredConfigs);
-
-    JSONObject data = new JSONObject();
-    data.put("Clusters", clusters);
-    return data;
-  }
-
 }

http://git-wip-us.apache.org/repos/asf/ambari/blob/95103a90/contrib/views/capacity-scheduler/src/main/resources/ui/app/adapters.js
----------------------------------------------------------------------
diff --git a/contrib/views/capacity-scheduler/src/main/resources/ui/app/adapters.js b/contrib/views/capacity-scheduler/src/main/resources/ui/app/adapters.js
index 3c47a90..07d6fbd 100644
--- a/contrib/views/capacity-scheduler/src/main/resources/ui/app/adapters.js
+++ b/contrib/views/capacity-scheduler/src/main/resources/ui/app/adapters.js
@@ -18,13 +18,36 @@
 
 var App = require('app');
 
+/**
+ * Gets the view backend URI
+ *
+ * @return  view backend URI
+ */
+function _getCapacitySchedulerViewUri(adapter) {
+  if (App.testMode)
+    return "/data";
+
+  var parts = window.location.pathname.match(/[^\/]*/g).filterBy('').removeAt(0),
+      view = parts[0],
+      version = parts[1],
+      instance = parts[2];
+  if (parts.length == 2) { // version is not present
+    instance = parts[1];
+    version = '';
+  }
+
+  return '/' + [adapter.namespace,'views',view,'versions',version,'instances',instance,'resources','scheduler','configuration'].join('/');
+}
+
+
 App.QueueAdapter = DS.Adapter.extend({
+  defaultSerializer:'queue',
   PREFIX: "yarn.scheduler.capacity",
+  namespace: 'api/v1',
   queues: [],
-  clusterName: '',
-  tag: '',
+
   createRecord: function(store, type, record) {
-    var data = this.serialize(record, { includeId: true });
+    var data = record.toJSON({ includeId: true });
     return new Ember.RSVP.Promise(function(resolve, reject) {
       return store.filter('queue',function (q) {
         return q.id === record.id;
@@ -34,50 +57,68 @@ App.QueueAdapter = DS.Adapter.extend({
           message = "Field can not be empty";
         } else if (queues.get('length') > 1) {
           message = "Queue already exists";
-        };
+        }
         if (message) {
           var error = new DS.InvalidError({path:[message]});
           store.recordWasInvalid(record, error.errors);
           return;
-        };
-
-        Ember.run(record, resolve, {'queue':data})
+        }
+        Ember.run(record, resolve, {'queue':data,'label':[]});
 
       });
     });
   },
 
+  deleteRecord:function (store, type, record) {
+    return new Ember.RSVP.Promise(function(resolve, reject) {
+      Ember.run(null, resolve, {'queue':record.serialize({ includeId: true , clone: true })});
+    });
+  },
+
   saveMark:'',
 
   updateRecord:function (store,type,record) {
-    var adapter = this,
-        saveMark = this.get('saveMark'),
-        uri = _getCapacitySchedulerUri(adapter.clusterName, adapter.tag),
+    var adapter = this;
+    var saveMark = this.get('saveMark'),
+        uri = _getCapacitySchedulerViewUri(this),
         serializer = store.serializerFor('queue'),
-        props = serializer.serializeConfig(record);
+        props = serializer.serializeConfig(record),
+        new_tag = 'version' + Math.floor(+moment()),
+        postSaveUri,data;
 
     if (saveMark) {
-      uri = [uri,saveMark].join('/');
+      postSaveUri = [_getCapacitySchedulerViewUri(this),saveMark].join('/');
       this.set('saveMark','');
-    };
+    }
+
+    data = JSON.stringify({'Clusters':
+      {'desired_config':
+        [{
+          'type': 'capacity-scheduler',
+          'tag': new_tag,
+          'service_config_version_note': props.service_config_version_note,
+          'properties': props.properties
+        }]
+      }
+    });
 
     return new Ember.RSVP.Promise(function(resolve, reject) {
-      adapter.ajax(uri,'PUT',{data:props}).then(function(data) {
-        Ember.run(null, resolve, data);
+      adapter.ajax(uri,'PUT',{contentType:'application/json; charset=utf-8',data:data}).then(function(data) {
+        store.setProperties({'current_tag':new_tag,'tag':new_tag});
+        Ember.run(null, resolve, data.resources.objectAt(0).configurations.objectAt(0).configs);
       }, function(jqXHR) {
         jqXHR.then = null;
         Ember.run(null, reject, jqXHR);
+      }).then(function () {
+        if (postSaveUri) {
+          adapter.postSave(postSaveUri);
+        }
       });
     });
   },
-  
-  /**
-   * Finds queue by id.
-   *
-   */
-  findQueue: function(id) {
-    var result = $.grep(App.Adapter.queues, function(e){ return e.get('id') == id; });
-    return result[0];
+
+  postSave:function(uri){
+    this.ajax(uri,'PUT',{contentType:'application/json; charset=utf-8',data:JSON.stringify({save:true})});
   },
 
   /**
@@ -85,9 +126,18 @@ App.QueueAdapter = DS.Adapter.extend({
    *
    */
   find: function(store, type, id) {
-    return store.findAll('queue').then(function (queues) {
-      return {"queue":queues.findBy('id',id).toJSON({includeId:true})};
-    });
+    id = id.toLowerCase();
+    var record = store.getById(type,id);
+    var key = type.typeKey;
+    if (record) {
+      return new Ember.RSVP.Promise(function(resolve, reject) {
+        resolve({key:record.toJSON({includeId:true})});
+      });
+    } else {
+      return store.findAll('queue').then(function (queues) {
+        resolve({key:store.getById(type,id).toJSON({includeId:true})});
+      });
+    }
   },
 
   /**
@@ -96,23 +146,62 @@ App.QueueAdapter = DS.Adapter.extend({
    */
   findAll: function(store, type) {
     var adapter = this;
-    var uri = _getCapacitySchedulerUri(adapter.clusterName, adapter.tag);
+    var uri = _getCapacitySchedulerViewUri(this);
     if (App.testMode)
       uri = uri + "/scheduler-configuration.json";
+
     return new Ember.RSVP.Promise(function(resolve, reject) {
-      adapter.ajax(uri).then(function(data) {
+      adapter.ajax(uri,'GET').then(function(data) {
+        var config = data.items.objectAt(0);
+        if (!store.get('isInitialized')) {
+          store.set('current_tag',config.tag);
+        }
+        store.setProperties({'clusterName':config.Config.cluster_name,'tag':config.tag});
+        Ember.run(null, resolve, data.items.objectAt(0).properties);
+      }, function(jqXHR) {
+        jqXHR.then = null;
+        Ember.run(null, reject, jqXHR);
+      });
+    });
+  },
+
+  findAllTagged: function(store, type) {
+    var adapter = this,
+        uri = [_getCapacitySchedulerViewUri(this),'byTag',store.get('tag')].join('/');
+
+    return new Ember.RSVP.Promise(function(resolve, reject) {
+      adapter.ajax(uri,'GET').then(function(data) {
         Ember.run(null, resolve, data);
       }, function(jqXHR) {
         jqXHR.then = null;
         Ember.run(null, reject, jqXHR);
       });
     });
+  },
+
+  getNodeLabels:function () {
+    var uri = [_getCapacitySchedulerViewUri(this),'nodeLabels'].join('/');
 
+    return new Ember.RSVP.Promise(function(resolve, reject) {
+      this.ajax(uri,'GET').then(function(data) {
+        var parsedData = JSON.parse(data), labels;
+          if (parsedData && Em.isArray(parsedData.nodeLabels)) {
+            labels = parsedData.nodeLabels;
+          } else {
+            labels = (parsedData && parsedData.nodeLabels)?[parsedData.nodeLabels]:[];
+          }
+        Ember.run(null, resolve, labels.map(function (label) {
+          return {name:label};
+        }));
+      }, function(jqXHR) {
+        jqXHR.then = null;
+        Ember.run(null, reject, jqXHR);
+      });
+    }.bind(this));
   },
 
   getPrivilege:function () {
-    var uri = _getCapacitySchedulerUri(this.clusterName, this.tag);
-    uri = [uri,'privilege'].join('/');
+    var uri = [_getCapacitySchedulerViewUri(this),'privilege'].join('/');
     if (App.testMode)
       uri = uri + ".json";
     return new Ember.RSVP.Promise(function(resolve, reject) {
@@ -124,7 +213,7 @@ App.QueueAdapter = DS.Adapter.extend({
       });
     }.bind(this));
   },
-     
+
   ajax: function(url, type, hash) {
     var adapter = this;
 
@@ -150,10 +239,6 @@ App.QueueAdapter = DS.Adapter.extend({
     hash.dataType = 'json';
     hash.context = this;
 
-    if (hash.data && type !== 'GET') {
-      hash.contentType = 'application/json; charset=utf-8';
-      hash.data = JSON.stringify(hash.data);
-    }
     hash.beforeSend = function (xhr) {
       xhr.setRequestHeader('X-Requested-By', 'view-capacity-scheduler');
     };
@@ -177,277 +262,23 @@ App.SchedulerAdapter = App.QueueAdapter.extend({
   }
 });
 
-App.ApplicationStore = DS.Store.extend({
-
-  adapter: App.QueueAdapter,
-
-  configNote:'',
-
-  markForRefresh:function (mark) {
-    this.set('defaultAdapter.saveMark','saveAndRefresh');
-  },
-
-  markForRestart:function (mark) {
-    this.set('defaultAdapter.saveMark','saveAndRestart');
-  },
-
-  flushPendingSave: function() {
-    var pending = this._pendingSave.slice(),
-        newPending = [[]],scheduler;
-
-    if (pending.length == 1) {
-      this._super();
-      return;
-    };
-    
-    pending.forEach(function (tuple) {
-      var record = tuple[0], resolver = tuple[1],
-          operation;
-      newPending[0].push(record)
-      newPending[1] = resolver;
-    });
-
-    this._pendingSave = [newPending];
-    this._super();
-  },
-  didSaveRecord: function(record, data) {
-    if (Em.isArray(record)) {
-      for (var i = 0; i < record.length; i++) {
-        this._super(record[i],data.findBy('id',record[i].id));
-      };
-    } else {
-      this._super(record, data);
-    };
-  },
-  recordWasError: function(record) {
-    if (Em.isArray(record)) {
-      for (var i = 0; i < record.length; i++) {
-        record[i].adapterDidError();
-      };
-    } else {
-      record.adapterDidError();
-    }
-  },
-  checkOperator:function () {
-    return this.get('defaultAdapter').getPrivilege();
-  }
-});
-
-App.ApplicationSerializer = DS.RESTSerializer.extend({
-
-  PREFIX:"yarn.scheduler.capacity",
-
-  serializeConfig:function (records) {
-    var config = {},
-        note = this.get('store.configNote'),
-        prefix = this.PREFIX;
-    records.forEach(function (record) {
-      if (record.id == 'scheduler') {
-        config[prefix + ".maximum-am-resource-percent"] = record.get('maximum_am_resource_percent')/100; // convert back to decimal
-        config[prefix + ".maximum-applications"] = record.get('maximum_applications');
-        config[prefix + ".node-locality-delay"] = record.get('node_locality_delay');
-        config[prefix + ".resource-calculator"] = record.get('resource_calculator');
-      } else {
-        config[prefix + "." + record.get('path') + ".unfunded.capacity"] = record.get('unfunded_capacity');
-        config[prefix + "." + record.get('path') + ".acl_administer_queue"] = record.get('acl_administer_queue');
-        config[prefix + "." + record.get('path') + ".acl_submit_applications"] = record.get('acl_submit_applications');
-        config[prefix + "." + record.get('path') + ".minimum-user-limit-percent"] = record.get('minimum_user_limit_percent');
-        config[prefix + "." + record.get('path') + ".maximum-capacity"] = record.get('maximum_capacity');
-        config[prefix + "." + record.get('path') + ".user-limit-factor"] = record.get('user_limit_factor');
-        config[prefix + "." + record.get('path') + ".state"] = record.get('state');
-        config[prefix + "." + record.get('path') + ".capacity"] = record.get('capacity');
-        config[prefix + "." + record.get('path') + ".queues"] = record.get('queueNames')||'';
-
-        // do not set property if not set
-        var ma = record.get('maximum_applications')||'';
-        if (ma) {
-          config[prefix + "." + record.get('path') + ".maximum-applications"] = ma;
-        }
+App.TagAdapter = App.QueueAdapter.extend({
+  /**
+   * Finds all tags.
+   *
+   */
+  findAll: function(store, type) {
+    var adapter = this;
+    var uri = [_getCapacitySchedulerViewUri(this),'all'].join('/');
 
-        // do not set property if not set
-        var marp = record.get('maximum_am_resource_percent')||'';
-        if (marp) {
-          marp = marp/100; // convert back to decimal
-          config[prefix + "." + record.get('path') + ".maximum-am-resource-percent"] = marp;
-        }
-      };
+    return new Ember.RSVP.Promise(function(resolve, reject) {
+      adapter.ajax(uri ,'GET').then(function(data) {
+        Ember.run(null, resolve, data);
+      }, function(jqXHR) {
+        jqXHR.then = null;
+        Ember.run(null, reject, jqXHR);
+      });
     });
-
-    for (var i in config) {
-      if (config[i] === null || config[i] === undefined) {
-        delete config[i];
-      }
-    }
-
-    this.set('store.configNote','');
-
-    return {properties : config, service_config_version_note: note};
-
-  },
-  normalizeConfig:function (store, payload) {
-    var queues = [];
-    var props = payload.items[0].properties;
-
-    var scheduler = [{
-      id:'scheduler',
-      maximum_am_resource_percent:props[this.PREFIX + ".maximum-am-resource-percent"]*100, // convert to percent
-      maximum_applications:props[this.PREFIX + ".maximum-applications"],
-      node_locality_delay:props[this.PREFIX + ".node-locality-delay"],
-      resource_calculator:props[this.PREFIX + ".resource-calculator"]
-    }];
-    queues = _recurseQueues(null, "root", 0, props, queues, store);
-
-    return {'queue':queues,'scheduler':scheduler};
-  },
-
-  extractFindAll: function(store, type, payload){
-    var queues = [];
-    var props = payload.items[0].properties;
-
-    var scheduler = {
-      id:'scheduler',
-      maximum_am_resource_percent:props[this.PREFIX + ".maximum-am-resource-percent"]*100, // convert to percent
-      maximum_applications:props[this.PREFIX + ".maximum-applications"],
-      node_locality_delay:props[this.PREFIX + ".node-locality-delay"],
-      resource_calculator:props[this.PREFIX + ".resource-calculator"]
-    };
-    queues = _recurseQueues(null, "root", 0, props, queues, store);
-
-    var config = this.normalizeConfig(store, payload);
-    
-    return this.extractArray(store, type, config);
-  },
-
-  extractUpdateRecord:function (store, type, payload, id, requestType) {
-    var queues = [];
-    var props = payload.items[0].properties;
-
-    var scheduler = {
-      id:'scheduler',
-      maximum_am_resource_percent:props[this.PREFIX + ".maximum-am-resource-percent"]*100, // convert to percent
-      maximum_applications:props[this.PREFIX + ".maximum-applications"],
-      node_locality_delay:props[this.PREFIX + ".node-locality-delay"],
-      resource_calculator:props[this.PREFIX + ".resource-calculator"]
-    };
-    queues = _recurseQueues(null, "root", 0, props, queues, store);
-    
-    var config = this.normalizeConfig(store, payload);
-
-    return this.extractConfig(store, App.Queue, {'queue':queues,'scheduler':[scheduler]});
-  },
-
-  extractConfig: function(store, primaryType, payload) {
-      payload = this.normalizePayload(payload);
-
-      var primaryTypeName = primaryType.typeKey,
-          primaryArray = [],
-          scheduler,queues;
-
-      for (var prop in payload) {
-        var typeKey = prop,
-            forcedSecondary = false;
-
-        if (prop.charAt(0) === '_') {
-          forcedSecondary = true;
-          typeKey = prop.substr(1);
-        }
-
-        var typeName = this.typeForRoot(typeKey),
-            type = store.modelFor(typeName),
-            typeSerializer = store.serializerFor(type),
-            isPrimary = (!forcedSecondary && (type.typeKey === primaryTypeName));
-
-        var normalizedArray = Ember.ArrayPolyfills.map.call(payload[prop], function(hash) {
-          return typeSerializer.normalize(type, hash, prop);
-        }, this);
-
-        if (typeKey == App.Scheduler.typeKey) {
-          scheduler = normalizedArray;
-        } else {
-          queues = normalizedArray;
-        }
-      }
-
-      return scheduler.concat(queues);
-    },
-
-  extractQueue: function(data, props) {
-    var q = { name: data.name, parentPath: data.parentPath, depth: data.depth };
-    var prefix = this.PREFIX;
-
-    if (q.parentPath == null || q.parentPath.length == 0){
-        q.path = q.name;
-      } else {
-        q.path = q.parentPath + '.' + q.name;
-      }
-      q.id = q.path.dasherize();
-
-    q.unfunded_capacity = props[prefix + "." + q.path + ".unfunded.capacity"];
-
-    q.state = props[prefix + "." + q.path + ".state"];
-    q.acl_administer_queue = props[prefix + "." + q.path + ".acl_administer_queue"];
-    q.acl_submit_applications = props[prefix + "." + q.path + ".acl_submit_applications"];
-
-    q.capacity = props[prefix + "." + q.path + ".capacity"];
-    q.maximum_capacity = props[prefix + "." + q.path + ".maximum-capacity"];
-
-    q.user_limit_factor = props[prefix + "." + q.path + ".user-limit-factor"];
-    q.minimum_user_limit_percent = props[prefix + "." + q.path + ".minimum-user-limit-percent"];
-    q.maximum_applications = props[prefix + "." + q.path + ".maximum-applications"];
-    q.maximum_am_resource_percent = props[prefix + "." + q.path + ".maximum-am-resource-percent"]
-
-    if (q.maximum_am_resource_percent)
-      q.maximum_am_resource_percent = q.maximum_am_resource_percent*100; // convert to percent
-
-    q.queueNames = props[prefix + "." + q.path + ".queues"];
-
-    return q;
   }
 });
 
-/**
- * Recursively builds the list of queues.
- *
- */
-function _recurseQueues(parentQueue, queueName, depth, props, queues, store) {
-  var serializer = store.serializerFor('queue');
-  var prefix = serializer.PREFIX;
-  var parentPath = '';
-  if (parentQueue != null) {
-    parentPath = parentQueue.path;
-    prefix += ".";
-  }
-
-  var queue = serializer.extractQueue({ name: queueName, parentPath: parentPath, depth: depth}, props);
-  queues.push(queue);
-
-  var queueProp = prefix + parentPath + "." + queueName + ".queues";
-  if (props[queueProp]) {
-    var qs = props[queueProp].split(',');
-    for (var i=0; i < qs.length; i++) {
-      queues = _recurseQueues(queue, qs[i], depth+1, props, queues, store)
-    }
-  }
-  
-  return queues;
-}
-
-/**
- * Gets the cluster name URI based on test mode
- *
- * @return  cluster name URI
- */
-function _getCapacitySchedulerUri() {
-  if (App.testMode)
-    return "/data";
-    
-  var parts = window.location.pathname.match(/\/[^\/]*/g);
-  var view = parts[1];
-  var version = '/versions' + parts[2];
-  var instance = parts[3];
-  if (parts.length == 4) { // version is not present
-    instance = parts[2];
-    version = '';
-  }
-  return '/api/v1/views' + view + version + '/instances' + instance+'/resources/scheduler/configuration';
-}

http://git-wip-us.apache.org/repos/asf/ambari/blob/95103a90/contrib/views/capacity-scheduler/src/main/resources/ui/app/assets/index.html
----------------------------------------------------------------------
diff --git a/contrib/views/capacity-scheduler/src/main/resources/ui/app/assets/index.html b/contrib/views/capacity-scheduler/src/main/resources/ui/app/assets/index.html
index ce0972c..2cdfe39 100644
--- a/contrib/views/capacity-scheduler/src/main/resources/ui/app/assets/index.html
+++ b/contrib/views/capacity-scheduler/src/main/resources/ui/app/assets/index.html
@@ -34,13 +34,11 @@
 </head>
 <body>
   <script type="text/x-handlebars">
-  <div class="wrap">
     <div class="container-fluid">
       <div class="row">
         {{outlet}}
       </div>
     </div>
-  </div>
   </script>
 
 

http://git-wip-us.apache.org/repos/asf/ambari/blob/95103a90/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/capacityBar.js
----------------------------------------------------------------------
diff --git a/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/capacityBar.js b/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/capacityBar.js
index 22d511b..6f35e28 100644
--- a/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/capacityBar.js
+++ b/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/capacityBar.js
@@ -20,22 +20,36 @@ var App = require('app');
 
 App.CapacityBarComponent = Em.Component.extend({
   layoutName:'components/capacityBar',
+
   classNames:['capacity-bar'],
+
+  capacityValue:null,
+
+  maxCapacityValue:null,
+
+  warn:false,
+
+  pattern:'width: %@%;',
+
   mark:function () {
     if (this.$().parent().hasClass('active')) {
       Ember.run.next(this, 'addInClass');
-    };
+    }
   }.on('didInsertElement'),
+
   addInClass:function () {
     this.$().children().addClass('in');
   },
+
   capacityWidth: function() {
-    return  (Number(this.get('capacityValue'))<=100)?('width: '+this.get('capacityValue')+'%'):'width: 100%';
+    return this.get('pattern').fmt(( +this.get('capacityValue') <= 100 ) ? +this.get('capacityValue') : 100);
   }.property('capacityValue'),
+
   maxCapacityWidth: function() {
-    var val = Number(this.get('maxCapacityValue')) - Number(this.get('capacityValue'));
-    return  (val<=100)?('width: '+val+'%'):'width: 100%';
+    var val = this.get('maxCapacityValue') - this.get('capacityValue');
+    return  this.get('pattern').fmt((val<=100)?val:100);
   }.property('maxCapacityValue','capacityValue'),
+
   showMaxCapacity:function () {
     return this.get('maxCapacityValue') > this.get('capacityValue');
   }.property('maxCapacityValue','capacityValue')

http://git-wip-us.apache.org/repos/asf/ambari/blob/95103a90/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/capacityInput.js
----------------------------------------------------------------------
diff --git a/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/capacityInput.js b/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/capacityInput.js
index 0bf4b85..8ac766d 100644
--- a/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/capacityInput.js
+++ b/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/capacityInput.js
@@ -24,8 +24,7 @@ App.InputRangeComponent = Em.TextField.extend({
   action: 'mouseUp',
 
   mouseUp: function () {
-    var value = this.get('value');
-    this.sendAction('action', value);
+    this.sendAction('action', this.get('value'));
   }
 });
 
@@ -42,35 +41,29 @@ App.IntInputComponent = Ember.TextField.extend({
   classNames:['form-control'],
   maxVal:null,
   intVal:function () {
-    var val = parseFloat(this.get('value'))||0;
+    var val = (!Em.isBlank(this.get('value'))) ? parseFloat(this.get('value')) : null;
     var maxVal = this.get('maxVal');
-    val = (maxVal && maxVal < val)?maxVal:val;
-    this.set('value', val);
+    this.set('value', (maxVal && maxVal < val)?maxVal:val);
   }.on('change'),
   checkNumber:function () {
-    var val = this.get('value'),
-        num = Number(val),
-        str = String(val);
-    if (typeof val !== "number" && !isNaN(num) && str == num.toString()) {
-      this.set('value', Number(val));
-    };
+    this.set('value', (!Em.isBlank(this.get('value')) && !isNaN(parseFloat(this.get('value')))) ? parseFloat(this.get('value')): null);
   }.observes('value')
 });
 
 App.CapacityInputComponent = App.IntInputComponent.extend({
-  
+
   totalCapacity:null,
   queue:null,
 
   keyDown: function(evt) {
     var newChar, val = this.get('value')||0;
     val = val.toString();
-    
-    if ((evt.keyCode > 64 && evt.keyCode < 91) || 
-      (evt.keyCode > 185 && evt.keyCode < 193) || 
+
+    if ((evt.keyCode > 64 && evt.keyCode < 91) ||
+      (evt.keyCode > 185 && evt.keyCode < 193) ||
       (evt.keyCode > 218 && evt.keyCode < 223)) {
       return false;
-    };
+    }
 
     if (evt.keyCode > 95 && evt.keyCode < 106) {
       newChar = (evt.keyCode - 96).toString();
@@ -80,25 +73,31 @@ App.CapacityInputComponent = App.IntInputComponent.extend({
 
     if (newChar.match(/[0-9]/)) {
       val = val.substring(0, evt.target.selectionStart) + newChar + val.substring(evt.target.selectionEnd);
-    };
+    }
 
-    return parseFloat(val)<=100;
-  },
+    return parseFloat(val) <= 100;
+  }
 });
 
 App.MaxCapacityInputComponent = App.CapacityInputComponent.extend({
   isInvalid:false,
   invalid:function (c,o) {
-    var queue = this.get('queue'),
-        max_capacity = Number(queue.get('maximum_capacity')),
-        capacity = Number(queue.get('capacity'));
+    var queue = this.get('queue'), max_capacity, capacity;
+
+    if (queue.get('maximum_capacity') === null) return;
+
+    max_capacity = +queue.get('maximum_capacity');
+    capacity = +queue.get('capacity');
+
     if (o == 'queue.capacity' && max_capacity < capacity) {
       return queue.set('maximum_capacity',capacity);
-    };
+    }
+
     if (max_capacity < capacity && queue.get('isDirty')) {
       queue.get('errors').add('maximum_capacity', 'Maximum must be equal or greater than capacity');
     } else {
       queue.get('errors').remove('maximum_capacity');
     }
+
   }.observes('queue.maximum_capacity','queue.capacity')
 });

http://git-wip-us.apache.org/repos/asf/ambari/blob/95103a90/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/confirmDelete.js
----------------------------------------------------------------------
diff --git a/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/confirmDelete.js b/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/confirmDelete.js
index 8b61ca3..5ada051 100644
--- a/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/confirmDelete.js
+++ b/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/confirmDelete.js
@@ -36,16 +36,18 @@ App.ConfirmDeleteComponent = Em.Component.extend(App.ClickElsewhereMixin,{
   tagName:'a',
   tooltip:function () {
     var element = this.$();
+
     if (this.get('confirm')) {
       element.tooltip({
         placement:'left',
         title:'Click again to confirm'
       }).tooltip('show');
     } else {
-      element.tooltip('destroy')
+      element.tooltip('destroy');
     }
+
   }.observes('confirm'),
-  click:function (e) {
+  click:function () {
     this.send('delete');
   },
   didChange:function () {

http://git-wip-us.apache.org/repos/asf/ambari/blob/95103a90/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/dropdownConfirmation.js
----------------------------------------------------------------------
diff --git a/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/dropdownConfirmation.js b/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/dropdownConfirmation.js
index 105d960..d86e3a0 100644
--- a/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/dropdownConfirmation.js
+++ b/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/dropdownConfirmation.js
@@ -20,6 +20,7 @@ var App = require('app');
 
 
 App.DropdownConfirmationComponent = Em.Component.extend(App.ClickElsewhereMixin,{
+  layoutName:'components/dropdownConfirmation',
   tagName:'li',
   restartConfirming:false,
   actions:{
@@ -36,20 +37,20 @@ App.DropdownConfirmationComponent = Em.Component.extend(App.ClickElsewhereMixin,
     this.$().parents('.dropdown-menu').parent().removeClass('open');
   },
   dropdownHideControl:function () {
-    var _this = this;
     this.$().parents('.dropdown-menu').parent().on(
       "hide.bs.dropdown", function() {
-        return !_this.get('restartConfirming');
-      });
+        return !this.get('restartConfirming');
+      }.bind(this));
   }.on('didInsertElement'),
   button:Em.Component.extend({
     tagName:'a',
-    click:function (e) {
+    click:function () {
       this.triggerAction({
         action: 'showRestartConfirmation',
         target: this.get('parentView'),
         actionContext: this.get('context')
       });
     }
-  })
+  }),
+  needRestart:false
 });

http://git-wip-us.apache.org/repos/asf/ambari/blob/95103a90/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/escapeAcl.js
----------------------------------------------------------------------
diff --git a/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/escapeAcl.js b/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/escapeAcl.js
index b5a3e07..125b037 100644
--- a/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/escapeAcl.js
+++ b/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/escapeAcl.js
@@ -19,7 +19,7 @@
 
 Ember.Handlebars.helper('escapeACL', function(value) {
   var output = '';
-  
+
   value = value || '';
 
   if (value.trim() == '') {
@@ -28,21 +28,22 @@ Ember.Handlebars.helper('escapeACL', function(value) {
     output = '<label class="label label-success"> <i class="fa fa-asterisk fa-fw"></i> Anyone</label>';
   } else {
     var ug = value.split(' ');
-    var users = ug[0].split(',')||[]; 
+    var users = ug[0].split(',')||[];
     var groups = (ug.length == 2)?ug[1].split(',')||[]:[];
 
-    output += ' <span class="users"> '
+    output += ' <span class="users"> ';
 
     users.forEach(function (user) {
       output += (user)?'<span class="label label-primary"><i class="fa fa-user fa-fw"></i> '+ user +'</span> ':'';
     });
 
-    output += ' </span> <span class="groups"> '
+    output += ' </span> <span class="groups"> ';
 
     groups.forEach(function (group) {
       output += (group)?'<span class="label label-primary"><i class="fa fa-users fa-fw"></i> '+ group +'</span> ':'';
     });
-    output += ' </span> '
+
+    output += ' </span> ';
   }
   return new Ember.Handlebars.SafeString(output);
 });

http://git-wip-us.apache.org/repos/asf/ambari/blob/95103a90/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/pathInput.js
----------------------------------------------------------------------
diff --git a/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/pathInput.js b/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/pathInput.js
index 92bfb31..fc8f77c 100644
--- a/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/pathInput.js
+++ b/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/pathInput.js
@@ -39,7 +39,7 @@ App.PathInputComponent = Em.Component.extend({
     cancel:function () {
       this.set('activeFlag',false);
     }
-  }, 
+  },
   queues:[],
   activeFlag:false,
   pathMap:Em.computed.mapBy('queues','path'),
@@ -60,7 +60,7 @@ App.PathInputComponent = Em.Component.extend({
       this.$().typeahead({
           source: this.get('pathSource'),
           matcher: function (item) {
-            return ~item.toLowerCase().indexOf(this.query.toLowerCase())
+            return ~item.toLowerCase().indexOf(this.query.toLowerCase());
           },
           minLength:2,
           items:100,
@@ -82,4 +82,4 @@ App.PathInputComponent = Em.Component.extend({
       this.$().tooltip('destroy');
     }
   })
-});
\ No newline at end of file
+});

http://git-wip-us.apache.org/repos/asf/ambari/blob/95103a90/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/queueListItem.js
----------------------------------------------------------------------
diff --git a/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/queueListItem.js b/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/queueListItem.js
index c9bf813..fc52eb9 100644
--- a/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/queueListItem.js
+++ b/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/queueListItem.js
@@ -18,7 +18,7 @@
 
 var App = require('app');
 
-App.RecurceQueuesComponent = Ember.View.extend({
+App.RecurceQueuesComponent = Em.View.extend({
   templateName: "components/queueListItem",
   depth:0,
   parent:'',
@@ -40,3 +40,68 @@ App.RecurceQueuesComponent = Ember.View.extend({
   }
 
 });
+
+App.DiffTooltipComponent = Em.Component.extend({
+  classNames:'fa fa-fw fa-lg blue fa-pencil'.w(),
+  tagName:'i',
+  queue:null,
+  initTooltip:function  () {
+    var queue = this.get('queue');
+    this.$().tooltip({
+        title:function () {
+          var caption = '',
+              fmtString = '<span>%@: %@ -> %@</span>\n',
+              emptyValue = '<small><em>not set</em></small>',
+              changes = queue.changedAttributes(),
+              idsToNames = function (l) {
+                return l.split('.').get('lastObject');
+              },
+              formatChangedAttributes = function (prefix,item) {
+                // don't show this to user.
+                if (item == '_accessAllLabels') return;
+
+                var oldV = this[item].objectAt(0),
+                    newV = this[item].objectAt(1);
+
+                caption += fmtString.fmt(
+                    [prefix,item].compact().join('.'),
+                    (oldV != null && '\'%@\''.fmt(oldV)) || emptyValue,
+                    (newV != null && '\'%@\''.fmt(newV)) || emptyValue
+                  );
+              },
+              initialLabels,
+              currentLabels,
+              isAllChanged,
+              oldV,
+              newV;
+
+          Em.keys(changes).forEach(Em.run.bind(changes,formatChangedAttributes,null));
+
+          if (queue.constructor.typeKey === 'queue') {
+            //cpmpare labels
+            isAllChanged = changes.hasOwnProperty('_accessAllLabels');
+            initialLabels = queue.get('initialLabels').sort();
+            currentLabels = queue.get('labels').mapBy('id').sort();
+
+            if (queue.get('isLabelsDirty') || isAllChanged) {
+
+              oldV = ((isAllChanged && changes._accessAllLabels.objectAt(0)) || (!isAllChanged && queue.get('_accessAllLabels')))?'*':initialLabels.map(idsToNames).join(',') || emptyValue;
+              newV = ((isAllChanged && changes._accessAllLabels.objectAt(1)) || (!isAllChanged && queue.get('_accessAllLabels')))?'*':currentLabels.map(idsToNames).join(',') || emptyValue;
+
+              caption += fmtString.fmt('accessible-node-labels', oldV, newV);
+            }
+
+            queue.get('labels').forEach(function (label) {
+              var labelsChanges = label.changedAttributes(),
+                  prefix = ['accessible-node-labels',label.get('name')].join('.');
+              Em.keys(labelsChanges).forEach(Em.run.bind(labelsChanges,formatChangedAttributes,prefix));
+            });
+          }
+
+          return caption;
+        },
+        html:true,
+        placement:'bottom'
+      });
+  }.on('didInsertElement')
+});

http://git-wip-us.apache.org/repos/asf/ambari/blob/95103a90/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/radioButton.js
----------------------------------------------------------------------
diff --git a/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/radioButton.js b/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/radioButton.js
index 30fd531..8be8919 100644
--- a/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/radioButton.js
+++ b/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/radioButton.js
@@ -23,7 +23,7 @@ App.RadioButtonInputComponent = Ember.View.extend({
     type : "radio",
     attributeBindings : [ "type", "value", "checked:checked" ],
     click : function() {
-      this.set("selection", this.get('value'))
+      this.set("selection", this.get('value'));
     }
 });
 
@@ -35,12 +35,12 @@ App.RadioButtonComponent = Em.Component.extend({
   //arguments
   selection:null,
   label:null,
-  value:null, 
+  value:null,
   click : function() {
     this.set("selection", this.get('value'))
   },
   isActive : function() {
-    return this.get("value") == this.get("selection");   
+    return this.get("value") == this.get("selection");
   }.property("selection"),
   layout:Em.Handlebars.compile('{{label}} {{radio-button-input selection=selection value=value checked=isActive}}')
 });

http://git-wip-us.apache.org/repos/asf/ambari/blob/95103a90/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/totalCapacity.js
----------------------------------------------------------------------
diff --git a/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/totalCapacity.js b/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/totalCapacity.js
index 806cf3b..d4fc80b 100644
--- a/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/totalCapacity.js
+++ b/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/totalCapacity.js
@@ -21,55 +21,149 @@ var App = require('app');
 App.TotalCapacityComponent = Ember.Component.extend({
   layoutName:'components/totalCapacity',
 
+  // TAKEN VALUES
+
+  /**
+   * Speaks for itself.
+   * @type {App.Queue}
+   */
+  currentQueue:null,
+
+  /**
+   * All queues in store.
+   * @type {DS.RecordArray}
+   */
+  allQueues:[],
+
+  /**
+   * All queues in store sorted by App.QueuesController
+   * @type {Array}
+   */
+  allQueuesArranged:[],
+
+  /**
+   * User admin status.
+   * @type {Boolean}
+   */
+  isOperator:false,
+
+  /**
+   * Target for rollbackProp action.
+   * @type {String}
+   */
+  rollbackProp:'',
+
+  // ACTIONS
+
   actions:{
-    toggleEdit:function () {
-      this.toggleProperty('isEdit');
-    },
-    addQueue:function (path) {
-      this.sendAction('addQueue',path);
+    toggleProperty:function (property,target) {
+      target = target || this;
+      target.toggleProperty(property);
     },
-    createQueue:function (queue) {
-      this.sendAction('createQueue',queue);
+    rollbackProp:function (prop, item) {
+      this.sendAction('rollbackProp', prop, item);
     },
-    deleteQueue:function (queue) {
-      this.sendAction('deleteQueue',queue);
+    toggleLabel:function (labelName, queue) {
+      var q = queue || this.get('currentQueue'),
+          labelRecord = q.store.getById('label',[q.get('path'),labelName].join('.').toLowerCase());
+
+      if (q.get('labels').contains(labelRecord)) {
+        this.recurseRemoveLabel(q,labelRecord);
+      } else {
+        q.get('labels').pushObject(labelRecord);
+      }
+      q.notifyPropertyChange('labels');
     }
   },
 
   /**
-   * passed params
+   * @param  {App.Queue} target queue
+   * @param  {App.Label} label ralated to queue. All labels with it's name will be removed from child queues.
+   * @method recurseRemoveLabel
    */
-  currentQueue:null,
-  allQueues:[],
-  allQueuesArranged:[],
+  recurseRemoveLabel:function(queue,label) {
+    label = queue.get('labels').findBy('name',label.get('name'));
+    if (label) {
+      queue.get('labels').removeObject(label);
+      this.get('allQueues').filterBy('parentPath',queue.get('path')).forEach(function (child) {
+        this.recurseRemoveLabel(child,label);
+      }.bind(this));
+    }
+  },
 
-  isEdit: true,
-  
-  disableEdit:function () {
-    this.set('isEdit',false);
-  }.observes('allQueues'),
+  // COMPUTED PROPERTIES
 
+  /**
+   * Path for current queue parent.
+   * @type {String}
+   */
   currentPrPath:Em.computed.alias('currentQueue.parentPath'),
 
-  leafQueuesCapacity: Ember.computed.map('leafQueues.@each.capacity', function (queue) {
-    return Number(queue.get('capacity')); 
-  }),
+  /**
+   * Current queue parent.
+   * @type {App.Queue}
+   */
+  parentQueue:function () {
+    return this.allQueues.findBy('path',this.get('currentPrPath'));
+  }.property('allQueues','currentPrPath'),
 
-  totalCapacity: Ember.computed.sum('leafQueuesCapacity'),
-  
+  /**
+   * Leaf queues.
+   * @type {Array}
+   */
   leafQueues:function () {
     return this.allQueuesArranged.filterBy('parentPath',this.get('currentPrPath')).filterBy('isNew',false);
   }.property('allQueuesArranged.length','currentPrPath'),
 
-  newLeafQueues:function () {
-    return this.allQueues.filterBy('parentPath',this.get('currentPrPath')).filterBy('isNew',true);
-  }.property('allQueues.length','currentPrPath'),
-  
-  parentQueue:function () {
-    return this.allQueues.findBy('path',this.get('currentPrPath'));
-  }.property('allQueues','currentPrPath'),
+  /**
+   * Array of leaf queues capacities.
+   * @type {Array}
+   */
+  leafQueuesCapacity: Ember.computed.map('leafQueues.@each.capacity', function (queue) {
+    return +queue.get('capacity');
+  }),
+
+  /**
+   * Sum of leaf queues capacities.
+   * @type {Number}
+   */
+  totalCapacity: Ember.computed.sum('leafQueuesCapacity'),
+
+  /**
+   * Node labels stored in store.
+   * @type {Array}
+   */
+  nodeLabels:Ember.computed.alias('currentQueue.store.nodeLabels.content'),
 
-  currentInLeaf:function (argument) {
+  /**
+   * Array of arrays of node labels.
+   * Made to split node labels in several rows.
+   *
+   * @return {Array}
+   */
+  arrangedNodeLabels:function () {
+    var llength = this.get('nodeLabels.length'),
+        cols = (llength % 4 === 1)?3:4,
+        arranged = [];
+
+    this.get('nodeLabels').forEach(function (l, idx, all) {
+      if (idx%cols === 0) {
+        arranged.pushObject([l]);
+      } else {
+        arranged.get('lastObject').pushObject(l)
+      }
+    });
+
+    return arranged;
+  }.property('nodeLabels.length'),
+
+  // OBSERVABLES
+
+  /**
+   * Marks currentQueue with 'isCurrent' flag.
+   * @method currentInLeaf
+   */
+  currentInLeaf:function () {
     var queues = this.get('allQueues');
     queues.setEach('isCurrent',false);
     if(!this.get('currentQueue.currentState.stateName').match(/root.deleted/)) {
@@ -77,22 +171,251 @@ App.TotalCapacityComponent = Ember.Component.extend({
     }
   }.observes('leafQueues','currentQueue').on('init'),
 
-  newQueueNameField: Em.TextField.extend({
+  // CHILD VIEWS
+
+  /**
+   * Toggle buttons for node labels.
+   * @type {Em.View}
+   */
+  nodeLabelsToggles: Em.View.extend(Ember.ViewTargetActionSupport,{
+
+    // TAKEN VALUES
+
+    /**
+     * Name of node label.
+     * @type {[type]}
+     */
+    labelName:null,
+    /**
+     * Node label's existence flag.
+     * @type {Boolean}
+     */
+    notExist:false,
+
+    /**
+     * Queue related to label.
+     * @type {App.Queue}
+     */
     queue:null,
-    classNames:['form-control'],
-    classNameBindings:['isValid::input-error'],
-    isValid:Em.computed.bool('queue.isValid')
+
+    /**
+     * Queue's leaf.
+     * @type {Array}
+     */
+    leaf:null,
+
+    // VIEW'S ATTRIBUTES
+
+    tagName:'label',
+
+    classNames:['btn','btn-default'],
+
+    classNameBindings:['isActive:active','warning','lockedByParent:disabled','notExist:not-exist'],
+
+    action:'toggleLabel',
+
+    click:function () {
+      if (this.get('parentView.isOperator')) {
+        this.triggerAction({
+          actionContext: [this.get('labelName'),this.get('queue')]
+        });
+        Em.run.next(function() {
+          this.notifyPropertyChange('currentLabel');
+        }.bind(this));
+      }
+    },
+
+    // COMPUTED PROPERTIES
+
+    /**
+     * Returns true if parent queue has no access to label with 'labelName'.
+     * @return {Boolean}
+     */
+    lockedByParent:function () {
+      var parent = this.get('controller.parentQueue');
+      return (Em.isEmpty(parent))?false:!parent.get('labels').findBy('name',this.get('labelName'));
+    }.property('controller.parentQueue'),
+
+    /**
+     * Returns true if current queue has access to label with 'labelName'.
+     * @return {Boolean}
+     */
+    isActive:function () {
+      return this.get('queue.labels').mapBy('name').contains(this.get('labelName'));
+    }.property('queue.labels.[]'),
+
+    /**
+     * Label record of type App.Label related to current queue.
+     * @return {App.Label}
+     */
+    currentLabel:function () {
+      return this.get('queue.labels').findBy('name',this.get('labelName'));
+    }.property('labelName','queue.labels.length'),
+
+    /**
+     * Label records from leaf queues, that has label with 'labelName'.
+     * @return {Array}
+     */
+    labels:function () {
+      return this.get('leaf').map(function (item) {
+        return item.get('labels').findBy('name',this.get('labelName'));
+      }.bind(this)).compact();
+    }.property('leaf.@each.labels.[]'),
+
+    /**
+     * Sum of label records from leaf queues, that has label with 'labelName'.
+     * @return {Number}
+     */
+    capacityValue:function () {
+      return this.get('labels').reduce(function (prev, label) {
+        return prev + ((label && label.get('capacity'))?+label.get('capacity'):0);
+      }, 0);
+    }.property('labels.@each.capacity','labels.[]'),
+
+    /**
+     * Returns true if total capacity of node labels in leaf are greater than 100.
+     * @type {Boolean}
+     */
+    warning:Em.computed.gt('capacityValue',100),
+
+    // OBSERVABLES
+
+    /**
+     * Marks labels with 'overCapacity' mark if they can't have such capacity value.
+     * @method capacityWatcher
+     */
+    capacityWatcher:function () {
+      this.get('labels').setEach('overCapacity',this.get('warning'));
+    }.observes('warning').on('didInsertElement'),
+
+    /**
+     * Initializes tooltip on button when inserted to DOM.
+     * @method capacityTooltip
+     */
+    capacityTooltip:function () {
+      this.$().tooltip({
+        title: function  () {
+          return this.get('capacityValue') + '';
+        }.bind(this),
+        container:"#"+this.elementId,
+        animation:false
+      });
+    }.on('didInsertElement'),
+
+    /**
+     * Marks buttons with 'first' and 'last' classes when inserted to DOM.
+     * @method setFirstAndLast
+     */
+    setFirstAndLast:function (item) {
+      var items = this.$().parents('.labels-toggle').find('label');
+      items.first().addClass('first');
+      items.last().addClass('last');
+    }.on('didInsertElement'),
+
+    /**
+     * Timer information with callback that hides tooltip.
+     * @type {Array}
+     */
+    isShownTimer:null,
+
+    /**
+     * Shows tooltip when label's capacity value changes.
+     * @method showCapacityTooltip
+     */
+    showCapacityTooltip: function() {
+      Em.run.next(this,function () {
+        if (Em.isNone(this.$())) return;
+
+        this.$().tooltip({
+          title: function  () {
+            return this.get('capacityValue') + '';
+          }.bind(this),
+          container:"#"+this.elementId,
+          animation:false
+        }).tooltip('show');
+
+        Em.run.cancel(this.get('isShownTimer'));
+        this.set('isShownTimer',Em.run.debounce(this,function() {
+          if (this.$()) this.$().tooltip('hide');
+        },500));
+      })
+    }.observes('currentLabel.capacity'),
+
+    /**
+     * Destrioy tooltips when element destroys.
+     * @method willClearRender
+     */
+    willClearRender:function() {
+      this.$().tooltip('destroy');
+    },
+
+    // CHILD VIEWS
+
+    /**
+     * Bar that shows total capcity value on node label.
+     * @type {Em.View}
+     */
+    nodeLabelsBar:Em.View.extend({
+
+      classNameBindings:[':progress-bar'],
+
+      attributeBindings:['capacityWidth:style'],
+
+      /**
+       * Formatting pattern.
+       * @type {String}
+       */
+      pattern:'width: %@%;',
+
+      /**
+       * Alias for parentView.capacityValue.
+       * @type {String}
+       */
+      value:Em.computed.alias('parentView.capacityValue'),
+
+      /**
+       * Formats pattern whit value.
+       * @return {String}
+       */
+      capacityWidth: function(c,o) {
+        return  this.get('pattern').fmt((+this.get('value')<=100)?this.get('value'):100);
+      }.property('value')
+    }),
+
+    /**
+     * Bar that shows current label capcity on node label.
+     * @type {Em.View}
+     */
+    currentQueueBar:Em.View.extend({
+
+      classNameBindings:[':progress-bar',':ghost'],
+
+      attributeBindings:['capacityWidth:style'],
+
+      /**
+       * Formatting pattern.
+       * @type {String}
+       */
+      pattern:'width: %@%;',
+
+      /**
+       * Alias for parentView.currentLabel.capacity.
+       * @type {String}
+       */
+      value:Em.computed.alias('parentView.currentLabel.capacity'),
+
+      capacityWidth: function(c,o) {
+        return  this.get('pattern').fmt((+this.get('value')<=100)?this.get('value'):0);
+      }.property('parentView.currentLabel.capacity','parentView.queue.labels.length')
+    })
   })
 });
 
 App.CapacityEditFormView = Em.View.extend({
-  mark:function () {
-    this.addObserver('controller.target.isEdit',this,'slide');
-    if (!this.get('controller.target.isEdit')) {
-      this.$('.capacity-edit-form').hide();
-    }
-  }.on('didInsertElement'),
-  slide:function () {
-    this.$('.capacity-edit-form').slideToggle(100);
-  }
+  dirty_capacity:function () {
+    return this.get('controller.content').changedAttributes().hasOwnProperty('capacity');
+  }.property('controller.content.capacity'),
+  dirty_maxcapacity:function () {
+    return this.get('controller.content').changedAttributes().hasOwnProperty('maximum_capacity');
+  }.property('controller.content.maximum_capacity')
 });

http://git-wip-us.apache.org/repos/asf/ambari/blob/95103a90/contrib/views/capacity-scheduler/src/main/resources/ui/app/controllers/queue.js
----------------------------------------------------------------------
diff --git a/contrib/views/capacity-scheduler/src/main/resources/ui/app/controllers/queue.js b/contrib/views/capacity-scheduler/src/main/resources/ui/app/controllers/queue.js
index 1bffbea..41a4dfd 100644
--- a/contrib/views/capacity-scheduler/src/main/resources/ui/app/controllers/queue.js
+++ b/contrib/views/capacity-scheduler/src/main/resources/ui/app/controllers/queue.js
@@ -23,129 +23,265 @@ var _stopState = 'STOPPED';
 
 App.QueueController = Ember.ObjectController.extend({
   needs:['queues'],
-  isOperator:Em.computed.alias('controllers.queues.isOperator'),
-  isNotOperator:Em.computed.alias('controllers.queues.isNotOperator'),
   actions:{
     setState:function (state) {
-      this.content.set('state', (state === "running") ? _runState : _stopState );
+      this.set('content.state', (state === "running") ? (this.get('content.state') == null) ? null : _runState : _stopState );
     },
-    createQ:function (record) {
+    createQ:function () {
       this.get('controllers.queues').send('createQ',this.get('content'));
     },
     addQ:function (path) {
       this.get('controllers.queues').send('addQ',path);
     },
     delQ:function (record) {
-      this.get('controllers.queues').send('delQ',this.get('content'));
+      this.get('controllers.queues').send('delQ',record);
     },
     renameQ:function (opt) {
       if (opt == 'ask') {
-        this.content.addObserver('name',this,this.setQueuePath);
+        this.set('tmpName',{name:this.get('content.name'),path:this.get('content.path')});
+        this.get('content').addObserver('name',this,this.setQueuePath);
         this.toggleProperty('isRenaming');
         return;
       }
       if (opt == 'cancel') {
-        this.send('rollbackProp','name');
-        this.send('rollbackProp','id');
-        this.send('rollbackProp','path');
-        this.content.removeObserver('name',this,this.setQueuePath);
+        this.get('content').removeObserver('name',this,this.setQueuePath);
+        this.get('content').setProperties({
+          name:this.get('tmpName.name'),
+          id:this.get('tmpName.path'),
+          path:this.get('tmpName.path')
+        });
         this.toggleProperty('isRenaming');
         return;
       }
-      if (opt) {
-        var self = this;
-        this.store.filter('queue',function (q) {
-          return q.id === self.content.id;
-        }).then(function(queues){
-          if (queues.get('length') > 1) {
-            return self.content.get('errors').add('path', 'Queue already exists');
-          }
-          self.toggleProperty('isRenaming');
-          self.content.removeObserver('name',self,self.setQueuePath);
-          self.transitionToRoute('queue',self.content.id);
-        })
+      if (opt && !this.get('content').get('errors.path')) {
+
+        this.store.filter('label',function (label){
+          return label.get('forQueue') == this.get('tmpName.path');
+        }.bind(this)).then(function (labels) {
+          labels.forEach(function (label) {
+            label.materializeId([this.get('id'),label.get('name')].join('.'));
+            label.store.updateId(label,label);
+          }.bind(this))
+        }.bind(this));
+        this.toggleProperty('isRenaming');
+        this.get('content').removeObserver('name',this,this.setQueuePath);
+        this.store.updateId(this.get('content'),this.get('content'));
+        this.transitionToRoute('queue',this.get('content.id'));
+
       }
 
     },
-    rollbackProp:function(prop){
-      attributes = this.content.changedAttributes();
+    // TODO bubble to route
+    rollbackProp:function(prop, queue){
+      queue = queue || this.get('content');
+      attributes = queue.changedAttributes();
       if (attributes.hasOwnProperty(prop)) {
-        this.content.set(prop,attributes[prop][0]);
+        queue.set(prop,attributes[prop][0]);
       }
     }
   },
-  setQueuePath:function (queue,o) {
-    var name = queue.get(o);
-    var parentPath = queue.get('parentPath');
-
-    queue.setProperties({
-      name:name.replace(/\s|\./g, ''),
-      path:parentPath+'.'+name,
-      id:(parentPath+'.'+name).dasherize()
-    });
 
-    if (name == '') {
-      queue.get('errors').add('path', 'This field is required');
-    }
-  },
+  /**
+   * Collection of modified fields in queue.
+   * @type {Object} - { [fileldName] : {Boolean} }
+   */
+  queueDirtyFilelds:{},
 
+  /**
+   * Represents renaming status.
+   * @type {Boolean}
+   */
   isRenaming:false,
 
-  unsetRenaming:function () {
-    this.set('isRenaming',false);
-  }.observes('content'),
+  /**
+   * Object contains temporary name and path while renaming the queue.
+   * @type {Object} - { name : {String}, path : {String} }
+   */
+  tmpName:{},
+
+
+
+  // COMPUTED PROPERTIES
 
-  isRoot:Ember.computed.equal('content.id', 'root'),
-  isRunning:Ember.computed.equal('content.state', _runState),
+  /**
+   * Alias for user admin status.
+   * @type {Boolean}
+   */
+  isOperator:Em.computed.alias('controllers.queues.isOperator'),
+
+  /**
+   * Inverted user admin status.
+   * @type {Boolean}
+   */
+  isNotOperator:Em.computed.alias('controllers.queues.isNotOperator'),
+
+  /**
+   * All queues in store.
+   * @type {DS.RecordArray}
+   */
   allQueues:Em.computed.alias('controllers.queues.content'),
-  allQueuesArranged:Em.computed.alias('controllers.queues.arrangedContent'),
 
-  handleAcl:function (key,value) {
-    if (value) {
-      this.set(key,(value == '*')?'*':' ');
-    }
-    return (this.get(key) == '*')? '*':'custom';
-  },
+  /**
+   * Array of leaf queues.
+   * @return {Array}
+   */
+  leafQueues:function () {
+    return this.get('allQueues').filterBy('parentPath',this.get('content.parentPath'));
+  }.property('allQueues.length','content.parentPath'),
+
+  /**
+   * Parent of current queue.
+   * @return {App.Queue}
+   */
+  parentQueue: function () {
+    return this.store.getById('queue',this.get('content.parentPath'));
+  }.property('content.parentPath'),
 
-  acl_administer_queue: function (key, value, previousValue) {
+  /**
+   * Returns true if queue is root.
+   * @type {Boolean}
+   */
+  isRoot:Ember.computed.match('content.id', /^(root|root.default)$/),
+
+  /**
+   * Represents queue run state. Returns true if state is null.
+   * @return {Boolean}
+   */
+  isRunning: function() {
+    return this.get('content.state') == _runState || this.get('content.state') == null;
+  }.property('content.state'),
+
+  /**
+   * Queue's acl_administer_queue property can be set to '*' (everyone) or ' ' (nobody) thru this property.
+   *
+   * @param  {String} key
+   * @param  {String} value
+   * @return {String} - '*' if equal to '*' or 'custom' in other case.
+   */
+  acl_administer_queue: function (key, value) {
     return this.handleAcl('content.acl_administer_queue',value);
   }.property('content.acl_administer_queue'),
+
+  /**
+   * Returns true if acl_administer_queue is set to '*'
+   * @type {Boolean}
+   */
   aaq_anyone:Ember.computed.equal('acl_administer_queue', '*'),
-  aaq_dirty:function () {
-    var attributes = this.content.changedAttributes();
-    return attributes.hasOwnProperty('acl_administer_queue');
-  }.property('content.acl_administer_queue'),
 
-  acl_submit_applications: function (key, value, previousValue) {
+  /**
+   * Queue's acl_submit_applications property can be set to '*' (everyone) or ' ' (nobody) thru this property.
+   *
+   * @param  {String} key
+   * @param  {String} value
+   * @return {String} - '*' if equal to '*' or 'custom' in other case.
+   */
+  acl_submit_applications: function (key, value) {
     return this.handleAcl('content.acl_submit_applications',value);
   }.property('content.acl_submit_applications'),
+
+  /**
+   * Returns true if acl_submit_applications is set to '*'
+   * @type {Boolean}
+   */
   asa_anyone:Ember.computed.equal('acl_submit_applications', '*'),
-  asa_dirty:function () {
-    var attributes = this.content.changedAttributes();
-    return attributes.hasOwnProperty('acl_submit_applications');
-  }.property('content.acl_submit_applications'),
 
+  /**
+   * Error messages for queue path.
+   * @type {[type]}
+   */
+  pathErrors:Ember.computed.mapBy('content.errors.path','message'),
+
+
+
+  // OBSERVABLES
+
+  /**
+   * Marks each queue in leaf with 'overCapacity' if sum if their capacity values is greater then 100.
+   * @method capacityControl
+   */
   capacityControl:function () {
-    var leafQueues = this.get('leafQueues');
-    var total = 0;
+    var leafQueues = this.get('leafQueues'),
+        total = leafQueues.reduce(function (prev, queue) {
+          return +queue.get('capacity') + prev;
+        },0);
 
-    leafQueues.forEach(function (queue) {
-      total+=Number(queue.get('capacity'));
-    });
     leafQueues.setEach('overCapacity',total>100);
   }.observes('content.capacity','leafQueues.@each.capacity'),
-  leafQueues:function () {
-    return this.get('allQueues').filterBy('parentPath',this.get('content.parentPath'));
-  }.property('allQueues.length','content.parentPath'),
 
-  queueNamesControl:function (c,o) {
-    var leaf = c.get('leafQueues');
-    var parent = c.get('allQueues').filterBy('path',c.get('content.parentPath')).get('firstObject');
-    if (parent) parent.set('queueNames',leaf.mapBy('name').join() || null);
+  /**
+   * Keeps track of leaf queues and sets 'queues' value of parent to list of their names.
+   * @method queueNamesControl
+   */
+  queueNamesControl:function () {
+    if (this.get('parentQueue')) this.set('parentQueue.queuesArray',this.get('leafQueues').mapBy('name'));
   }.observes('allQueues.length','allQueues.@each.name','content'),
 
-  pathErrors:Ember.computed.mapBy('content.errors.path','message')
-  
+  /**
+   * Adds observers for each queue attribute.
+   * @method dirtyObserver
+   */
+  dirtyObserver:function () {
+    this.get('content.constructor.transformedAttributes.keys.list').forEach(function(item) {
+      this.addObserver('content.' + item,this,'propertyBecomeDirty');
+    }.bind(this));
+  }.observes('content'),
+
+
+
+  // METHODS
+
+  /**
+   * Sets ACL value to '*' or ' ' and returns '*' and 'custom' respectively.
+   * @param  {String} key   - ACL attribute
+   * @param  {String} value - ACL value
+   * @return {String}
+   */
+  handleAcl:function (key,value) {
+    if (value) {
+      this.set(key,(value == '*')?'*':' ');
+    }
+    return (this.get(key) == '*' || this.get(key) == null) ? '*' : 'custom';
+  },
+
+  /**
+   * Sets queue path and id in accordance with the name.
+   * Also adds errors to queue if name is incorrect.
+   *
+   * @method setQueuePath
+   */
+  setQueuePath:function (queue) {
+    var name = queue.get('name').replace(/\s|\./g, ''),
+        parentPath = queue.get('parentPath');
+
+    queue.setProperties({
+      name:name,
+      path:parentPath+'.'+name,
+      id:(parentPath+'.'+name).dasherize()
+    });
+
+    if (name == '') {
+      queue.get('errors').add('path', 'This field is required');
+    }
+
+    this.store.filter('queue',function (q) {
+      return q.get('id') === queue.get('id');
+    }.bind(this)).then(function (queues){
+      if (queues.get('length') > 1) {
+        return this.get('content').get('errors').add('path', 'Queue already exists');
+      }
+    }.bind(this));
+  },
+
+  /**
+   * Adds modified queue fileds to q queueDirtyFilelds collection.
+   * @param  {String} controller
+   * @param  {String} property
+   * @method propertyBecomeDirty
+   */
+  propertyBecomeDirty:function (controller, property) {
+    var queueProp = property.split('.').objectAt(1);
+    this.set('queueDirtyFilelds.' + queueProp, this.get('content').changedAttributes().hasOwnProperty(queueProp));
+  }
 });
 
 App.ErrorController = Ember.ObjectController.extend();

http://git-wip-us.apache.org/repos/asf/ambari/blob/95103a90/contrib/views/capacity-scheduler/src/main/resources/ui/app/controllers/queues.js
----------------------------------------------------------------------
diff --git a/contrib/views/capacity-scheduler/src/main/resources/ui/app/controllers/queues.js b/contrib/views/capacity-scheduler/src/main/resources/ui/app/controllers/queues.js
index b03e633..aecd265 100644
--- a/contrib/views/capacity-scheduler/src/main/resources/ui/app/controllers/queues.js
+++ b/contrib/views/capacity-scheduler/src/main/resources/ui/app/controllers/queues.js
@@ -24,8 +24,13 @@ App.QueuesController = Ember.ArrayController.extend({
   sortProperties: ['name'],
   sortAscending: true,
   actions:{
+    loadTagged:function (tag) {
+      this.transitionToRoute('queue','root').then(function() {
+         this.store.fetchTagged(App.Queue,tag);
+       }.bind(this));
+    },
     goToQueue:function (queue) {
-      this.transitionToRoute('queue',queue)
+      this.transitionToRoute('queue',queue);
     },
     askPath:function () {
       this.set('isWaitingPath',true);
@@ -53,6 +58,9 @@ App.QueuesController = Ember.ArrayController.extend({
       record.save().then(Em.run.bind(this,this.set,'newQueue',null));
     },
     delQ:function (record) {
+      var queues = this.get('content'),
+          parentPath = record.get('parentPath'),
+          name = record.get('name');
       if (record.get('isNew')) {
         this.set('newQueue',null);
       }
@@ -60,9 +68,15 @@ App.QueuesController = Ember.ArrayController.extend({
         this.set('hasDeletedQueues',true);
       }
       if (record.isCurrent) {
-        this.transitionToRoute('queue',record.get('parentPath'));
+        this.transitionToRoute('queue',parentPath.toLowerCase())
+          .then(Em.run.schedule('afterRender', function () {
+            record.destroyRecord().then(function() {
+              queues.findBy('path',parentPath).set('queuesArray',{'exclude':name});
+            });
+          }));
+      } else {
+        record.destroyRecord();
       }
-      this.store.deleteRecord(record);
     },
     saveConfig:function (mark) {
       if (mark == 'restart') {
@@ -70,11 +84,15 @@ App.QueuesController = Ember.ArrayController.extend({
       } else if (mark == 'refresh') {
         this.get('store').markForRefresh();
       }
+      var collectedLabels = this.get('model').reduce(function (prev,q) {
+        return prev.pushObjects(q.get('labels.content'));
+      },[]);
 
       var hadDeletedQueues = this.get('hasDeletedQueues'),
           scheduler = this.get('scheduler').save(),
           model = this.get('model').save(),
-          all = Em.RSVP.Promise.all([model,scheduler]);
+          labels = DS.ManyArray.create({content:collectedLabels}).save(),
+          all = Em.RSVP.Promise.all([labels,model,scheduler]);
 
       all.catch(Em.run.bind(this,this.saveError,hadDeletedQueues));
 
@@ -85,18 +103,147 @@ App.QueuesController = Ember.ArrayController.extend({
     }
   },
 
+  /**
+   * User admin status.
+   * @type {Boolean}
+   */
+  isOperator:false,
+
+  /**
+   * Inverted isOperator value.
+   * @type {Boolean}
+   */
+  isNotOperator:cmp.not('isOperator'),
+
+  /**
+   * Flag to show input for adding queue.
+   * @type {Boolean}
+   */
+  isWaitingPath:false,
+
+  /**
+   * Property for error message which may appear when saving queue.
+   * @type {Object}
+   */
   alertMessage:null,
+
+  /**
+   * Temporary filed for new queue
+   * @type {App.Queue}
+   */
+  newQueue:null,
+
+  /**
+   * True if newQueue is not empty.
+   * @type {Boolean}
+   */
+  hasNewQueue: cmp.bool('newQueue'),
+
+  /**
+   * Current configuration version tag.
+   * @type {[type]}
+   */
+  current_tag: cmp.alias('store.current_tag'),
+
+  /**
+   * Scheduler record
+   * @type {App.Scheduler}
+   */
+  scheduler:null,
+
+  /**
+   * Collection of modified fields in Scheduler.
+   * @type {Object} - { [fileldName] : {Boolean} }
+   */
+  schedulerDirtyFilelds:{},
+
+
+  configNote: cmp.alias('store.configNote'),
+
+  /*configNote:function (arg,val) {
+    if (arguments.length > 1) {
+      this.set('store.configNote',val);
+    }
+    return this.get('store.configNote');
+  }.property('store.configNote'),*/
+
+  tags:function () {
+    return this.store.find('tag');
+  }.property('store.current_tag'),
+
+  sortedTags: cmp.sort('tags', function(a, b){
+    return (+a.id > +b.id)?(+a.id < +b.id)?0:-1:1;
+  }),
+
+
   saveError:function (hadDeletedQueues,error) {
     this.set('hasDeletedQueues',hadDeletedQueues);
     var response = JSON.parse(error.responseText);
     this.set('alertMessage',response);
   },
 
-  isOperator:false,
-  isNotOperator:cmp.not('isOperator'),
+  propertyBecomeDirty:function (controller,property) {
+    var schedProp = property.split('.').objectAt(1);
+    this.set('schedulerDirtyFilelds.' + schedProp, this.get('scheduler').changedAttributes().hasOwnProperty(schedProp));
+  },
+
+  dirtyObserver:function () {
+    this.get('scheduler.constructor.transformedAttributes.keys.list').forEach(function(item) {
+      this.addObserver('scheduler.' + item,this,'propertyBecomeDirty');
+    }.bind(this));
+  }.observes('scheduler'),
+
+
+  trackNewQueue:function () {
+    var newQueue = this.get('newQueue');
+    if (Em.isEmpty(newQueue)) {
+      return;
+    }
+    var name = newQueue.get('name');
+    var parentPath = newQueue.get('parentPath');
+
+    this.get('newQueue').setProperties({
+      name:name.replace(/\s/g, ''),
+      path:parentPath+'.'+name,
+      id:(parentPath+'.'+name).dasherize()
+    });
+
+  }.observes('newQueue.name'),
+
+
+
+  // TRACKING OF RESTART REQUIREMENT
+
+  /**
+   * check if RM needs restart
+   * @type {bool}
+   */
+  needRestart: cmp.any('hasDeletedQueues', 'hasRenamedQueues'),
+
+  /**
+   * True if some queue of desired configs was removed.
+   * @type {Boolean}
+   */
+  hasDeletedQueues:false,
+
+  /**
+   * List of queues with modified name.
+   * @type {Array}
+   */
+  renamedQueues:cmp.filter('content.@each.name',function (queue){
+    return queue.changedAttributes().hasOwnProperty('name') && !queue.get('isNewQueue');
+  }),
+
+  /**
+   * True if renamedQueues is not empty.
+   * @type {Boolean}
+   */
+  hasRenamedQueues: cmp.notEmpty('renamedQueues.[]'),
+
+
+
+  // TRACKING OF REFRESH REQUIREMENT
 
-  isWaitingPath:false,
-  
   /**
    * check if RM needs refresh
    * @type {bool}
@@ -104,34 +251,59 @@ App.QueuesController = Ember.ArrayController.extend({
   needRefresh: cmp.and('needRefreshProps','noNeedRestart'),
 
   /**
-   * props for 'needRefresh'
+   * Inverted needRestart value.
+   * @type {Boolean}
+   */
+  noNeedRestart: cmp.not('needRestart'),
+
+  /**
+   * Check properties for refresh requirement
+   * @type {Boolean}
+   */
+  needRefreshProps: cmp.any('hasChanges', 'hasNewQueues','dirtyScheduler'),
+
+  /**
+   * List of modified queues.
+   * @type {Array}
+   */
+  dirtyQueues:function () {
+    return this.get('content').filter(function (q) {
+      return q.get('isAnyDirty');
+    });
+  }.property('content.@each.isAnyDirty'),
+
+  /**
+   * True if dirtyQueues is not empty.
+   * @type {Boolean}
    */
-  dirtyQueues: cmp.filterBy('content', 'isDirty', true),
-  dirtyScheduler: cmp.bool('scheduler.isDirty'),
-  newQueues: cmp.filterBy('content', 'isNewQueue', true),
   hasChanges: cmp.notEmpty('dirtyQueues.[]'),
+
+  /**
+   * List of new queues.
+   * @type {Array}
+   */
+  newQueues: cmp.filterBy('content', 'isNewQueue', true),
+
+  /**
+   * True if newQueues is not empty.
+   * @type {Boolean}
+   */
   hasNewQueues: cmp.notEmpty('newQueues.[]'),
 
-  needRefreshProps: cmp.any('hasChanges', 'hasNewQueues','dirtyScheduler'),
-  noNeedRestart: cmp.not('needRestart'),
-  
   /**
-   * check if RM needs restart 
-   * @type {bool}
+   * True if scheduler is modified.
+   * @type {[type]}
    */
-  needRestart: cmp.any('hasDeletedQueues', 'hasRenamedQueues'),
-  
+  dirtyScheduler: cmp.bool('scheduler.isDirty'),
+
+
+   // TRACKING OF PRESERVATION POSSIBILITY
+
   /**
-   * props for 'needRestart'
+   * check there is some changes for save
+   * @type {bool}
    */
-  hasDeletedQueues:false,
-  hasRenamedQueues: cmp.notEmpty('renamedQueues.[]'),
-  renamedQueues:function () {
-    return this.content.filter(function(queue){
-      var attr = queue.changedAttributes();
-      return attr.hasOwnProperty('name') && !queue.get('isNewQueue');
-    });
-  }.property('content.@each.name'),
+  needSave: cmp.any('needRestart', 'needRefresh'),
 
   /**
    * check if can save configs
@@ -140,43 +312,42 @@ App.QueuesController = Ember.ArrayController.extend({
   canNotSave: cmp.any('hasOverCapacity', 'hasUncompetedAddings','hasNotValid','isNotOperator'),
 
   /**
-   * props for canNotSave
+   * List of not valid queues.
+   * @type {Array}
    */
   notValid:cmp.filterBy('content','isValid',false),
-  overCapacityQ:cmp.filterBy('content','overCapacity',true),
-  uncompetedAddings:cmp.filterBy('content', 'isNew', true),
-  hasNotValid:cmp.notEmpty('notValid.[]'),
-  hasOverCapacity:cmp.notEmpty('overCapacityQ.[]'),
-  hasUncompetedAddings:cmp.notEmpty('uncompetedAddings.[]'),
 
   /**
-   * check there is some changes for save
-   * @type {bool}
+   * True if notValid is not empty.
+   * @type {Boolean}
    */
-  needSave: cmp.any('needRestart', 'needRefresh'),
-
-  newQueue:null,
-  hasNewQueue: cmp.bool('newQueue'),
-  trackNewQueue:function () {
-    var newQueue = this.get('newQueue');
-    if (Em.isEmpty(newQueue)){
-      return;
-    }
-    var name = newQueue.get('name');
-    var parentPath = newQueue.get('parentPath');
+  hasNotValid:cmp.notEmpty('notValid.[]'),
 
-    this.get('newQueue').setProperties({
-      name:name.replace(/\s/g, ''),
-      path:parentPath+'.'+name,
-      id:(parentPath+'.'+name).dasherize()
+  /**
+   * List of queues with excess of capacity
+   * @type {Array}
+   */
+  overCapacityQ:function () {
+    return this.get('content').filter(function (q) {
+      return q.get('overCapacity');
     });
+  }.property('content.@each.overCapacity'),
 
-  }.observes('newQueue.name'),
+  /**
+   * True if overCapacityQ is not empty.
+   * @type {Boolean}
+   */
+  hasOverCapacity:cmp.notEmpty('overCapacityQ.[]'),
 
-  configNote:function (arg,val) {
-    if (arguments.length > 1) {
-      this.set('store.configNote',val);
-    }
-    return this.get('store.configNote');
-  }.property('store.configNote')
+  /**
+   * List of queues with incompete adding process
+   * @type {[type]}
+   */
+  uncompetedAddings:cmp.filterBy('content', 'isNew', true),
+
+  /**
+   * True if uncompetedAddings is not empty.
+   * @type {Boolean}
+   */
+  hasUncompetedAddings:cmp.notEmpty('uncompetedAddings.[]')
 });

http://git-wip-us.apache.org/repos/asf/ambari/blob/95103a90/contrib/views/capacity-scheduler/src/main/resources/ui/app/initialize.js
----------------------------------------------------------------------
diff --git a/contrib/views/capacity-scheduler/src/main/resources/ui/app/initialize.js b/contrib/views/capacity-scheduler/src/main/resources/ui/app/initialize.js
index dda4c83..57f57a0 100644
--- a/contrib/views/capacity-scheduler/src/main/resources/ui/app/initialize.js
+++ b/contrib/views/capacity-scheduler/src/main/resources/ui/app/initialize.js
@@ -24,6 +24,12 @@ App.testMode = false;
 // adapters
 require('adapters');
 
+//serializers
+require('serializers');
+
+//store
+require('store');
+
 //components
 require('components');
 

http://git-wip-us.apache.org/repos/asf/ambari/blob/95103a90/contrib/views/capacity-scheduler/src/main/resources/ui/app/models/queue.js
----------------------------------------------------------------------
diff --git a/contrib/views/capacity-scheduler/src/main/resources/ui/app/models/queue.js b/contrib/views/capacity-scheduler/src/main/resources/ui/app/models/queue.js
index cf22437..2044ff1 100644
--- a/contrib/views/capacity-scheduler/src/main/resources/ui/app/models/queue.js
+++ b/contrib/views/capacity-scheduler/src/main/resources/ui/app/models/queue.js
@@ -18,11 +18,49 @@
 
 var App = require('app');
 
+App.Label = DS.Model.extend({
+  queue: DS.belongsTo('queue'),
+  capacity: DS.attr('number', { defaultValue: 0 }),
+  maximum_capacity: DS.attr('number', { defaultValue: 0 }),
+  forQueue:function() {
+    return this.get('id').substring(0,this.get('id').lastIndexOf('.'));
+  }.property('id'),
+  name:function() {
+    return this.get('id').substr(this.get('id').lastIndexOf('.')+1);
+  }.property('id'),
+
+  overCapacity:false,
+  isNotExist:function () {
+    return this.get('store.nodeLabels.content').findBy('name',this.get('name')).notExist;
+  }.property('store.nodeLabels.content.@each.notExist')
+});
+
 App.Scheduler = DS.Model.extend({
   maximum_am_resource_percent: DS.attr('number', { defaultValue: 0 }),
   maximum_applications: DS.attr('number', { defaultValue: 0 }),
   node_locality_delay: DS.attr('number', { defaultValue: 0 }),
-  resource_calculator: DS.attr('string', { defaultValue: '' }),
+  resource_calculator: DS.attr('string', { defaultValue: '' })
+});
+
+
+/**
+ * Represents tagged of configuraion vresion.
+ *
+ */
+App.Tag = DS.Model.extend({
+  tag:DS.attr('string'),
+  isCurrent:function () {
+    return this.get('tag') === this.get('store.current_tag');
+  }.property('store.current_tag'),
+  changed:function () {
+    return (this.get('tag').match(/version[1-9]+/))?moment(+this.get('tag').replace('version','')).fromNow():'';
+  }.property('tag'),
+  updateTime:function () {
+    Em.run.later(this,function () {
+      this.trigger('tick');
+      this.notifyPropertyChange('tag');
+    },5000);
+  }.on('init','tick')
 });
 
 /**
@@ -30,6 +68,55 @@ App.Scheduler = DS.Model.extend({
  *
  */
 App.Queue = DS.Model.extend({
+  labels: DS.hasMany('label'),
+  sortBy:['name'],
+  sortedLabels:Em.computed.sort('labels','sortBy'),
+
+  _accessAllLabels:DS.attr('boolean'),
+  accessAllLabels:function (key,val) {
+    var labels = this.get('store.nodeLabels').map(function(label) {
+      return this.store.getById('label',[this.get('id'),label.name].join('.'));
+    }.bind(this));
+
+    if (arguments.length > 1) {
+      this.set('_accessAllLabels',val);
+
+      if (this.get('_accessAllLabels')) {
+          labels.forEach(function(lb) {
+
+            var containsByParent = (Em.isEmpty(this.get('parentPath')))?true:this.store.getById('queue',this.get('parentPath')).get('labels').findBy('name',lb.get('name'));
+            if (!this.get('labels').contains(lb) && !!containsByParent) {
+              this.get('labels').pushObject(lb);
+              this.notifyPropertyChange('labels');
+            }
+          }.bind(this));
+      }
+    }
+
+    if (this.get('labels.length') != labels.get('length')) {
+      this.set('_accessAllLabels',false);
+    }
+
+    return this.get('_accessAllLabels');
+  }.property('_accessAllLabels','labels.[]'),
+
+  isAnyDirty: function () {
+    return this.get('isDirty') || !Em.isEmpty(this.get('labels').findBy('isDirty',true)) || this.get('isLabelsDirty');
+  }.property('isDirty','labels.@each.isDirty','initialLabels','isLabelsDirty'),
+
+  initialLabels:[],
+  labelsLoad:function() {
+    this.set('initialLabels',this.get('labels').mapBy('id'));
+  }.on('didLoad','didUpdate','didCreate'),
+
+  isLabelsDirty:function () {
+    var il = this.get('initialLabels').sort();
+    var cl = this.get('labels').mapBy('id').sort();
+    return !((il.length == cl.length) && il.every(function(element, index) {
+      return element === cl[index];
+    }));
+  }.property('initialLabels', 'labels.[]', '_accessAllLabels'),
+
   name: DS.attr('string'),
   parentPath: DS.attr('string'),
   depth: DS.attr('number'),
@@ -42,20 +129,59 @@ App.Queue = DS.Model.extend({
 
   capacity: DS.attr('number', { defaultValue: 0 }),
   maximum_capacity: DS.attr('number', { defaultValue: 0 }),
-  unfunded_capacity: DS.attr('number', { defaultValue: 0 }),
-    
+  //unfunded_capacity: DS.attr('number', { defaultValue: 0 }),
+
   user_limit_factor: DS.attr('number', { defaultValue: 1 }),
   minimum_user_limit_percent: DS.attr('number', { defaultValue: 100 }),
-  maximum_applications: DS.attr('number', { defaultValue: '' }),
-  maximum_am_resource_percent: DS.attr('number', { defaultValue: '' }),
+  maximum_applications: DS.attr('number', { defaultValue: null }),
+  maximum_am_resource_percent: DS.attr('number', { defaultValue: null }),
 
-  queueNames: DS.attr('string'),
-  queueNamesArray:function () {
-    return (this.get('queueNames.length')>0)?this.get('queueNames').split(','):[];
-  }.property('queueNames'),
+  queues: DS.attr('string'),
+  queuesArray:function (key,val) {
+    var qrray;
+    if (arguments.length > 1) {
+      if (typeof val === 'object' && val.hasOwnProperty('exclude')) {
+        qrray = (this.get('queues'))?this.get('queues').split(','):[];
+        this.set('queues',qrray.removeObject(val.exclude).join(',') || null);
+      } else {
+        this.set('queues',val.join(',') || null);
+      }
+    }
+    return (this.get('queues'))?this.get('queues').split(','):[];
+  }.property('queues'),
 
-  overCapacity:false,
+  _overCapacity:false,
+  overCapacity:function(key,val) {
+    if (arguments.length > 1) {
+      this.set('_overCapacity',val);
+    }
+
+    return this.get('_overCapacity') || !Em.isEmpty(this.get('labels').filterBy('overCapacity'));
+  }.property('_overCapacity','labels.@each.overCapacity'),
 
   //new queue flag
-  isNewQueue:DS.attr('boolean', {defaultValue: false})
+  isNewQueue:DS.attr('boolean', {defaultValue: false}),
+
+  version:null,
+  clearTag:function () {
+    this.set('version', null);
+  }.observes(
+    'name',
+    'parentPath',
+    'depth',
+    'path',
+    'state',
+    'acl_administer_queue',
+    'acl_submit_applications',
+    'capacity',
+    'maximum_capacity',
+    'unfunded_capacity',
+    'user_limit_factor',
+    'minimum_user_limit_percent',
+    'maximum_applications',
+    'maximum_am_resource_percent',
+    'queues',
+    'labels.@each.capacity',
+    'labels.@each.maximum_capacity'
+  )
 });


Mime
View raw message