Bladeren bron

AMBARI-17602. Capacity Scheduler View - Fetching current RM configuration of queues and preemption implementation (Akhil PB via pallavkul)

Pallav Kulshreshtha 9 jaren geleden
bovenliggende
commit
6a89de639b
29 gewijzigde bestanden met toevoegingen van 877 en 144 verwijderingen
  1. 25 0
      contrib/views/capacity-scheduler/src/main/java/org/apache/ambari/view/capacityscheduler/ConfigurationService.java
  2. 6 0
      contrib/views/capacity-scheduler/src/main/resources/ui/app/adapters.js
  3. 75 0
      contrib/views/capacity-scheduler/src/main/resources/ui/app/components/capacityInput.js
  4. 6 2
      contrib/views/capacity-scheduler/src/main/resources/ui/app/components/displayNodeLabels.js
  5. 1 2
      contrib/views/capacity-scheduler/src/main/resources/ui/app/components/editLabelCapacity.js
  6. 21 6
      contrib/views/capacity-scheduler/src/main/resources/ui/app/components/queueSummary.js
  7. 10 1
      contrib/views/capacity-scheduler/src/main/resources/ui/app/controllers/capsched.js
  8. 198 27
      contrib/views/capacity-scheduler/src/main/resources/ui/app/controllers/editqueue.js
  9. 87 14
      contrib/views/capacity-scheduler/src/main/resources/ui/app/controllers/queuesconf.js
  10. 24 1
      contrib/views/capacity-scheduler/src/main/resources/ui/app/models/queue.js
  11. 15 6
      contrib/views/capacity-scheduler/src/main/resources/ui/app/router.js
  12. 8 1
      contrib/views/capacity-scheduler/src/main/resources/ui/app/serializers.js
  13. 76 0
      contrib/views/capacity-scheduler/src/main/resources/ui/app/store.js
  14. 55 14
      contrib/views/capacity-scheduler/src/main/resources/ui/app/styles/application.less
  15. 1 0
      contrib/views/capacity-scheduler/src/main/resources/ui/app/templates.js
  16. 11 2
      contrib/views/capacity-scheduler/src/main/resources/ui/app/templates/capsched.hbs
  17. 41 5
      contrib/views/capacity-scheduler/src/main/resources/ui/app/templates/capsched/partials/labelCapacity.hbs
  18. 75 0
      contrib/views/capacity-scheduler/src/main/resources/ui/app/templates/capsched/partials/preemption.hbs
  19. 13 5
      contrib/views/capacity-scheduler/src/main/resources/ui/app/templates/capsched/partials/queueCapacity.hbs
  20. 26 22
      contrib/views/capacity-scheduler/src/main/resources/ui/app/templates/capsched/partials/queueResources.hbs
  21. 8 5
      contrib/views/capacity-scheduler/src/main/resources/ui/app/templates/capsched/queuesconf.hbs
  22. 4 0
      contrib/views/capacity-scheduler/src/main/resources/ui/app/templates/capsched/queuesconf/editqueue.hbs
  23. 21 0
      contrib/views/capacity-scheduler/src/main/resources/ui/app/templates/capsched/trace.hbs
  24. 17 13
      contrib/views/capacity-scheduler/src/main/resources/ui/app/templates/components/editLabelCapacity.hbs
  25. 2 2
      contrib/views/capacity-scheduler/src/main/resources/ui/app/templates/components/editQueueCapacity.hbs
  26. 2 2
      contrib/views/capacity-scheduler/src/main/resources/ui/app/templates/components/labelCapacityBar.hbs
  27. 22 7
      contrib/views/capacity-scheduler/src/main/resources/ui/app/templates/components/queueMapping.hbs
  28. 17 5
      contrib/views/capacity-scheduler/src/main/resources/ui/app/templates/components/queueSummary.hbs
  29. 10 2
      contrib/views/capacity-scheduler/src/main/resources/ui/app/views/editqueue.js

+ 25 - 0
contrib/views/capacity-scheduler/src/main/java/org/apache/ambari/view/capacityscheduler/ConfigurationService.java

@@ -107,6 +107,7 @@ public class ConfigurationService {
   private static final String CONFIGURATION_URL_BY_TAG = "configurations?type=capacity-scheduler&tag=%s";
 
   private static final String RM_GET_NODE_LABEL_URL = "%s/ws/v1/cluster/get-node-labels";
+  private static final String RM_GET_SCHEDULER_CONFIG = "%s/ws/v1/cluster/scheduler";
 
   // ================================================================================
   // Privilege Reading
@@ -283,6 +284,30 @@ public class ConfigurationService {
     return response;
   }
 
+  /**
+   * Gets scheduler info from RM
+   *
+   * @return scheduler info
+   */
+  @GET
+  @Produces(MediaType.APPLICATION_JSON)
+  @Path("/rmCurrentConfig")
+  public Response getRmSchedulerConfig() {
+    try {
+      String url = String.format(RM_GET_SCHEDULER_CONFIG, getRMUrl());
+
+      InputStream rmResponse = context.getURLStreamProvider().readFrom(
+          url, "GET", (String) null, new HashMap<String, String>());
+      String result = IOUtils.toString(rmResponse);
+      return Response.ok(result).build();
+    } catch (ConnectException ex) {
+      throw new ServiceFormattedException("Connection to Resource Manager refused", ex);
+    } catch (WebApplicationException ex) {
+      throw ex;
+    } catch (Exception ex) {
+      throw new ServiceFormattedException(ex.getMessage(), ex);
+    }
+  }
 
   /**
    * Checks if the user is an operator.

+ 6 - 0
contrib/views/capacity-scheduler/src/main/resources/ui/app/adapters.js

@@ -255,6 +255,12 @@ App.QueueAdapter = DS.Adapter.extend({
       _ajax(uri,'GET').then(function(data) {
         var parsedData = JSON.parse(data), labels;
 
+        if (parsedData !== null) {
+          store.set('isNodeLabelsEnabledByRM', true);
+        } else {
+          store.set('isNodeLabelsEnabledByRM', false);
+        }
+
         if (stackVersion >= 2.5) {
           if (parsedData && Em.isArray(parsedData.nodeLabelInfo)) {
             labels = parsedData.nodeLabelInfo;

+ 75 - 0
contrib/views/capacity-scheduler/src/main/resources/ui/app/components/capacityInput.js

@@ -116,3 +116,78 @@ App.MaxCapacityInputComponent = App.CapacityInputComponent.extend({
 
   }.observes('queue.maximum_capacity','queue.capacity')
 });
+
+App.DecimalCapacityInputComponent = Ember.TextField.extend({
+  classNames: ['form-control'],
+  maxVal: null,
+  queue: null,
+
+  initVal: function() {
+    this.set('value', parseFloat(this.get('value')));
+  }.on('init'),
+
+  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 < 190) ||
+      (evt.keyCode > 190 && evt.keyCode < 193) ||
+      (evt.keyCode > 218 && evt.keyCode < 223)) {
+      return false;
+    }
+
+    if (evt.keyCode === 190) {
+      return true;
+    }
+
+    if (evt.keyCode > 95 && evt.keyCode < 106) {
+      newChar = (evt.keyCode - 96).toString();
+    } else {
+      newChar = String.fromCharCode(evt.keyCode);
+    }
+
+    if (newChar.match(/[0-9]/)) {
+      val = val.substring(0, evt.target.selectionStart) + newChar + val.substring(evt.target.selectionEnd);
+    }
+
+    //Restricting three decimal places, allow decimal precision=2
+    if (/^\d+\.\d{3}$/.test(val)) {
+      return false;
+    }
+
+    return parseFloat(val) <= 100;
+  },
+
+  valueDidChange: function() {
+    var val = this.get('value'),
+    maxVal = this.get('maxVal');
+    if (/^\d+(\.\d{1,2})?$/.test(val)) {
+      this.set('value', (parseFloat(val) > maxVal)? parseFloat(maxVal) : parseFloat(val));
+    }
+  }.observes('value').on('change')
+
+});
+
+App.DecimalMaxcapacityInputComponent = App.DecimalCapacityInputComponent.extend({
+  checkInvalid: function(c, o) {
+    var queue = this.get('queue'),
+    max_capacity = +queue.get('maximum_capacity'),
+    capacity = +queue.get('capacity');
+
+    if (queue.get('maximum_capacity') === null) {
+      return;
+    }
+
+    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 greater than or equal to capacity');
+    } else {
+      queue.get('errors').remove('maximum_capacity');
+    }
+
+  }.observes('queue.capacity', 'queue.maximum_capacity')
+});

+ 6 - 2
contrib/views/capacity-scheduler/src/main/resources/ui/app/components/displayNodeLabels.js

@@ -30,12 +30,16 @@ App.DisplayRootLabelComponent = Ember.Component.extend({
     enableRootLabel: function(label) {
       var store = this.get('label.store'),
       rootQ = store.getById('queue', 'root');
-      rootQ.addQueueNodeLabel(this.get('label'));
+      rootQ.recursiveAddChildQueueLabels(label);
+      var rootLabel = store.getById('label', ['root', label.get('name')].join('.'));
+      rootLabel.setCapacity(100);
     },
     disableRootLabel: function(label) {
       var store = this.get('label.store'),
       rootQ = store.getById('queue', 'root');
-      rootQ.recursiveRemoveChildQueueLabels(this.get('label'));
+      rootQ.recursiveRemoveChildQueueLabels(label);
+      var rootLabel = store.getById('label', ['root', label.get('name')].join('.'));
+      rootLabel.setCapacity(0);
     }
   },
 

+ 1 - 2
contrib/views/capacity-scheduler/src/main/resources/ui/app/components/editLabelCapacity.js

@@ -33,10 +33,9 @@ App.EditLabelCapacityComponent = Ember.Component.extend({
       }
     },
     enableAccess: function() {
-      this.get('queue').addQueueNodeLabel(this.get('label'));
+      this.get('queue').recursiveAddChildQueueLabels(this.get('label'));
     },
     disableAccess: function() {
-      this.get('label').set('capacity', 0);
       this.get('queue').recursiveRemoveChildQueueLabels(this.get('label'));
     }
   },

+ 21 - 6
contrib/views/capacity-scheduler/src/main/resources/ui/app/components/queueSummary.js

@@ -20,13 +20,23 @@
 
  var _runState = 'RUNNING';
  var _stopState = 'STOPPED';
-
- var _notStartedState = 'NOT STARTED';
+ var _notStartedState = 'NOT SAVED';
 
  App.QueueSummaryComponent = Ember.Component.extend({
    layoutName: 'components/queueSummary',
    queue: null,
    allQueues: null,
+   precision: 2,
+   queuesNeedRefresh: null,
+
+   isQueueNeedRefreshOrRestart: function() {
+     var qsNeedRefresh = this.get('queuesNeedRefresh'),
+     qq = this.get('queue');
+     if (qsNeedRefresh && qsNeedRefresh.findBy('path', qq.get('path'))) {
+       return true;
+     }
+     return false;
+   }.property('queuesNeedRefresh.[]', 'queue'),
 
    isRunningState: function() {
      return this.get('queue.state') === _runState || this.get('queue.state') === null;
@@ -44,7 +54,7 @@
 
    qStateColor: function() {
      if (this.get('queue.isNewQueue')) {
-       return 'text-info';
+       return 'text-danger';
      } else if (this.get('isRunningState')) {
        return 'text-success';
      } else {
@@ -56,6 +66,10 @@
      return this.get('queue').changedAttributes().hasOwnProperty('state');
    }.property('queue.state'),
 
+   isNewQueue: function() {
+     return this.get('queue.isNewQueue');
+   }.property('queue.isNewQueue'),
+
    effectiveCapacity: function() {
      var currentQ = this.get('queue'),
      allQueues = this.get('allQueues'),
@@ -64,8 +78,9 @@
        effectiveCapacityRatio *= (currentQ.get('capacity') / 100);
        currentQ = allQueues.findBy('id', currentQ.get('parentPath').toLowerCase()) || null;
      }
-     var effectiveCapacityPercent = effectiveCapacityRatio * 100;
-     this.get('queue').set('absolute_capacity', effectiveCapacityPercent || 0);
-     return effectiveCapacityPercent;
+     var effectiveCapacityPercent = effectiveCapacityRatio * 100,
+     absoluteCapacity = parseFloat(parseFloat(effectiveCapacityPercent).toFixed(this.get('precision')));
+     this.get('queue').set('absolute_capacity', absoluteCapacity || 0);
+     return absoluteCapacity;
    }.property('queue.capacity', 'allQueues.@each.capacity', 'allQueues.length')
  });

+ 10 - 1
contrib/views/capacity-scheduler/src/main/resources/ui/app/controllers/capsched.js

@@ -50,6 +50,15 @@ App.CapschedController = Ember.Controller.extend({
 
   sortedTags: Ember.computed.sort('tags', function(a, b){
     return (+a.id > +b.id)? (+a.id < +b.id)? 0 : -1 : 1;
-  })
+  }),
 
+  showSpinner: false,
+
+  startSpinner: function() {
+    this.set('showSpinner', true);
+  },
+
+  stopSpinner: function() {
+    this.set('showSpinner', false);
+  }
 });

+ 198 - 27
contrib/views/capacity-scheduler/src/main/resources/ui/app/controllers/editqueue.js

@@ -28,6 +28,8 @@ App.CapschedQueuesconfEditqueueController = Ember.Controller.extend({
   scheduler: Ember.computed.alias('controllers.capsched.content'),
   allQueues: Ember.computed.alias('controllers.capsched.queues'),
   isNodeLabelsEnabledByRM: Ember.computed.alias('store.isNodeLabelsEnabledByRM'),
+  isRmOffline: Ember.computed.alias('store.isRmOffline'),
+  precision: 2,
 
   isRangerEnabledForYarn: function() {
     var isRanger = this.get('controllers.capsched.isRangerEnabledForYarn');
@@ -77,7 +79,7 @@ App.CapschedQueuesconfEditqueueController = Ember.Controller.extend({
     originalQName = this.get('content.name'),
     qParentPath = this.get('content.parentPath'),
     qPath = [qParentPath, qName].join('.'),
-    qAlreadyExists = this.store.hasRecordForId('queue', qPath.toLowerCase());
+    qAlreadyExists = (this.get('allQueues').findBy('name', qName))? true : false;
     if (Ember.isBlank(qName)) {
       this.set('isInvalidQName', true);
       this.set('invalidQNameMessage', 'Enter queue name');
@@ -156,24 +158,41 @@ App.CapschedQueuesconfEditqueueController = Ember.Controller.extend({
 
    /**
     * Returns maximum applications for a queue if defined,
-    * else the inherited value (for all queues)
+    * else the inherited value
     */
    maximumApplications: function(key, val) {
-     if (arguments.length > 1) {
-       if (val !== this.get('scheduler.maximum_applications')) {
-         this.set('content.maximum_applications', val);
-       } else {
-         this.set('content.maximum_applications', null);
-       }
-     }
-     var schedulerMaxApps = this.get('scheduler.maximum_applications'),
-     absoluteCapacity = this.get('content.absolute_capacity');
+    if (arguments.length > 1) {
+      this.set('content.maximum_applications', val);
+    }
+    var queueMaxApps = this.get('content.maximum_applications');
+    if (queueMaxApps) {
+      return queueMaxApps;
+    } else {
+      return this.getInheritedMaximumApplications();
+    }
+   }.property('content.maximum_applications', 'scheduler.maximum_applications'),
+
+   isMaximumApplicationsInherited: function() {
      if (this.get('content.maximum_applications')) {
-       return this.get('content.maximum_applications');
+       return false;
      } else {
-       return Math.round(schedulerMaxApps * (absoluteCapacity / 100));
+       return true;
      }
-   }.property('content.maximum_applications', 'content.absolute_capacity', 'scheduler.maximum_applications'),
+   }.property('content.maximum_applications'),
+
+   /**
+    * Returns inherited maximum applications for a queue
+    */
+   getInheritedMaximumApplications: function() {
+     var parentQ = this.store.getById('queue', this.get('content.parentPath').toLowerCase());
+     while (parentQ !== null) {
+       if (parentQ.get('maximum_applications')) {
+         return parentQ.get('maximum_applications');
+       }
+       parentQ = this.store.getById('queue', parentQ.get('parentPath').toLowerCase());
+     }
+     return this.get('scheduler.maximum_applications');
+   },
 
    /**
     * Returns maximum AM resource percent for a queue if defined,
@@ -181,20 +200,37 @@ App.CapschedQueuesconfEditqueueController = Ember.Controller.extend({
     */
    maximumAMResourcePercent: function(key, val) {
      if (arguments.length > 1) {
-       if (val !== this.get('scheduler.maximum_am_resource_percent')) {
-         this.set('content.maximum_am_resource_percent', val);
-       } else {
-         this.set('content.maximum_am_resource_percent', null);
-       }
+       this.set('content.maximum_am_resource_percent', val);
      }
-     var schedulerResoucePercent = this.get('scheduler.maximum_am_resource_percent'),
-     absoluteCapacity = this.get('content.absolute_capacity');
+     var qMaxAmPercent = this.get('content.maximum_am_resource_percent');
+     if (qMaxAmPercent) {
+       return qMaxAmPercent;
+     } else {
+       return this.getInheritedMaxAmResourcePercent();
+     }
+   }.property('content.maximum_am_resource_percent', 'scheduler.maximum_am_resource_percent'),
+
+   isMaxAmResourcePercentInherited: function() {
      if (this.get('content.maximum_am_resource_percent')) {
-        return this.get('content.maximum_am_resource_percent')
+       return false;
      } else {
-       return (schedulerResoucePercent * (absoluteCapacity / 100));
+       return true;
+     }
+   }.property('content.maximum_am_resource_percent'),
+
+   /**
+    * Returns inherited maximum am resource percent for a queue
+    */
+   getInheritedMaxAmResourcePercent: function() {
+     var parentQ = this.store.getById('queue', this.get('content.parentPath').toLowerCase());
+     while (parentQ !== null) {
+       if (parentQ.get('maximum_am_resource_percent')) {
+         return parentQ.get('maximum_am_resource_percent');
+       }
+       parentQ = this.store.getById('queue', parentQ.get('parentPath').toLowerCase());
      }
-   }.property('content.maximum_am_resource_percent', 'content.absolute_capacity', 'scheduler.maximum_am_resource_percent'),
+     return this.get('scheduler.maximum_am_resource_percent');
+   },
 
    /**
     * Sets ACL value to '*' or ' ' and returns '*' and 'custom' respectively.
@@ -344,9 +380,11 @@ App.CapschedQueuesconfEditqueueController = Ember.Controller.extend({
       var childrenQs = this.get('childrenQueues'),
       totalCapacity = 0;
       childrenQs.forEach(function(currentQ){
-        totalCapacity += currentQ.get('capacity');
+        if (typeof currentQ.get('capacity') === 'number') {
+          totalCapacity += currentQ.get('capacity');
+        }
       });
-      return totalCapacity;
+      return parseFloat(totalCapacity.toFixed(this.get('precision')));
     }.property('childrenQueues.length', 'childrenQueues.@each.capacity'),
 
     widthPattern: 'width: %@%',
@@ -388,11 +426,14 @@ App.CapschedQueuesconfEditqueueController = Ember.Controller.extend({
      this.set('queueDirtyFields.' + queueProp, this.get('content').changedAttributes().hasOwnProperty(queueProp));
    },
 
+   //Node Labels
    allNodeLabels: Ember.computed.alias('store.nodeLabels.content'),
    queueNodeLabels: Ember.computed.alias('content.sortedLabels'),
    hasQueueLabels: Ember.computed.gt('content.sortedLabels.length', 0),
    nonAccessibleLabels: Ember.computed.alias('content.nonAccessibleLabels'),
    allLabelsForQueue: Ember.computed.union('queueNodeLabels', 'nonAccessibleLabels'),
+   sortLabelsBy: ['name'],
+   sortedAllLabelsForQueue: Ember.computed.sort('allLabelsForQueue', 'sortLabelsBy'),
    hasAnyNodeLabelsForQueue: Ember.computed.gt('allLabelsForQueue.length', 0),
 
    accessibleLabelNames: function() {
@@ -435,6 +476,136 @@ App.CapschedQueuesconfEditqueueController = Ember.Controller.extend({
      return chidrenQLabels;
    }.property('childrenQueues.length', 'childrenQueues.@each.labels.[]', 'content.labels.length'),
 
-   hasChildrenQueueLabels: Ember.computed.gt('childrenQueueLabels.length', 0)
+   hasChildrenQueueLabels: Ember.computed.gt('childrenQueueLabels.length', 0),
+
+   //Default Node Label
+   initDefaultLabelOptions: function() {
+     var optionsObj = [{label: 'None', value: null}],
+     labels = this.get('queueNodeLabels'),
+     len = this.get('queueNodeLabels.length');
+     if (len > 0) {
+       labels.forEach(function(lb){
+         var obj = {label: lb.get('name'), value: lb.get('name')};
+         optionsObj.pushObject(obj);
+       });
+     }
+     this.set('defaultNodeLabelOptions', optionsObj);
+   }.observes('queueNodeLabels.[]').on('init'),
+
+   defaultNodeLabelOptions: [],
+
+   isDefaultNodeLabelInherited: function() {
+     if (this.get('content.default_node_label_expression') !== null) {
+       return false;
+     } else {
+       return true;
+     }
+   }.property('content.default_node_label_expression'),
+
+   queueDefaultNodeLabelExpression: function(key, value) {
+     if (arguments.length > 1) {
+       if (value !== null) {
+         this.set('content.default_node_label_expression', value);
+       } else {
+         this.set('content.default_node_label_expression', null);
+       }
+     }
+     if (this.get('content.default_node_label_expression') !== null) {
+       return this.get('content.default_node_label_expression');
+     } else {
+       return this.getDefaultNodeLabelExpressionInherited();
+     }
+   }.property('content.default_node_label_expression'),
+
+   getDefaultNodeLabelExpressionInherited: function() {
+     var store = this.get('store'),
+     currentQ = this.get('content'),
+     parentQ = store.getById('queue', currentQ.get('parentPath').toLowerCase()),
+     dnlexpr = null;
+     while (parentQ !== null) {
+       if (parentQ.get('default_node_label_expression') !== null) {
+         return parentQ.get('default_node_label_expression');
+       }
+       parentQ = store.getById('queue', parentQ.get('parentPath').toLowerCase());
+     }
+     return dnlexpr;
+   },
+
+   selectedDefaultNodeLabel: function() {
+     var labels = this.get('queueNodeLabels');
+     var dnle = this.get('content.default_node_label_expression') || this.getDefaultNodeLabelExpressionInherited();
+     if (dnle !== null && labels.findBy('name', dnle)) {
+       return dnle;
+     }
+     return 'None';
+   }.property('content.default_node_label_expression'),
+
+   isValidDefaultNodeLabel: function() {
+     return this.get('selectedDefaultNodeLabel') !== 'None';
+   }.property('selectedDefaultNodeLabel'),
+
+   //Preemption
+   isPreemptionSupported: Ember.computed.alias('store.isPreemptionSupported'),
+   currentStack: Ember.computed.alias('store.stackId'),
+
+   isPreemptionOverriden: function(key, value) {
+     if (arguments.length > 1) {
+       this.set('content.isPreemptionOverriden', value);
+     }
+     return this.get('content.isPreemptionOverriden');
+   }.property('content.isPreemptionOverriden'),
+
+   isPreemptionInherited: function() {
+     return this.get('content.isPreemptionInherited');
+   }.property('content.disable_preemption', 'content.isPreemptionInherited'),
+
+   queueDisablePreemption: function() {
+     if (!this.get('isPreemptionInherited')) {
+       return (this.get('content.disable_preemption')==='true')?true:false;
+     } else {
+       return this.getInheritedQueuePreemption();
+     }
+   }.property('content.disable_preemption', 'content.isPreemptionInherited'),
+
+   getInheritedQueuePreemption: function() {
+     var store = this.get('store'),
+     currentQ = this.get('content'),
+     parentQ = store.getById('queue', currentQ.get('parentPath').toLowerCase()),
+     preemption = '';
+     while (parentQ !== null) {
+       if (!parentQ.get('isPreemptionInherited')) {
+         preemption = parentQ.get('disable_preemption');
+         return (preemption==='true')?true:false;
+       }
+       parentQ = store.getById('queue', parentQ.get('parentPath').toLowerCase());
+     }
+     return preemption;
+   },
+
+   isQueuePreemptionDirty: function() {
+     return this.get('queueDirtyFields.disable_preemption');
+   }.property('content.disable_preemption', 'content.isPreemptionInherited'),
+
+   doOverridePreemption: function(key, value) {
+     if (value) {
+       this.set('content.isPreemptionInherited', false);
+       this.set('content.disable_preemption', (value === 'disable')?'true':'false');
+     }
+     if (this.get('content.isPreemptionInherited')) {
+       return '';
+     } else {
+       return (this.get('content.disable_preemption')==='true')?'disable':'enable';
+     }
+   }.property('content.disable_preemption', 'content.isPreemptionInherited'),
+
+   preemptionOvierideWatcher: function() {
+     var override = this.get('content.isPreemptionOverriden'),
+     wasInheritedInitially = (this.get('content').changedAttributes().hasOwnProperty('isPreemptionInherited')
+      && this.get('content').changedAttributes()['isPreemptionInherited'][0] === true);
+     if (override === false && wasInheritedInitially) {
+       this.set('content.isPreemptionInherited', true);
+       this.set('content.disable_preemption', '');
+     }
+   }.observes('content.isPreemptionOverriden')
 
 });

+ 87 - 14
contrib/views/capacity-scheduler/src/main/resources/ui/app/controllers/queuesconf.js

@@ -26,7 +26,10 @@ App.CapschedQueuesconfController = Ember.Controller.extend({
   queues: Ember.computed.alias('controllers.capsched.queues'),
   isOperator: Ember.computed.alias('controllers.capsched.isOperator'),
   allNodeLabels: Ember.computed.alias('store.nodeLabels.content'),
+  isRmOffline: Ember.computed.alias('store.isRmOffline'),
+  isNodeLabelsEnabledByRM: Ember.computed.alias('store.isNodeLabelsEnabledByRM'),
   allNodeLabelRecords: [],
+  queuesNeedRefresh: Ember.computed.alias('store.queuesNeedRefresh'),
 
   actions: {
     addNewQueue: function() {
@@ -68,6 +71,13 @@ App.CapschedQueuesconfController = Ember.Controller.extend({
       });
       this.set('newQueue', newQueue);
       store.saveAndUpdateQueue(newQueue).then(Em.run.bind(this, 'saveAndUpdateQueueSuccess', newQueue));
+
+      if (!this.get('queuesNeedRefresh').findBy('path', queuePath)) {
+        this.get('queuesNeedRefresh').addObject({
+          path: queuePath,
+          name: queueName
+        });
+      }
     },
     clearCreateQueue: function() {
       this.set('newQueueName', '');
@@ -145,8 +155,7 @@ App.CapschedQueuesconfController = Ember.Controller.extend({
     var parentPath = this.get('selectedQueue.path'),
     queueName = this.get('newQueueName'),
     queuePath = [parentPath, queueName].join('.'),
-    qAlreadyExists = this.store.hasRecordForId('queue', queuePath.toLowerCase());
-
+    qAlreadyExists = (this.get('queues').findBy('name', queueName))? true : false;
     if (Ember.isBlank(queueName)) {
       this.set('isInvalidQueueName', true);
       this.set('invalidQueueNameMessage', 'Enter queue name');
@@ -165,6 +174,11 @@ App.CapschedQueuesconfController = Ember.Controller.extend({
   }.observes('newQueueName', 'newQueueName.length'),
 
   initNodeLabelRecords: function() {
+    if (!this.get('isNodeLabelsEnabledByRM') || this.get('isRmOffline')) {
+      return;
+    }
+    this.setDefaultNodeLabelAccesses('root');
+    this.checkNodeLabelsInvalidCapacity('root');
     var allQs = this.get('queues'),
     allLabels = this.get('allNodeLabels'),
     store = this.get('store'),
@@ -183,9 +197,69 @@ App.CapschedQueuesconfController = Ember.Controller.extend({
       queue.set('nonAccessibleLabels', nonAccessible);
     });
     this.set('allNodeLabelRecords', records);
-
   }.on('init'),
 
+  setDefaultNodeLabelAccesses: function(queuePath) {
+    var allLabels = this.get('allNodeLabels'),
+    store = this.get('store'),
+    ctrl = this,
+    queue = store.getById('queue', queuePath.toLowerCase()),
+    parentQ = store.getById('queue', queue.get('parentPath').toLowerCase()),
+    children = store.all('queue').filterBy('depth', queue.get('depth') + 1).filterBy('parentPath', queue.get('path'));
+
+    if (Ember.isEmpty(queue.get('labels'))) {
+      if (parentQ && parentQ.get('labels.length') > 0) {
+        parentQ.get('labels').forEach(function(label) {
+          var qlabel = store.getById('label', [queue.get('path'), label.get('name')].join('.'));
+          queue.addQueueNodeLabel(qlabel);
+        });
+      } else {
+        allLabels.forEach(function(label) {
+          var qlabel = store.getById('label', [queue.get('path'), label.name].join('.'));
+          queue.addQueueNodeLabel(qlabel);
+        });
+      }
+    }
+
+    if (queue.get('name') === 'root') {
+      queue.get('labels').setEach('capacity', 100);
+    }
+
+    children.forEach(function(child) {
+      ctrl.setDefaultNodeLabelAccesses(child.get('path'));
+    });
+  },
+
+  checkNodeLabelsInvalidCapacity: function(queuePath) {
+    var allLabels = this.get('allNodeLabels'),
+    store = this.get('store'),
+    ctrl = this,
+    queue = store.getById('queue', queuePath.toLowerCase()),
+    children = store.all('queue').filterBy('depth', queue.get('depth') + 1).filterBy('parentPath', queue.get('path'));
+
+    allLabels.forEach(function(lab) {
+      var total = 0;
+      children.forEach(function(child) {
+        var cLabel = child.get('labels').findBy('name', lab.name);
+        if (cLabel) {
+          total += cLabel.get('capacity');
+        }
+      });
+      if (total !== 100) {
+        children.forEach(function(child) {
+          var cLabel = child.get('labels').findBy('name', lab.name);
+          if (cLabel) {
+            cLabel.set('overCapacity', true);
+          }
+        });
+      }
+    });
+
+    children.forEach(function(child) {
+      ctrl.checkNodeLabelsInvalidCapacity(child.get('path'));
+    });
+  },
+
   /**
    * Marks each queue in leaf with 'overCapacity' if sum if their capacity values is greater than 100.
    * @method capacityControl
@@ -361,24 +435,23 @@ App.CapschedQueuesconfController = Ember.Controller.extend({
     return this.get('selectedQueue.state') === _stopState;
   }.property('selectedQueue.state'),
 
-
   isSelectedQLeaf: function() {
     return this.get('selectedQueue.queues') === null;
   }.property('selectedQueue.queues'),
 
-  isSelectedQLeafAndRunning: function() {
-    return this.get('isSelectedQLeaf') && this.get('isSelectedQRunning');
-  }.property('isSelectedQRunning', 'isSelectedQLeaf'),
+  canAddChildrenQueues: function() {
+    return this.get('isRmOffline') || !this.get('isSelectedQLeaf') || this.get('isSelectedQueueRmStateNotRunning');
+  }.property('isRmOffline', 'isSelectedQLeaf', 'isSelectedQueueRmStateNotRunning'),
 
-  /**
-   * Returns true if queue is root.
-   * @type {Boolean}
-   */
    isRootQSelected: Ember.computed.match('selectedQueue.id', /^(root)$/),
 
-   isSelectedQDeletable: function() {
-     return !this.get('isRootQSelected') && !this.get('isSelectedQRunning');
-   }.property('isRootQSelected', 'isSelectedQRunning'),
+   isSelectedQueueRmStateNotRunning: function() {
+     return this.get('selectedQueue.isNewQueue') || this.get('selectedQueue.rmQueueState') === _stopState;
+   }.property('selectedQueue.rmQueueState', 'selectedQueue.isNewQueue'),
+
+   isSelectedQueueDeletable: function() {
+     return !this.get('isRootQSelected') && (this.get('isRmOffline') || this.get('isSelectedQueueRmStateNotRunning'));
+   }.property('isRootQSelected', 'isSelectedQueueRmStateNotRunning', 'isRmOffline'),
 
    configNote: Ember.computed.alias('store.configNote'),
    saveMode: '',

+ 24 - 1
contrib/views/capacity-scheduler/src/main/resources/ui/app/models/queue.js

@@ -36,7 +36,13 @@ App.Label = DS.Model.extend({
   isDefault: function () {
     return this.get('queue.default_node_label_expression') === this.get('name');
   }.property('queue.default_node_label_expression'),
-  absoluteCapacity: 0
+  absoluteCapacity: 0,
+  setCapacity: function(cap) {
+    this.set('capacity', cap);
+  },
+  setMaxCapacity: function(maxCap) {
+    this.set('maximum_capacity', maxCap);
+  }
 });
 
 App.Scheduler = DS.Model.extend({
@@ -155,6 +161,8 @@ App.Queue = DS.Model.extend({
       if (!this.get('nonAccessibleLabels').contains(qLabel)) {
         this.get('nonAccessibleLabels').addObject(qLabel);
       }
+      qLabel.setCapacity(0);
+      qLabel.setMaxCapacity(100);
       this.notifyPropertyChange('labels');
       this.notifyPropertyChange('nonAccessibleLabels');
     }
@@ -183,6 +191,15 @@ App.Queue = DS.Model.extend({
     this.notifyPropertyChange('nonAccessibleLabels');
   },
 
+  recursiveAddChildQueueLabels: function(qLabel) {
+    qLabel = this.store.getById('label', [this.get('path'), qLabel.get('name')].join('.'));
+    this.addQueueNodeLabel(qLabel);
+    var childrenQs = this.store.all('queue').filterBy('depth', this.get('depth') + 1).filterBy('parentPath', this.get('path'));
+    childrenQs.forEach(function(child) {
+      child.recursiveAddChildQueueLabels(qLabel);
+    }.bind(this));
+  },
+
   isAnyDirty: function () {
     return this.get('isDirty') || !Em.isEmpty(this.get('labels').findBy('isDirty',true)) || this.get('isLabelsDirty');
   }.property('isDirty','labels.@each.isDirty','initialLabels','isLabelsDirty'),
@@ -231,6 +248,9 @@ App.Queue = DS.Model.extend({
   minimum_user_limit_percent: DS.attr('number', { defaultValue: 100 }),
   maximum_applications: DS.attr('number', { defaultValue: null }),
   maximum_am_resource_percent: DS.attr('number', { defaultValue: null }),
+  disable_preemption: DS.attr('string', {defaultValue: ''}),
+  isPreemptionInherited: DS.attr('boolean', {defaultValue: true}),
+  isPreemptionOverriden: false,
 
   queues: DS.attr('string'),
   queuesArray:function (key,val) {
@@ -260,6 +280,9 @@ App.Queue = DS.Model.extend({
 
   nonAccessibleLabels: [],
 
+  //queue state from resource manager
+  rmQueueState: 'UNKNOWN',
+
   //new queue flag
   isNewQueue:DS.attr('boolean', {defaultValue: false}),
 

+ 15 - 6
contrib/views/capacity-scheduler/src/main/resources/ui/app/router.js

@@ -23,17 +23,15 @@ App.Router.map(function() {
     this.resource('queue', { path: '/:queue_id' });
     this.resource('trace', { path: '/log' });
   });
-  this.route('refuse');
-
   this.resource('capsched', {path: '/capacity-scheduler'}, function() {
     this.route('scheduler', {path: '/scheduler'});
     this.route('advanced', {path: '/advanced'});
     this.route('trace', {path: '/log'});
-    this.route('refuse', {path: '/refuse'});
     this.route('queuesconf', {path: '/queues'}, function() {
       this.route('editqueue', {path: '/:queue_id'});
     });
   });
+  this.route('refuse');
 });
 
 var RANGER_SITE = 'ranger-yarn-plugin-properties';
@@ -226,6 +224,7 @@ App.CapschedRoute = Ember.Route.extend({
         opt = 'saveAndRefresh';
       }
 
+      capschedCtrl.startSpinner();
       Em.RSVP.Promise.all([labels, queues, scheduler]).then(
         Em.run.bind(that,'saveConfigsSuccess'),
         Em.run.bind(that,'saveConfigsError', 'save')
@@ -233,8 +232,13 @@ App.CapschedRoute = Ember.Route.extend({
         if (opt) {
           return store.relaunchCapSched(opt);
         }
-      })
-      .catch(Em.run.bind(this,'saveConfigsError', opt));
+      }).then(function(){
+        return store.getRmSchedulerConfigInfo();
+      }).catch(
+        Em.run.bind(this,'saveConfigsError', opt)
+      ).finally(function(){
+        capschedCtrl.stopSpinner();
+      });
     }
   },
   beforeModel: function(transition) {
@@ -277,6 +281,11 @@ App.CapschedRoute = Ember.Route.extend({
         return store.find('queue');
       }).then(function(queues) {
         controller.set('queues', queues);
+        loadingController.set('model', {
+          message: 'loading rm info'
+        });
+        return store.getRmSchedulerConfigInfo();
+      }).then(function() {
         return store.find('scheduler', 'scheduler');
       }).then(function(scheduler){
         resolve(scheduler);
@@ -326,6 +335,6 @@ App.CapschedQueuesconfEditqueueRoute = Ember.Route.extend({
 
 App.CapschedTraceRoute = Ember.Route.extend({
   model: function() {
-    return this.controllerFor('capsched.queuesconf').get('alertMessage');
+    return this.controllerFor('capsched').get('alertMessage');
   }
 });

+ 8 - 1
contrib/views/capacity-scheduler/src/main/resources/ui/app/serializers.js

@@ -106,7 +106,9 @@ App.SerializerMixin = Em.Mixin.create({
           ordering_policy:               props[base_path + ".ordering-policy"] || null,
           enable_size_based_weight:      props[base_path + ".ordering-policy.fair.enable-size-based-weight"] || null,
           default_node_label_expression: props[base_path + ".default-node-label-expression"] || null,
-          labelsEnabled:                 props.hasOwnProperty(labelsPath)
+          labelsEnabled:                 props.hasOwnProperty(labelsPath),
+          disable_preemption:            props[base_path + '.disable_preemption'] || '',
+          isPreemptionInherited:         (props[base_path + '.disable_preemption'] !== undefined)?false:true
         };
 
     switch ((props.hasOwnProperty(labelsPath))?props[labelsPath]:'') {
@@ -250,6 +252,11 @@ App.QueueSerializer = DS.RESTSerializer.extend(App.SerializerMixin,{
       }
     }, this);
 
+    var isPreemptionSupported = record.get('store.isPreemptionSupported');
+    if (isPreemptionSupported && !record.get('isPreemptionInherited')) {
+      json[this.PREFIX + "." + record.get('path') + ".disable_preemption"] = (record.get('disable_preemption')==='true')? true:false;
+    }
+
     return json;
   },
   serializeHasMany:function (record, json, relationship) {

+ 76 - 0
contrib/views/capacity-scheduler/src/main/resources/ui/app/store.js

@@ -105,6 +105,46 @@ function _fetchTagged(adapter, store, type, sinceToken) {
   }, null, "DS: Extract payload of findAll " + type);
 }
 
+function _fillRmQueueStateIntoQueues(data, store) {
+  var parsed = JSON.parse(data),
+  queuesNeedRefresh = store.get('queuesNeedRefresh'),
+  rootQInfo = parsed['scheduler']['schedulerInfo'];
+  var queueStates = _getRmQueueStates(rootQInfo, 'root', []);
+  store.all('queue').forEach(function(queue) {
+    var qInfo = queueStates.findBy('path', queue.get('path'));
+    if (qInfo && qInfo.state) {
+      queue.set('rmQueueState', qInfo.state);
+      if (queuesNeedRefresh.findBy('path', queue.get('path'))) {
+        queuesNeedRefresh.removeObject(queuesNeedRefresh.findBy('path', queue.get('path')));
+      }
+    } else {
+      if (!queuesNeedRefresh.findBy('path', queue.get('path'))) {
+        queuesNeedRefresh.addObject({
+          path: queue.get('path'),
+          name: queue.get('name')
+        });
+      }
+    }
+  });
+}
+
+function _getRmQueueStates(queueInfo, qPath, qStates) {
+  var qObj = {
+    name: queueInfo.queueName,
+    path: qPath.toLowerCase(),
+    state: queueInfo.state || 'RUNNING'
+  };
+  qStates.addObject(qObj);
+  if (queueInfo.queues) {
+    var children = queueInfo.queues.queue || [];
+    children.forEach(function(child) {
+      var cPath = [qPath, child.queueName].join('.');
+      return _getRmQueueStates(child, cPath, qStates);
+    });
+  }
+  return qStates;
+}
+
 App.ApplicationStore = DS.Store.extend({
 
   adapter: App.QueueAdapter,
@@ -119,10 +159,21 @@ App.ApplicationStore = DS.Store.extend({
 
   stackId: '',
 
+  isPreemptionSupported: function() {
+    var stackId = this.get('stackId'),
+    stackVersion = stackId.substr(stackId.indexOf('-') + 1);
+    if (stackVersion >= 2.3) {
+      return true;
+    }
+    return false;
+  }.property('stackId'),
+
   hasDeletedQueues:Em.computed.notEmpty('deletedQueues.[]'),
 
   deletedQueues:[],
 
+  queuesNeedRefresh: [],
+
   buildConfig: function (fmt) {
     var records = [],
         config = '',
@@ -379,5 +430,30 @@ App.ApplicationStore = DS.Store.extend({
   },
   checkCluster:function () {
     return this.get('defaultAdapter').checkCluster(this);
+  },
+  getRmSchedulerConfigInfo: function() {
+    var store = this,
+    adapter = this.get('defaultAdapter');
+    return new Ember.RSVP.Promise(function(resolve, reject) {
+      adapter.getRmSchedulerConfigInfo().then(function(data) {
+        if (data) {
+          _fillRmQueueStateIntoQueues(data, store);
+        }
+        store.set('isRmOffline', false);
+        resolve([]);
+      }, function() {
+        store.set('isRmOffline', true);
+        resolve([]);
+      }).finally(function() {
+        store.pollRmSchedulerConfigInfo();
+      });
+    });
+  },
+  pollRmSchedulerConfigInfo: function() {
+    var store = this;
+    //Poll getRmSchedulerConfigInfo every 1 minute.
+    Ember.run.later(store, function() {
+      store.getRmSchedulerConfigInfo();
+    }, 60000);
   }
 });

+ 55 - 14
contrib/views/capacity-scheduler/src/main/resources/ui/app/styles/application.less

@@ -961,6 +961,10 @@
         padding: 2px 3px;
       }
     }
+    .tooltip-inner {
+      white-space: pre;
+      max-width: none;
+    }
   }
 }
 
@@ -1059,6 +1063,9 @@
     &:focus {
       outline: none;
     }
+    &:disabled {
+      cursor: not-allowed !important;
+    }
     &::-webkit-slider-runnable-track {
       width: 100%;
       height: 2px;
@@ -1144,11 +1151,11 @@
       margin-top: 5px;
     }
     .capacity-input-percent {
-      width: 25%;
+      width: 90px;
       float: left;
       display: inline-block;
       input {
-        width: 60%;
+        width: 55px;
       }
     }
     .capacity-input-slider {
@@ -1166,6 +1173,7 @@
   }
   .total-capacity-progress {
     width: 100%;
+    margin-bottom: 0;
   }
 }
 
@@ -1199,9 +1207,9 @@
     margin-bottom: 10px;
   }
   .label-capacity-input {
-    width: 35%;
+    width: 77px;
     input {
-      width: 60% !important;
+      width: 50px !important;
     }
     .input-group-addon {
       float: left;
@@ -1218,6 +1226,7 @@
     background-color: #f9f9f9!important;
   }
   .total-capacity-progress {
+    margin-bottom: 0;
     .progress-bar {
       color: #000;
     }
@@ -1229,23 +1238,55 @@
     width: 54%;
     display: inline-block;
   }
+  .default-label-options {
+    max-width: 170px;
+  }
 }
 
 .capsched-versions-wrapper {
   padding-top: 40px;
   padding-left: 0;
+  .capsched-versions-panel {
+    .panel-heading {
+      margin-bottom: -1px;
+    }
+    .panel-body {
+      padding: 0px 15px 5px 15px;
+      max-height: 428px;
+      overflow-y: auto;
+    }
+    .table {
+      margin-bottom: 0;
+    }
+  }
 }
 
-.capsched-versions-panel {
-  .panel-heading {
-    margin-bottom: -1px;
-  }
-  .panel-body {
-    padding: 0px 15px 5px 15px;
-    max-height: 428px;
-    overflow-y: auto;
+.spinner-wrapper {
+  .save-spinner {
+    position: fixed;
+    width: 150px;
+    height: 100px;
+    left: 45%;
+    background-color: #f5f5f5;
+    border-radius: 4px;
+    border: 1px solid #e4e4e4;
+    .fa-spinner {
+      margin-left: 45px;
+      margin-top: 20px;
+    }
+    .saving-text {
+      margin-left: 50px;
+      margin-top: 10px;
+    }
   }
-  .table {
-    margin-bottom: 0;
+}
+
+.preemption-container {
+  .override-btns {
+    margin-top: 10px;
   }
 }
+
+.alert-message-dialog {
+  display: block;
+}

+ 1 - 0
contrib/views/capacity-scheduler/src/main/resources/ui/app/templates.js

@@ -59,3 +59,4 @@ require('templates/components/saveConfigDialog');
 require('templates/components/labelCapacityBar');
 require('templates/components/displayRootLabel');
 require('templates/components/displayLeafLabel');
+require('templates/capsched/partials/preemption');

+ 11 - 2
contrib/views/capacity-scheduler/src/main/resources/ui/app/templates/capsched.hbs

@@ -31,7 +31,7 @@
         <a href="#">Queues</a>
       {{/link-to}}
       {{#link-to "capsched.advanced" tagName="li"}}
-        <a href="#">Advanced</a>
+        <a href="#">User Queue Mappings</a>
       {{/link-to}}
     </ul>
     <div class="tab-content">
@@ -45,7 +45,7 @@
 
 {{!-- ALERT --}}
 {{#if alertMessage}}
-  <div class="alert alert-danger">
+  <div class="alert alert-danger modal alert-message-dialog" tabindex="-1" role="dialog" aria-hidden="false">
     <button {{action 'clearAlert'}} type="button" class="close" aria-hidden="false">&times;</button>
     <strong> {{alertMessage.status}} </strong> {{alertMessage.simpleMessage}}
     <div>
@@ -55,3 +55,12 @@
     {{#link-to 'capsched.trace' class="alert-link"}}Trace{{/link-to}}
   </div>
 {{/if}}
+
+<div {{bind-attr class=":spinner-wrapper showSpinner::hide"}}>
+  <div class="save-spinner">
+    <i class="fa fa-spinner fa-spin fa-3x fa-fw"></i>
+    <div class="saving-text">
+      SAVING...
+    </div>
+  </div>
+</div>

+ 41 - 5
contrib/views/capacity-scheduler/src/main/resources/ui/app/templates/capsched/partials/labelCapacity.hbs

@@ -31,6 +31,34 @@
               <label>Accessible Node Labels: </label>
               <span>{{accessibleLabelNames}}</span>
             </div>
+            <div class="row">
+              <div class="col-sm-6 col-md-6">
+                <label>Default Label: </label>
+                <span>{{selectedDefaultNodeLabel}}</span>
+                {{#if isDefaultNodeLabelInherited}}
+                  {{#unless isRoot}}
+                    {{#if isValidDefaultNodeLabel}}
+                      <span>(Inherited)</span>
+                    {{/if}}
+                  {{/unless}}
+                {{/if}}
+              </div>
+              <div class="col-sm-6 col-md-6">
+                <label>Set Default Node Label: </label>
+                {{view Ember.Select
+                  class="default-label-options"
+                  content=defaultNodeLabelOptions
+                  optionLabelPath="content.label"
+                  optionValuePath="content.value"
+                  value=queueDefaultNodeLabelExpression
+                }}
+                {{#if queueDirtyFields.default_node_label_expression}}
+                  <div class="btn-group btn-group-xs">
+                    <a {{action 'rollbackProp' 'default_node_label_expression' content}} href="#" class="btn btn-default btn-warning"><i class="fa fa-undo"></i></a>
+                  </div>
+                {{/if}}
+              </div>
+            </div>
             {{#if isRoot}}
               <div class="border-wrapper">
                 <div class="row">
@@ -45,7 +73,7 @@
                   </div>
                 </div>
                 {{#if hasAnyNodeLabelsForQueue}}
-                  {{#each label in allLabelsForQueue}}
+                  {{#each label in sortedAllLabelsForQueue}}
                     {{display-root-label queueLabels=queueNodeLabels nonAccessibleLabels=nonAccessibleLabels label=label isRoot=isRoot}}
                   {{/each}}
                 {{else}}
@@ -100,8 +128,12 @@
               {{/if}}
             {{/if}}
           {{else}}
-            <div>
-              <small>Node labels are disabled by RM. Configure and enable labels to map the labels to queues.</small>
+            <div class="text-warning">
+              {{#if isRmOffline}}
+                <small>Unable to obtain information about the node labels from the resource manager</small>
+              {{else}}
+                <small>Node labels are disabled by resource manager. Enable node labels to map the labels to queues.</small>
+              {{/if}}
             </div>
           {{/if}}
         {{else}}
@@ -120,8 +152,12 @@
               </div>
             {{/if}}
           {{else}}
-            <div>
-              <small>Node labels are disabled</small>
+            <div class="text-warning">
+              {{#if isRmOffline}}
+                <small>Unable to obtain information about the node labels from the resource manager</small>
+              {{else}}
+                <small>Node labels are disabled by resource manager</small>
+              {{/if}}
             </div>
           {{/if}}
         {{/if}}

+ 75 - 0
contrib/views/capacity-scheduler/src/main/resources/ui/app/templates/capsched/partials/preemption.hbs

@@ -0,0 +1,75 @@
+{{!
+* 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="preemption-container">
+  <div class="panel panel-default">
+    <div class="panel-heading">
+      <div class="panel-title">
+        Preemption <a id="collapseQueuePreemptionPanelBtn" href="#collapsibleQueuePreemptionPanel" data-toggle="collapse" {{bind-attr class=":pull-right view.isPreemptionPanelCollapsed:collapsed"}}><i {{bind-attr class=":fa view.isPreemptionPanelCollapsed:fa-plus:fa-minus"}}></i></a>
+      </div>
+    </div>
+    <div id="collapsibleQueuePreemptionPanel" {{bind-attr class=":panel-collapse :collapse view.isPreemptionPanelCollapsed::in"}}>
+      <div class="panel-body">
+        {{#if isPreemptionSupported}}
+          <div class="row">
+            <div class="col-sm-10 col-md-10">
+              <label>Can this queue be preempted by other queues: </label>
+              {{#if queueDisablePreemption}}
+                <span class="text-danger">Disabled </span>
+              {{else}}
+                <span class="text-success">Enabled </span>
+              {{/if}}
+              {{#if isPreemptionInherited}}
+                {{#if isRoot}}
+                  <span>(Inherited from global settings)</span>
+                {{else}}
+                  <span>(Inherited)</span>
+                {{/if}}
+              {{/if}}
+              {{#if isQueuePreemptionDirty}}
+                {{warning-info class="yellow-warning" tooltip="Need refresh queues/restart RM"}}
+              {{/if}}
+            </div>
+          </div>
+          <div class="row">
+            <div class="col-sm-4 col-md-4">
+              <div class="checkbox">
+                <label>
+                  {{input type="checkbox" name="preemptionOverride" checked=isPreemptionOverriden}}
+                  Override Preemption
+                </label>
+              </div>
+            </div>
+            {{#if isPreemptionOverriden}}
+              <div class="col-sm-3 col-md-3">
+                <div class="btn-group btn-group-sm override-btns" data-toggle="buttons">
+                  {{radio-button label="Enable" classNames="btn-small" selectionBinding="doOverridePreemption" value="enable"}}
+                  {{radio-button label="Disable" classNames="btn-small" selectionBinding="doOverridePreemption" value="disable"}}
+                </div>
+              </div>
+            {{/if}}
+          </div>
+        {{else}}
+          <div class="text-warning">
+            <span>Preemption is not supported for your current stack version {{currentStack}}</span>
+          </div>
+        {{/if}}
+      </div>
+    </div>
+  </div>
+</div>

+ 13 - 5
contrib/views/capacity-scheduler/src/main/resources/ui/app/templates/capsched/partials/queueCapacity.hbs

@@ -20,7 +20,7 @@
   <div class="panel panel-default">
     <div class="panel-heading">
       <div class="panel-title">
-        Capacity <a id="collapseQueueCapacityPanelBtn" href="#collapsibleQueueCapacityPanel" data-toggle="collapse" {{bind-attr class=":pull-right view.isCapacityPanelCollapsed:collapsed"}}><i {{bind-attr class=":fa view.isCapacityPanelCollapsed:fa-plus:fa-minus"}}></i></a>
+        Queue Capacity <a id="collapseQueueCapacityPanelBtn" href="#collapsibleQueueCapacityPanel" data-toggle="collapse" {{bind-attr class=":pull-right view.isCapacityPanelCollapsed:collapsed"}}><i {{bind-attr class=":fa view.isCapacityPanelCollapsed:fa-plus:fa-minus"}}></i></a>
       </div>
     </div>
     <div id="collapsibleQueueCapacityPanel" {{bind-attr class=":panel-collapse :collapse view.isCapacityPanelCollapsed::in"}}>
@@ -61,8 +61,16 @@
             </div>
             {{#if warnInvalidCapacity}}
               <div class="row">
-                <div class="col-sm-6 col-md-6">
-                  <span class="text-danger">Total capacity must be equal to 100%</span>
+                <div class="col-sm-6 col-md-6 col-md-offset-2 col-sm-offset-2">
+                  <small class="text-danger">Total capacity must be equal to 100%</small>
+                </div>
+              </div>
+            {{/if}}
+            {{#if parentQueue}}
+              <div class="row">
+                <div class="col-md-10 col-sm-10">
+                  <small>To edit capacity and maximum capacity at parent queue level</small>
+                  <small>{{#link-to 'capsched.queuesconf.editqueue' parentQueue}}Click Here{{/link-to}}</small>
                 </div>
               </div>
             {{/if}}
@@ -79,8 +87,8 @@
             </div>
             <div class="row">
               <div class="col-md-10 col-sm-10">
-                <span>To edit capacity and maximum capacity at parent queue level</span>
-                <span>{{#link-to 'capsched.queuesconf.editqueue' parentQueue}}Click Here{{/link-to}}</span>
+                <small>To edit capacity and maximum capacity at parent queue level</small>
+                <small>{{#link-to 'capsched.queuesconf.editqueue' parentQueue}}Click Here{{/link-to}}</small>
               </div>
             </div>
           {{/unless}}

+ 26 - 22
contrib/views/capacity-scheduler/src/main/resources/ui/app/templates/capsched/partials/queueResources.hbs

@@ -96,13 +96,15 @@
                 </div>
               {{/if}}
             </div>
-            <div class="col-sm-2 inherited-value">
-              <span>(Inherited)</span>
-            </div>
+            {{#if isMaximumApplicationsInherited}}
+              <div class="col-sm-2 inherited-value">
+                <span>(Inherited)</span>
+              </div>
+            {{/if}}
           {{else}}
             <div class="col-sm-3">
             {{#if maximumApplications}}
-              <p class="form-control-static">{{maximumApplications}} (Inherited)</p>
+              <p class="form-control-static">{{maximumApplications}} {{#if isMaximumApplicationsInherited}}(Inherited){{/if}}</p>
             {{else}}
               <p class="form-control-static">Inherited</p>
             {{/if}}
@@ -117,28 +119,30 @@
             message='The maximum percentage of total capacity of this specific queue that can be utilized by application masters at any point in time.'
           }}
           {{#if isOperator}}
-          <div class="col-sm-3 control-value input-percent-wrap">
-            <div class="input-group input-percent">
-              {{int-input placeholder="Inherited" value=maximumAMResourcePercent class="input-sm" maxVal=100}}
-              <span class="input-group-addon">%</span>
+            <div class="col-sm-3 control-value input-percent-wrap">
+              <div class="input-group input-percent">
+                {{int-input placeholder="Inherited" value=maximumAMResourcePercent class="input-sm" maxVal=100}}
+                <span class="input-group-addon">%</span>
+              </div>
+              {{#if queueDirtyFields.maximum_am_resource_percent}}
+                <div class="btn-group btn-group-xs">
+                  <a {{action 'rollbackProp' 'maximum_am_resource_percent' content}} href="#" class="btn btn-default btn-warning"><i class="fa fa-undo"></i></a>
+                </div>
+              {{/if}}
             </div>
-            {{#if queueDirtyFields.maximum_am_resource_percent}}
-              <div class="btn-group btn-group-xs">
-                <a {{action 'rollbackProp' 'maximum_am_resource_percent' content}} href="#" class="btn btn-default btn-warning"><i class="fa fa-undo"></i></a>
+            {{#if isMaxAmResourcePercentInherited}}
+              <div class="col-sm-2 inherited-value">
+                <span>(Inherited)</span>
               </div>
             {{/if}}
-          </div>
-          <div class="col-sm-2 inherited-value">
-            <span>(Inherited)</span>
-          </div>
           {{else}}
-          <div class="col-sm-2">
-            {{#if maximumAMResourcePercent}}
-              <p class="form-control-static">{{maximumAMResourcePercent}}% (Inherited)</p>
-            {{else}}
-              <p class="form-control-static">Inherited</p>
-            {{/if}}
-          </div>
+            <div class="col-sm-2">
+              {{#if maximumAMResourcePercent}}
+                <p class="form-control-static">{{maximumAMResourcePercent}}% {{#if isMaxAmResourcePercentInherited}}(Inherited){{/if}}</p>
+              {{else}}
+                <p class="form-control-static">Inherited</p>
+              {{/if}}
+            </div>
           {{/if}}
         </div>
 

+ 8 - 5
contrib/views/capacity-scheduler/src/main/resources/ui/app/templates/capsched/queuesconf.hbs

@@ -28,8 +28,11 @@
             {{#if selectedQueue}}
               <div class="queue-actions-wrapper">
                 <div class="pull-left">
-                  <button type="button" {{bind-attr class=":btn :btn-primary :btn-xs isSelectedQLeafAndRunning:disabled"}} name="addQueueBtn" {{action "addNewQueue"}}>Add Queue</button>
+                  <button type="button" {{bind-attr class=":btn :btn-primary :btn-xs canAddChildrenQueues::disabled"}} name="addQueueBtn" {{action "addNewQueue"}}>Add Queue</button>
                 </div>
+                {{#unless canAddChildrenQueues}}
+                  {{warning-info class="yellow-warning" tooltip="Queue must be STOPPED to add children queues"}}
+                {{/unless}}
                 <div class="pull-right">
                   {{#if isSelectedQRunning}}
                     <button type="button" class="btn btn-primary btn-xs" name="queueStateBtn" {{action "stopQueue"}}>Stop Queue</button>
@@ -38,9 +41,9 @@
                     <button type="button" class="btn btn-primary btn-xs" name="queueStateBtn" {{action "startQueue"}}>Start Queue</button>
                   {{/if}}
                   {{#unless isRootQSelected}}
-                    <button type="button" {{bind-attr class=":btn :btn-danger :btn-xs isSelectedQDeletable::disabled"}} class="btn btn-danger btn-xs" name="deleteQueueBtn" {{action "showDeleteQueueDialog" target="view"}}>Delete Queue</button>
-                    {{#unless isSelectedQDeletable}}
-                      {{warning-info class="yellow-warning" tooltip="Queue should be STOPPED to delete"}}
+                    <button type="button" {{bind-attr class=":btn :btn-danger :btn-xs isSelectedQueueDeletable::disabled"}} class="btn btn-danger btn-xs" name="deleteQueueBtn" {{action "showDeleteQueueDialog" target="view"}}>Delete Queue</button>
+                    {{#unless isSelectedQueueDeletable}}
+                      {{warning-info class="yellow-warning" tooltip="Queue must be STOPPED to delete"}}
                     {{/unless}}
                   {{/unless}}
                 </div>
@@ -66,7 +69,7 @@
           {{/if}}
         </div>
         <div class="col-sm-5">
-          {{queue-summary queue=selectedQueue allQueues=queues}}
+          {{queue-summary queue=selectedQueue allQueues=queues queuesNeedRefresh=queuesNeedRefresh}}
         </div>
       </div>
       <hr>

+ 4 - 0
contrib/views/capacity-scheduler/src/main/resources/ui/app/templates/capsched/queuesconf/editqueue.hbs

@@ -68,6 +68,10 @@
     {{partial "capsched/partials/labelCapacity"}}
   </div>
 
+  <div class="queue-preemption-wrapper">
+    {{partial "capsched/partials/preemption"}}
+  </div>
+
   <div class="queue-acl-wrapper">
     {{partial "capsched/partials/accessControlList"}}
   </div>

+ 21 - 0
contrib/views/capacity-scheduler/src/main/resources/ui/app/templates/capsched/trace.hbs

@@ -0,0 +1,21 @@
+{{!
+* 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.
+}}
+
+<pre>
+  {{trace}}
+</pre>

+ 17 - 13
contrib/views/capacity-scheduler/src/main/resources/ui/app/templates/components/editLabelCapacity.hbs

@@ -35,13 +35,15 @@
         <span class="input-group-addon">%</span>
       </div>
       <div class="form-group label-capacity-slider">
-        {{input-range min="0" max="100" step="1" value=label.capacity class="input-sm"}}
+        {{input-range min="0" max="100" step="1" value=label.capacity class="input-sm" disabled=isAccessDisabled}}
       </div>
-      {{#if isLabelCapacityDirty}}
-        <div class="btn-group btn-group-xs label-capacity-rollback">
-          <a {{action 'rollbackProp' 'capacity' label}} href="#" class="btn btn-default btn-warning"><i class="fa fa-undo"></i></a>
-        </div>
-      {{/if}}
+      {{#unless isAccessDisabled}}
+        {{#if isLabelCapacityDirty}}
+          <div class="btn-group btn-group-xs label-capacity-rollback">
+            <a {{action 'rollbackProp' 'capacity' label}} href="#" class="btn btn-default btn-warning"><i class="fa fa-undo"></i></a>
+          </div>
+        {{/if}}
+      {{/unless}}
     </div>
     <div class="col-md-4 col-sm-4">
       <div {{bind-attr class=":form-group :input-group :label-capacity-input isInvalidLabelMaxCapacity:has-error"}}>
@@ -49,16 +51,18 @@
         <span class="input-group-addon">%</span>
       </div>
       <div class="form-group label-capacity-slider">
-        {{input-range min="0" max="100" step="1" value=label.maximum_capacity class="input-sm"}}
+        {{input-range min="0" max="100" step="1" value=label.maximum_capacity class="input-sm" disabled=isAccessDisabled}}
       </div>
-      {{#if isLabelMaxCapacityDirty}}
-        <div class="btn-group btn-group-xs label-capacity-rollback">
-          <a {{action 'rollbackProp' 'maximum_capacity' label}} href="#" class="btn btn-default btn-warning"><i class="fa fa-undo"></i></a>
-        </div>
-      {{/if}}
+      {{#unless isAccessDisabled}}
+        {{#if isLabelMaxCapacityDirty}}
+          <div class="btn-group btn-group-xs label-capacity-rollback">
+            <a {{action 'rollbackProp' 'maximum_capacity' label}} href="#" class="btn btn-default btn-warning"><i class="fa fa-undo"></i></a>
+          </div>
+        {{/if}}
+      {{/unless}}
       {{#if isInvalidLabelMaxCapacity}}
         <div class="label-maxcapacity-error">
-          <span class="text-danger">Maximum capacity must be greater than or equal to capacity</span>
+          <small class="text-danger">Maximum capacity must be greater than or equal to capacity</small>
         </div>
       {{/if}}
     </div>

+ 2 - 2
contrib/views/capacity-scheduler/src/main/resources/ui/app/templates/components/editQueueCapacity.hbs

@@ -18,7 +18,7 @@
 
 <div class="row form-inline">
   <div class="col-md-2 col-sm-2">
-    <span class="queue-name">{{queue.name}}</span>
+    {{#link-to 'capsched.queuesconf.editqueue' queue}}{{queue.name}}{{/link-to}}
   </div>
   <div class="col-md-5 col-md-5">
     <div {{bind-attr class=":form-group :input-group :capacity-input-percent warnInvalidTotalCapacity:has-error"}}>
@@ -50,7 +50,7 @@
     {{#if isInvalidQueueMaximumCapacity}}
       <div class="row">
         <div class="col-md-12 col-sm-12">
-          <span class="text-danger">Maximum capacity must be greater than or equal to capacity</span>
+          <small class="text-danger">Maximum capacity must be greater than or equal to capacity</small>
         </div>
       </div>
     {{/if}}

+ 2 - 2
contrib/views/capacity-scheduler/src/main/resources/ui/app/templates/components/labelCapacityBar.hbs

@@ -32,8 +32,8 @@
     </div>
     {{#if warnInvalidLabelCapacity}}
       <div class="row">
-        <div class="col-sm-6 col-md-6">
-          <span class="text-danger">Total capacity must be equal to 100%</span>
+        <div class="col-sm-6 col-md-6 col-sm-offset-2 col-md-offset-2">
+          <small class="text-danger">Total capacity must be equal to 100%</small>
         </div>
       </div>
     {{/if}}

+ 22 - 7
contrib/views/capacity-scheduler/src/main/resources/ui/app/templates/components/queueMapping.hbs

@@ -48,17 +48,23 @@
             <div class="queue-mapping-options">
               <div class="row">
                 <div class="radio">
-                  <label title="Assign job to queue with same name as user">
+                  <label>
                     {{view view.radioButton selectionBinding="selectedMapping" name="queueMappingType" value="u:%user:%user"}}
-                    User %user -> queue %user
+                    {{tooltip-label
+                      label='Map user to queue with the same name of user'
+                      message='User %user -> queue %user <br/> Ex: User=john will be mapped to queue=john, and user=alice will be mapped to queue=alice'
+                    }}
                   </label>
                 </div>
               </div>
               <div class="row">
                 <div class="radio">
-                  <label title="Assign job to queue with same name as user's primary group">
+                  <label>
                     {{view view.radioButton selectionBinding="selectedMapping" name="queueMappingType" value="u:%user:%primary_group"}}
-                    User %user -> queue %primary_group
+                    {{tooltip-label
+                      label='Map user to queue with the same name of user’s primary group'
+                      message='User %user -> queue %primary_group <br/> Ex: User=john will be mapped to queue=john\'s primary group, and user=alice will be mapped to queue=alice\'s primary group'
+                    }}
                   </label>
                 </div>
               </div>
@@ -66,7 +72,10 @@
                 <div class="radio">
                   <label>
                     {{view view.radioButton selectionBinding="selectedMapping" name="queueMappingType" value="u:%name:%qname"}}
-                    Assign users to queue
+                    {{tooltip-label
+                      label='Assign specific users to given queue'
+                      message='Specified users will be mapped to queue specified'
+                    }}
                   </label>
                 </div>
                 {{#if isCustomUserMapping}}
@@ -100,7 +109,10 @@
                 <div class="radio">
                   <label>
                     {{view view.radioButton selectionBinding="selectedMapping" name="queueMappingType" value="g:%name:%qname"}}
-                    Assign groups to queue
+                    {{tooltip-label
+                      label='Assign users from specific groups to given queue'
+                      message='Users from specified groups will be mapped to queue specified'
+                    }}
                   </label>
                 </div>
                 {{#if isCustomGroupMapping}}
@@ -144,7 +156,10 @@
           <div class="checkbox">
             <label>
               {{input type="checkbox" name="queueMappingOverride" checked=mappingsOverrideEnable}}
-              Allow user to override queue mapping
+              {{tooltip-label
+                label='Allow application to override user-queue mapping'
+                message='When checked, applications submitted with queues specified will be used other than those defined in default queue mapping'
+              }}
             </label>
           </div>
         </div>

+ 17 - 5
contrib/views/capacity-scheduler/src/main/resources/ui/app/templates/components/queueSummary.hbs

@@ -43,25 +43,37 @@
           <div class="col-sm-5">
             <span>{{queue.capacity}}%</span>
           </div>
+          {{#if isQueueNeedRefreshOrRestart}}
+            <div class="col-sm-2">
+              {{warning-info class="yellow-warning" tooltip="Need refresh queues/restart RM to take effect"}}
+            </div>
+          {{/if}}
         </div>
         <div class="row">
           <div class="col-sm-5">
-            <label>Absolute Capacity</label>
+            <label>Absolute Capacity of Cluster</label>
           </div>
           <div class="col-sm-5">
             <span>{{effectiveCapacity}}%</span>
           </div>
+          {{#if isQueueNeedRefreshOrRestart}}
+            <div class="col-sm-2">
+              {{warning-info class="yellow-warning" tooltip="Need refresh queues/restart RM to take effect"}}
+            </div>
+          {{/if}}
         </div>
         <div class="row">
           <div class="col-sm-5">
             <label>State</label>
           </div>
-          <div class="col-sm-6">
+          <div class="col-sm-5">
             <span {{bind-attr class="qStateColor"}}>{{queueState}}</span>
-            {{#if isDirtyState}}
-              {{warning-info class="yellow-warning" tooltip="Not Yet Saved"}}
-            {{/if}}
           </div>
+          {{#if isQueueNeedRefreshOrRestart}}
+            <div class="col-sm-2">
+              {{warning-info class="yellow-warning" tooltip="Need refresh queues/restart RM to take effect"}}
+            </div>
+          {{/if}}
         </div>
       </div>
     </div>

+ 10 - 2
contrib/views/capacity-scheduler/src/main/resources/ui/app/views/editqueue.js

@@ -19,10 +19,11 @@
 var App = require('app');
 
 App.CapschedQueuesconfEditqueueView = Ember.View.extend({
+  isCapacityPanelCollapsed: true,
+  isLabelsPanelCollapsed: true,
+  isPreemptionPanelCollapsed: true,
   isAclPanelCollapsed: true,
   isResourcesPanelCollapsed: true,
-  isCapacityPanelCollapsed: false,
-  isLabelsPanelCollapsed: true,
 
   doExpandCollapsePanel: function() {
     var that = this;
@@ -46,11 +47,18 @@ App.CapschedQueuesconfEditqueueView = Ember.View.extend({
         this.toggleProperty('isLabelsPanelCollapsed');
       });
     });
+    this.$('#collapseQueuePreemptionPanelBtn').on('click', function(e) {
+      Ember.run.next(that, function() {
+        this.toggleProperty('isPreemptionPanelCollapsed');
+      });
+    });
   }.on('didInsertElement'),
 
   destroyEventListeners: function() {
     this.$('#collapseQueueAclPanelBtn').off('click');
     this.$('#collapseResourcesPanelBtn').off('click');
     this.$('#collapseQueueCapacityPanelBtn').off('click');
+    this.$('#collapseLabelCapacityPanelBtn').off('click');
+    this.$('#collapseQueuePreemptionPanelBtn').off('click');
   }.on('willDestroyElement')
 });