You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ambari.apache.org by on...@apache.org on 2014/02/07 14:52:33 UTC

git commit: AMBARI-4556. Refactor and Unit tests for host summary. (onechiporenko)

Updated Branches:
  refs/heads/trunk 556102801 -> e6e6a5ef7


AMBARI-4556. Refactor and Unit tests for host summary. (onechiporenko)


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

Branch: refs/heads/trunk
Commit: e6e6a5ef71858d9746745dbd8cbe3b09992418fe
Parents: 5561028
Author: Oleg Nechiporenko <on...@apache.org>
Authored: Fri Feb 7 15:47:56 2014 +0200
Committer: Oleg Nechiporenko <on...@apache.org>
Committed: Fri Feb 7 15:47:56 2014 +0200

----------------------------------------------------------------------
 ambari-web/app/app.js                           |   9 +
 ambari-web/app/assets/test/tests.js             |   2 +
 ambari-web/app/templates/main/host/summary.hbs  |   4 +-
 .../main/host/details/host_component_view.js    | 237 +++++++-----
 ambari-web/app/views/main/host/summary.js       | 192 ++++-----
 ambari-web/test/app_test.js                     |  10 +
 .../host/details/host_component_view_test.js    | 315 +++++++++++++++
 ambari-web/test/views/main/host/summary_test.js | 386 +++++++++++++++++++
 8 files changed, 956 insertions(+), 199 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/ambari/blob/e6e6a5ef/ambari-web/app/app.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/app.js b/ambari-web/app/app.js
index 06797f2..252edd5 100644
--- a/ambari-web/app/app.js
+++ b/ambari-web/app/app.js
@@ -73,11 +73,17 @@ module.exports = Em.Application.create({
     return !this.HostComponent.find().someProperty('componentName', 'SECONDARY_NAMENODE');
   }.property('router.clusterController.isLoaded'),
 
+  /**
+   * List of components with allowed action for them
+   * @type {Em.Object}
+   */
   components: Ember.Object.create({
     reassignable: ['NAMENODE', 'SECONDARY_NAMENODE', 'JOBTRACKER', 'RESOURCEMANAGER'],
     restartable: ['APP_TIMELINE_SERVER'],
     deletable: ['SUPERVISOR', 'HBASE_MASTER', 'DATANODE', 'TASKTRACKER', 'NODEMANAGER', 'HBASE_REGIONSERVER'],
     rollinRestartAllowed: ["DATANODE", "TASKTRACKER", "NODEMANAGER", "HBASE_REGIONSERVER", "SUPERVISOR"],
+    decommissionAllowed: ["DATANODE", "TASKTRACKER", "NODEMANAGER", "HBASE_REGIONSERVER"],
+    addableToHost: ["DATANODE", "TASKTRACKER", "NODEMANAGER", "HBASE_REGIONSERVER", "HBASE_MASTER", "ZOOKEEPER_SERVER", "SUPERVISOR"],
     slaves: function() {
       return require('data/service_components').filter(function(component){
         return !component.isClient && !component.isMaster
@@ -86,6 +92,9 @@ module.exports = Em.Application.create({
 
     masters: function() {
       return require('data/service_components').filterProperty('isMaster', true).mapProperty('component_name').uniq();
+    }.property().cacheable(),
+    clients: function() {
+      return require('data/service_components').filterProperty('isClient', true).mapProperty('component_name').uniq();
     }.property().cacheable()
   })
 });

http://git-wip-us.apache.org/repos/asf/ambari/blob/e6e6a5ef/ambari-web/app/assets/test/tests.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/assets/test/tests.js b/ambari-web/app/assets/test/tests.js
index 1550d0b..fd7cb29 100644
--- a/ambari-web/app/assets/test/tests.js
+++ b/ambari-web/app/assets/test/tests.js
@@ -98,6 +98,8 @@ require('test/views/main/dashboard/widgets/resource_manager_uptime_test');
 require('test/views/main/dashboard/widgets/links_widget_test');
 require('test/views/main/dashboard/widgets/pie_chart_widget_test');
 require('test/views/main/dashboard/widgets/namenode_cpu_test');
+require('test/views/main/host/summary_test');
+require('test/views/main/host/details/host_component_view_test');
 require('test/views/common/configs/services_config_test');
 require('test/views/wizard/step9_view_test');
 require('test/models/host_test');

http://git-wip-us.apache.org/repos/asf/ambari/blob/e6e6a5ef/ambari-web/app/templates/main/host/summary.hbs
----------------------------------------------------------------------
diff --git a/ambari-web/app/templates/main/host/summary.hbs b/ambari-web/app/templates/main/host/summary.hbs
index 4866c7f..de4fd8b 100644
--- a/ambari-web/app/templates/main/host/summary.hbs
+++ b/ambari-web/app/templates/main/host/summary.hbs
@@ -58,7 +58,7 @@
 
           {{#each component in view.sortedComponents}}
             <div class="row-fluid">
-              {{view App.HostComponentView contentBinding="component" decommissionDataNodeHostNamesBinding="view.decommissionDataNodeHostNames"}}
+              {{view App.HostComponentView contentBinding="component"}}
             </div>
           {{/each}}
         {{/if}}
@@ -77,7 +77,7 @@
             <div class="span5 row">
               {{#if App.isAdmin}}
                 <div class="btn-group pull-right">
-                  <button id="add_component" data-toggle="dropdown" {{bindAttr class="view.addComponentDisabled:disabled :btn :btn-info :dropdown-toggle"}}>
+                  <button id="add_component" data-toggle="dropdown" {{bindAttr class=":btn :btn-info :dropdown-toggle"}}>
                     {{t common.installed}}
                     <span class="caret pull-right"></span>
                   </button>

http://git-wip-us.apache.org/repos/asf/ambari/blob/e6e6a5ef/ambari-web/app/views/main/host/details/host_component_view.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/views/main/host/details/host_component_view.js b/ambari-web/app/views/main/host/details/host_component_view.js
index b515b18..20ae8de 100644
--- a/ambari-web/app/views/main/host/details/host_component_view.js
+++ b/ambari-web/app/views/main/host/details/host_component_view.js
@@ -20,30 +20,14 @@ var App = require('app');
 var uiEffects = require('utils/ui_effects');
 
 App.HostComponentView = Em.View.extend({
+
   templateName: require('templates/main/host/details/host_component'),
+
   /**
    * @type {App.HostComponent}
    */
   content: null,
-  didInsertElement: function () {
-    App.tooltip($('[rel=componentHealthTooltip]'));
-    App.tooltip($('[rel=passiveTooltip]'));
-    if (this.get('isInProgress')) {
-      this.doBlinking();
-    }
-    if (this.get('isDataNode')){
-      this.loadDataNodeDecommissionStatus();
-    }
-    if (this.get('isNodeManager')){
-      this.loadNodeManagerDecommissionStatus();
-    }
-    if (this.get('isTaskTracker')){
-      this.loadTaskTrackerDecommissionStatus();
-    }
-    if (this.get('isRegionServer')){
-      this.loadRegionServerDecommissionStatus();
-    }
-  },
+
   /**
    * @type {App.HostComponent}
    */
@@ -128,6 +112,10 @@ App.HostComponentView = Em.View.extend({
     return '';
   }.property('content.passiveState','parentView.content.passiveState'),
 
+  /**
+   * CSS-class for host component status
+   * @type {String}
+   */
   statusClass: function () {
     //If the component is DataNode
     if (this.get('isDataNode')) {
@@ -178,67 +166,57 @@ App.HostComponentView = Em.View.extend({
   }.property('content.passiveState','workStatus', 'isDataNodeRecommissionAvailable', 'isNodeManagerRecommissionAvailable', 'isTaskTrackerRecommissionAvailable', 'isRegionServerRecommissionAvailable'),
 
   /**
+   * CSS-class for disabling drop-down menu with list of host component actions
+   * Disabled if host's <code>healthClass</code> is health-status-DEAD-YELLOW (lost heartbeat)
    * @type {String}
    */
   disabled: function () {
     return (this.get('parentView.content.healthClass') === "health-status-DEAD-YELLOW") ? 'disabled' : '';
   }.property('parentView.content.healthClass'),
+
   /**
    * For Upgrade failed state
+   * @type {bool}
    */
   isUpgradeFailed: function () {
     return App.HostComponentStatus.getKeyName(this.get('workStatus')) === "upgrade_failed";
   }.property("workStatus"),
+
   /**
    * For Install failed state
+   * @type {bool}
    */
   isInstallFailed: function () {
     return App.HostComponentStatus.getKeyName(this.get('workStatus')) === "install_failed";
   }.property("workStatus"),
+
   /**
-   * Do blinking for 1 minute
-   */
-  doBlinking: function () {
-    var workStatus = this.get('workStatus');
-    var self = this;
-    var pulsate = [ App.HostComponentStatus.starting, App.HostComponentStatus.stopping, App.HostComponentStatus.installing].contains(workStatus);
-    if (!pulsate && (this.get('isDataNode') || this.get('isRegionServer') || this.get('isNodeManager') || this.get('isTaskTracker'))) {
-      var component = this.get('content');
-      if (component && workStatus != "INSTALLED") {
-        pulsate = this.get('isDecommissioning');
-      }
-    }
-    if (pulsate && !self.get('isBlinking')) {
-      self.set('isBlinking', true);
-      uiEffects.pulsate(self.$('.components-health'), 1000, function () {
-        self.set('isBlinking', false);
-        self.doBlinking();
-      });
-    }
-  },
-  /**
-   * Start blinking when host component is starting/stopping/decommissioning
+   * For Started and Starting states
+   * @type {bool}
    */
-  startBlinking: function () {
-    this.$('.components-health').stop(true, true);
-    this.$('.components-health').css({opacity: 1.0});
-    this.doBlinking();
-  }.observes('workStatus','isDataNodeRecommissionAvailable', 'isDecommissioning', 'isRegionServerRecommissionAvailable',
-      'isNodeManagerRecommissionAvailable', 'isTaskTrackerRecommissionAvailable'),
-
   isStart: function () {
     return (this.get('workStatus') == App.HostComponentStatus.started || this.get('workStatus') == App.HostComponentStatus.starting);
   }.property('workStatus'),
 
+  /**
+   * For Installed state
+   * @type {bool}
+   */
   isStop: function () {
     return (this.get('workStatus') == App.HostComponentStatus.stopped);
   }.property('workStatus'),
 
+  /**
+   * For Installing state
+   * @type {bool}
+   */
   isInstalling: function () {
     return (this.get('workStatus') == App.HostComponentStatus.installing);
   }.property('workStatus'),
+
   /**
    * No action available while component is starting/stopping/unknown
+   * @type {String}
    */
   noActionAvailable: function () {
     var workStatus = this.get('workStatus');
@@ -249,38 +227,148 @@ App.HostComponentView = Em.View.extend({
     }
   }.property('workStatus'),
 
+  /**
+   * For Stopping or Starting states, also for decommissioning
+   * @type {bool}
+   */
   isInProgress: function () {
     return (this.get('workStatus') === App.HostComponentStatus.stopping ||
       this.get('workStatus') === App.HostComponentStatus.starting) ||
       this.get('isDecommissioning');
   }.property('workStatus', 'isDecommissioning'),
 
+  /**
+   * For ACTIVE <code>passiveState</code> of host component
+   * @type {bool}
+   */
+  isActive: function () {
+    return (this.get('content.passiveState') == "ACTIVE");
+  }.property('content.passiveState'),
+
+  /**
+   * For PASSIVE <code>passiveState</code> of host or service
+   * @type {bool}
+   */
+  isImplied: function() {
+    return (this.get('parentView.content.passiveState') === 'PASSIVE' || this.get('content.service.passiveState') === 'PASSIVE');
+  }.property('parentView.content.passiveState', 'content.service.passiveState'),
+
+  /**
+   *
+   * @type {bool}
+   */
+  isDecommissioning: function () {
+    return ( (this.get('isDataNode') && this.get("isDataNodeDecommissioning")) || (this.get('isRegionServer') && this.get("isRegionServerDecommissioning"))
+      || (this.get('isNodeManager') && this.get("isNodeManagerDecommissioning")) || (this.get('isTaskTracker') && this.get('isTaskTrackerDecommissioning')));
+  }.property("workStatus", "isDataNodeDecommissioning", "isRegionServerDecommissioning", "isNodeManagerDecommissioning", "isTaskTrackerDecommissioning"),
+
+  /**
+   * Shows whether we need to show Delete button
+   * @type {bool}
+   */
+  isDeletableComponent: function () {
+    return App.get('components.deletable').contains(this.get('content.componentName'));
+  }.property('content'),
+
+  /**
+   * Host component with some <code>workStatus</code> can't be deleted (so, disable such action in the dropdown list)
+   * @type {bool}
+   */
+  isDeleteComponentDisabled: function () {
+    return ![App.HostComponentStatus.stopped, App.HostComponentStatus.unknown, App.HostComponentStatus.install_failed, App.HostComponentStatus.upgrade_failed].contains(this.get('workStatus'));
+  }.property('workStatus'),
+
+  /**
+   * Check if component may be reassinged to another host
+   * @type {bool}
+   */
+  isReassignable: function () {
+    return App.supports.reassignMaster && App.get('components.reassignable').contains(this.get('content.componentName')) && App.Host.find().content.length > 1;
+  }.property('content.componentName'),
+
+  /**
+   * Check if component is restartable
+   * @type {bool}
+   */
+  isRestartableComponent: function() {
+    return App.get('components.restartable').contains(this.get('content.componentName'));
+  }.property('content'),
+
+  /**
+   * Host component with some <code>workStatus</code> can't be restarted (so, disable such action in the dropdown list)
+   * @type {bool}
+   */
+  isRestartComponentDisabled: function() {
+    return ![App.HostComponentStatus.started].contains(this.get('workStatus'));
+  }.property('workStatus'),
+
+  didInsertElement: function () {
+    App.tooltip($('[rel=componentHealthTooltip]'));
+    App.tooltip($('[rel=passiveTooltip]'));
+    if (this.get('isInProgress')) {
+      this.doBlinking();
+    }
+    if (this.get('isDataNode')){
+      this.loadDataNodeDecommissionStatus();
+    }
+    if (this.get('isNodeManager')){
+      this.loadNodeManagerDecommissionStatus();
+    }
+    if (this.get('isTaskTracker')){
+      this.loadTaskTrackerDecommissionStatus();
+    }
+    if (this.get('isRegionServer')){
+      this.loadRegionServerDecommissionStatus();
+    }
+  },
+
+  /**
+   * Do blinking for 1 minute
+   */
+  doBlinking: function () {
+    var workStatus = this.get('workStatus');
+    var self = this;
+    var pulsate = [ App.HostComponentStatus.starting, App.HostComponentStatus.stopping, App.HostComponentStatus.installing].contains(workStatus);
+    if (!pulsate && (this.get('isDataNode') || this.get('isRegionServer') || this.get('isNodeManager') || this.get('isTaskTracker'))) {
+      var component = this.get('content');
+      if (component && workStatus != "INSTALLED") {
+        pulsate = this.get('isDecommissioning');
+      }
+    }
+    if (pulsate && !self.get('isBlinking')) {
+      self.set('isBlinking', true);
+      uiEffects.pulsate(self.$('.components-health'), 1000, function () {
+        self.set('isBlinking', false);
+        self.doBlinking();
+      });
+    }
+  },
+  /**
+   * Start blinking when host component is starting/stopping/decommissioning
+   */
+  startBlinking: function () {
+    this.$('.components-health').stop(true, true);
+    this.$('.components-health').css({opacity: 1.0});
+    this.doBlinking();
+  }.observes('workStatus','isDataNodeRecommissionAvailable', 'isDecommissioning', 'isRegionServerRecommissionAvailable',
+      'isNodeManagerRecommissionAvailable', 'isTaskTrackerRecommissionAvailable'),
+
   isDataNode: function () {
     return this.get('content.componentName') === 'DATANODE';
   }.property('content'),
+
   isNodeManager: function () {
     return this.get('content.componentName') === 'NODEMANAGER';
   }.property('content'),
+
   isTaskTracker: function () {
     return this.get('content.componentName') === 'TASKTRACKER';
   }.property('content'),
+
   isRegionServer: function () {
     return this.get('content.componentName') === 'HBASE_REGIONSERVER';
   }.property('content'),
 
-  isActive: function () {
-    return (this.get('content.passiveState') == "ACTIVE");
-  }.property('content.passiveState'),
-
-  isImplied: function() {
-    return (this.get('parentView.content.passiveState') === 'PASSIVE' || this.get('content.service.passiveState') === 'PASSIVE');
-  }.property('content.passiveState'),
-
-  isDecommissioning: function () {
-    return ( (this.get('isDataNode') && this.get("isDataNodeDecommissioning")) || (this.get('isRegionServer') && this.get("isRegionServerDecommissioning"))
-      || (this.get('isNodeManager') && this.get("isNodeManagerDecommissioning")) || (this.get('isTaskTracker') && this.get('isTaskTrackerDecommissioning')));
-  }.property("workStatus", "isDataNodeDecommissioning", "isRegionServerDecommissioning", "isNodeManagerDecommissioning", "isTaskTrackerDecommissioning"),
-
   isDataNodeDecommissioning: null,
   isDataNodeDecommissionAvailable: null,
   isDataNodeRecommissionAvailable: null,
@@ -620,31 +708,6 @@ App.HostComponentView = Em.View.extend({
       deferred.resolve(desired_admin_state);
     });
     return deferred.promise();
-  }.observes('App.router.mainHostDetailsController.content'),
-
-  /**
-   * Shows whether we need to show Delete button
-   */
-  isDeletableComponent: function () {
-    return App.get('components.deletable').contains(this.get('content.componentName'));
-  }.property('content'),
-
-  isDeleteComponentDisabled: function () {
-    return !(this.get('workStatus') == App.HostComponentStatus.stopped || this.get('workStatus') == App.HostComponentStatus.unknown ||
-      this.get('workStatus') == App.HostComponentStatus.install_failed || this.get('workStatus') == App.HostComponentStatus.upgrade_failed);
-  }.property('workStatus'),
-
-  isReassignable: function () {
-    return App.supports.reassignMaster && App.get('components.reassignable').contains(this.get('content.componentName')) && App.Host.find().content.length > 1;
-  }.property('content.componentName'),
-
-  isRestartableComponent: function() {
-    return App.get('components.restartable').contains(this.get('content.componentName'));
-  }.property('content'),
-
-  isRestartComponentDisabled: function() {
-    var allowableStates = [App.HostComponentStatus.started];
-    return !allowableStates.contains(this.get('workStatus'));
-  }.property('workStatus')
+  }.observes('App.router.mainHostDetailsController.content')
 
 });

http://git-wip-us.apache.org/repos/asf/ambari/blob/e6e6a5ef/ambari-web/app/views/main/host/summary.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/views/main/host/summary.js b/ambari-web/app/views/main/host/summary.js
index 4c8903f..520b4c7 100644
--- a/ambari-web/app/views/main/host/summary.js
+++ b/ambari-web/app/views/main/host/summary.js
@@ -17,13 +17,19 @@
  */
 
 var App = require('app');
-var uiEffects = require('utils/ui_effects');
 
 App.MainHostSummaryView = Em.View.extend({
+
   templateName: require('templates/main/host/summary'),
 
+  /**
+   * @type {bool}
+   */
   isStopCommand:true,
 
+  /**
+   * @type {App.Host}
+   */
   content: function () {
     return App.router.get('mainHostDetailsController.content');
   }.property('App.router.mainHostDetailsController.content'),
@@ -34,6 +40,10 @@ App.MainHostSummaryView = Em.View.extend({
     window.open(gangliaMobileUrl);
   },
 
+  /**
+   * Message for "restart" block
+   * @type {String}
+   */
   needToRestartMessage: function() {
     var componentsCount, word;
     componentsCount = this.get('content.componentsWithStaleConfigsCount');
@@ -48,11 +58,29 @@ App.MainHostSummaryView = Em.View.extend({
   didInsertElement: function () {
     this.addToolTip();
   },
+
+  /**
+   * Create tooltip for "Add" button if nothing to add to the current host
+   */
   addToolTip: function() {
     if (this.get('addComponentDisabled')) {
       App.tooltip($('#add_component'), {title: Em.I18n.t('services.nothingToAdd')});
     }
   }.observes('addComponentDisabled'),
+
+  /**
+   * List of installed services
+   * @type {String[]}
+   */
+  installedServices: function() {
+    return App.Service.find().mapProperty('serviceName');
+  }.property('App.Service.@each'),
+
+  /**
+   * List of installed masters and slaves
+   * Masters first, then slaves
+   * @type {DS.Model[]}
+   */
   sortedComponents: function () {
     var slaveComponents = [];
     var masterComponents = [];
@@ -64,7 +92,12 @@ App.MainHostSummaryView = Em.View.extend({
       }
     }, this);
     return masterComponents.concat(slaveComponents);
-  }.property('content', 'content.hostComponents.length'),
+  }.property('content.hostComponents.length'),
+
+  /**
+   * List of installed clients
+   * @type {DS.Model[]}
+   */
   clients: function () {
     var clients = [];
     this.get('content.hostComponents').forEach(function (component) {
@@ -93,7 +126,10 @@ App.MainHostSummaryView = Em.View.extend({
     }).length;
   }.property('clients.@each.staleConfigs'),
 
-
+  /**
+   * Template for addable component
+   * @type {Em.Object}
+   */
   addableComponentObject: Em.Object.extend({
     componentName: '',
     subComponentNames: null,
@@ -104,134 +140,70 @@ App.MainHostSummaryView = Em.View.extend({
       return App.format.role(this.get('componentName'));
     }.property('componentName')
   }),
+
+  /**
+   * If host lost heartbeat, components can't be added on it
+   * @type {bool}
+   */
   isAddComponent: function () {
     return this.get('content.healthClass') !== 'health-status-DEAD-YELLOW';
   }.property('content.healthClass'),
 
+  /**
+   * Disable "Add" button if components can't be added to the current host
+   * @type {bool}
+   */
   addComponentDisabled: function() {
     return (!this.get('isAddComponent')) || (this.get('addableComponents.length') == 0);
   }.property('isAddComponent', 'addableComponents.length'),
 
+  /**
+   * List of client's that may be installed to the current host
+   * @type {String[]}
+   */
   installableClientComponents: function() {
-    var installableClients = [];
     if (!App.supports.deleteHost) {
-      return installableClients;
+      return [];
     }
-    App.Service.find().forEach(function(svc){
-      switch(svc.get('serviceName')){
-        case 'PIG':
-          installableClients.push('PIG');
-          break;
-        case 'SQOOP':
-          installableClients.push('SQOOP');
-          break;
-        case 'HCATALOG':
-          installableClients.push('HCAT');
-          break;
-        case 'HDFS':
-          installableClients.push('HDFS_CLIENT');
-          break;
-        case 'OOZIE':
-          installableClients.push('OOZIE_CLIENT');
-          break;
-        case 'ZOOKEEPER':
-          installableClients.push('ZOOKEEPER_CLIENT');
-          break;
-        case 'HIVE':
-          installableClients.push('HIVE_CLIENT');
-          break;
-        case 'HBASE':
-          installableClients.push('HBASE_CLIENT');
-          break;
-        case 'YARN':
-          installableClients.push('YARN_CLIENT');
-          break;
-        case 'MAPREDUCE':
-          installableClients.push('MAPREDUCE_CLIENT');
-          break;
-        case 'MAPREDUCE2':
-          installableClients.push('MAPREDUCE2_CLIENT');
-          break;
-        case 'TEZ':
-          installableClients.push('TEZ_CLIENT');
-          break;
-      }
+    var componentServiceMap = App.QuickDataMapper.componentServiceMap();
+    var allClients = App.get('components.clients');
+    var installedServices = this.get('installedServices');
+    var installedClients = this.get('clients').mapProperty('componentName');
+    return allClients.filter(function(componentName) {
+      // service for current client is installed but client isn't installed on current host
+      return installedServices.contains(componentServiceMap[componentName]) && !installedClients.contains(componentName);
     });
-    this.get('content.hostComponents').forEach(function (component) {
-      var index = installableClients.indexOf(component.get('componentName'));
-      if (index > -1) {
-        installableClients.splice(index, 1);
-      }
-    }, this);
-    return installableClients;
-  }.property('content', 'content.hostComponents.length', 'App.Service', 'App.supports.deleteHost'),
-  
+  }.property('content.hostComponents.length', 'installedServices.length', 'App.supports.deleteHost'),
+
+  /**
+   * List of components that may be added to the current host
+   * @type {Em.Object[]}
+   */
   addableComponents: function () {
     var components = [];
-    var services = App.Service.find();
-    var dataNodeExists = false;
-    var taskTrackerExists = false;
-    var regionServerExists = false;
-    var zookeeperServerExists = false;
-    var nodeManagerExists = false;
-    var hbaseMasterExists = false;
-    var supervisorExists = false;
-    
+    var self = this;
     var installableClients = this.get('installableClientComponents');
-    
-    this.get('content.hostComponents').forEach(function (component) {
-      switch (component.get('componentName')) {
-        case 'DATANODE':
-          dataNodeExists = true;
-          break;
-        case 'TASKTRACKER':
-          taskTrackerExists = true;
-          break;
-        case 'HBASE_REGIONSERVER':
-          regionServerExists = true;
-          break;
-        case 'ZOOKEEPER_SERVER':
-          zookeeperServerExists = true;
-          break;
-        case 'NODEMANAGER':
-          nodeManagerExists = true;
-          break;
-        case 'HBASE_MASTER':
-          hbaseMasterExists = true;
-          break;
-        case 'SUPERVISOR':
-          supervisorExists = true;
-          break;
-      }
-    }, this);
+    var installedComponents = this.get('content.hostComponents').mapProperty('componentName');
+    var addableToHostComponents = App.get('components.addableToHost');
+    var installedServices = this.get('installedServices');
+    var componentServiceMap = App.QuickDataMapper.componentServiceMap();
 
-    if (!dataNodeExists) {
-      components.pushObject(this.addableComponentObject.create({ 'componentName': 'DATANODE' }));
-    }
-    if (!taskTrackerExists && services.findProperty('serviceName', 'MAPREDUCE')) {
-      components.pushObject(this.addableComponentObject.create({ 'componentName': 'TASKTRACKER' }));
-    }
-    if (!regionServerExists && services.findProperty('serviceName', 'HBASE')) {
-      components.pushObject(this.addableComponentObject.create({ 'componentName': 'HBASE_REGIONSERVER' }));
-    }
-    if (!hbaseMasterExists && services.findProperty('serviceName', 'HBASE')) {
-      components.pushObject(this.addableComponentObject.create({ 'componentName': 'HBASE_MASTER' }));
-    }
-    if (!zookeeperServerExists && services.findProperty('serviceName', 'ZOOKEEPER')) {
-      components.pushObject(this.addableComponentObject.create({ 'componentName': 'ZOOKEEPER_SERVER' }));
-    }
-    if (!nodeManagerExists && services.findProperty('serviceName', 'YARN')) {
-      components.pushObject(this.addableComponentObject.create({ 'componentName': 'NODEMANAGER' }));
-    }
-    if (!supervisorExists && services.findProperty('serviceName', 'STORM')) {
-      components.pushObject(this.addableComponentObject.create({ 'componentName': 'SUPERVISOR' }));
-    }
+    addableToHostComponents.forEach(function(addableComponent) {
+      if(installedServices.contains(componentServiceMap[addableComponent]) && !installedComponents.contains(addableComponent)) {
+        components.pushObject(self.addableComponentObject.create({'componentName': addableComponent}));
+      }
+    });
     if (installableClients.length > 0) {
       components.pushObject(this.addableComponentObject.create({ 'componentName': 'CLIENTS', subComponentNames: installableClients }));
     }
+
     return components;
-  }.property('content', 'content.hostComponents.length', 'installableClientComponents'),
+  }.property('content.hostComponents.length', 'installableClientComponents'),
 
+  /**
+   * Formatted with <code>$.timeago</code> value of host's last heartbeat
+   * @type {String}
+   */
   timeSinceHeartBeat: function () {
     var d = this.get('content.lastHeartBeatTime');
     if (d) {

http://git-wip-us.apache.org/repos/asf/ambari/blob/e6e6a5ef/ambari-web/test/app_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/app_test.js b/ambari-web/test/app_test.js
index 59ec825..cc3155c 100644
--- a/ambari-web/test/app_test.js
+++ b/ambari-web/test/app_test.js
@@ -19,10 +19,20 @@
 var App = require('app');
 
 describe('#App.components', function() {
+
   it('slaves and masters should not intersect', function() {
     var intersected = App.get('components.slaves').filter(function(item){
       return App.get('components.masters').contains(item);
     });
     expect(intersected).to.eql([]);
   });
+
+  it('decommissionAllowed', function() {
+    expect(App.get('components.decommissionAllowed')).to.eql(["DATANODE", "TASKTRACKER", "NODEMANAGER", "HBASE_REGIONSERVER"]);
+  });
+
+  it('addableToHost', function() {
+    expect(App.get('components.addableToHost')).to.eql(["DATANODE", "TASKTRACKER", "NODEMANAGER", "HBASE_REGIONSERVER", "HBASE_MASTER", "ZOOKEEPER_SERVER", "SUPERVISOR"]);
+  });
+
 });

http://git-wip-us.apache.org/repos/asf/ambari/blob/e6e6a5ef/ambari-web/test/views/main/host/details/host_component_view_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/views/main/host/details/host_component_view_test.js b/ambari-web/test/views/main/host/details/host_component_view_test.js
new file mode 100644
index 0000000..bd30686
--- /dev/null
+++ b/ambari-web/test/views/main/host/details/host_component_view_test.js
@@ -0,0 +1,315 @@
+/**
+ * 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.
+ */
+
+var App = require('app');
+require('models/host_component');
+require('views/main/host/details/host_component_view');
+
+var hostComponentView;
+
+describe('App.HostComponentView', function() {
+
+  beforeEach(function() {
+    hostComponentView = App.HostComponentView.create({
+      startBlinking: function(){},
+      doBlinking: function(){}
+    });
+  });
+
+  describe('#componentTextStatus', function() {
+
+    var tests = Em.A([
+      {
+        content: Em.Object.create({passiveState: 'PASSIVE'}),
+        m: 'PASSIVE state',
+        e: Em.I18n.t('hosts.component.passive.short.mode')
+      },
+      {
+        content: Em.Object.create({passiveState: 'IMPLIED'}),
+        m: 'IMPLIED state',
+        e: Em.I18n.t('hosts.component.passive.short.mode')
+      }
+    ]);
+
+    tests.forEach(function(test) {
+      it(test.m, function() {
+        hostComponentView.set('content', test.content);
+        expect(hostComponentView.get('componentTextStatus')).to.equal(test.e);
+      });
+    });
+
+  });
+
+  describe('#passiveImpliedTextStatus', function() {
+
+    var tests = Em.A([
+      {
+        content: {service: {passiveState: 'PASSIVE'}},
+        parentView: {content: {passiveState: 'PASSIVE'}},
+        m: 'service in PASSIVE, host in PASSIVE',
+        e: Em.I18n.t('hosts.component.passive.implied.host.mode.tooltip')
+      },
+      {
+        content: {service: {passiveState: 'PASSIVE', serviceName:'SERVICE_NAME'}},
+        parentView: {content: {passiveState: 'ACTIVE'}},
+        m: 'service in PASSIVE, host in ACTIVE',
+        e: Em.I18n.t('hosts.component.passive.implied.service.mode.tooltip').format('SERVICE_NAME')
+      },
+      {
+        content: {service: {passiveState: 'ACTIVE'}},
+        parentView: {content: {passiveState: 'ACTIVE'}},
+        m: 'service in ACTIVE, host in ACTIVE',
+        e: ''
+      }
+    ]);
+
+    tests.forEach(function(test) {
+      it(test.m, function() {
+        hostComponentView = App.HostComponentView.create({
+          startBlinking: function(){},
+          doBlinking: function(){},
+          parentView: test.parentView,
+          content: test.content
+        });
+        expect(hostComponentView.get('passiveImpliedTextStatus')).to.equal(test.e);
+      });
+    });
+
+  });
+
+  describe('#disabled', function() {
+
+    var tests = Em.A([
+      {
+        parentView: {content: {healthClass: 'health-status-DEAD-YELLOW'}},
+        e: 'disabled'
+      },
+      {
+        parentView: {content: {healthClass: 'another-class'}},
+        e: ''
+      }
+    ]);
+
+    tests.forEach(function(test) {
+      it(test.m, function() {
+        hostComponentView = App.HostComponentView.create({
+          startBlinking: function(){},
+          doBlinking: function(){},
+          parentView: test.parentView
+        });
+        expect(hostComponentView.get('disabled')).to.equal(test.e);
+      });
+    });
+
+  });
+
+  describe('#isUpgradeFailed', function() {
+
+    var tests = Em.A([
+      {workStatus: 'UPGRADE_FAILED', e: true},
+      {workStatus: 'OTHER_STATUS', e: false}
+    ]);
+
+    tests.forEach(function(test) {
+      it(test.workStatus, function() {
+        hostComponentView.set('content', {workStatus: test.workStatus});
+        expect(hostComponentView.get('isUpgradeFailed')).to.equal(test.e);
+      });
+    });
+
+  });
+
+  describe('#isInstallFailed', function() {
+
+    var tests = Em.A([
+      {workStatus: 'INSTALL_FAILED', e: true},
+      {workStatus: 'OTHER_STATUS', e: false}
+    ]);
+
+    tests.forEach(function(test) {
+      it(test.workStatus, function() {
+        hostComponentView.set('content', {workStatus: test.workStatus});
+        expect(hostComponentView.get('isInstallFailed')).to.equal(test.e);
+      });
+    });
+
+  });
+
+  describe('#isStart', function() {
+
+    var tests = Em.A([
+      {workStatus: 'STARTED', e: true},
+      {workStatus: 'STARTING', e: true},
+      {workStatus: 'OTHER_STATUS', e: false}
+    ]);
+
+    tests.forEach(function(test) {
+      it(test.workStatus, function() {
+        hostComponentView.set('content', {workStatus: test.workStatus});
+        expect(hostComponentView.get('isStart')).to.equal(test.e);
+      });
+    });
+
+  });
+
+  describe('#isStop', function() {
+
+    var tests = Em.A([
+      {workStatus: 'INSTALLED', e: true},
+      {workStatus: 'OTHER_STATUS', e: false}
+    ]);
+
+    tests.forEach(function(test) {
+      it(test.workStatus, function() {
+        hostComponentView.set('content', {workStatus: test.workStatus});
+        expect(hostComponentView.get('isStop')).to.equal(test.e);
+      });
+    });
+
+  });
+
+  describe('#isInstalling', function() {
+
+    var tests = Em.A([
+      {workStatus: 'INSTALLING', e: true},
+      {workStatus: 'OTHER_STATUS', e: false}
+    ]);
+
+    tests.forEach(function(test) {
+      it(test.workStatus, function() {
+        hostComponentView.set('content', {workStatus: test.workStatus});
+        expect(hostComponentView.get('isInstalling')).to.equal(test.e);
+      });
+    });
+
+  });
+
+  describe('#noActionAvailable', function() {
+
+    var tests = Em.A([
+      {workStatus: 'STARTING', e: 'hidden'},
+      {workStatus: 'STOPPING', e: 'hidden'},
+      {workStatus: 'UNKNOWN', e: 'hidden'},
+      {workStatus: 'OTHER_STATUS', e: ''}
+    ]);
+
+    tests.forEach(function(test) {
+      it(test.workStatus, function() {
+        hostComponentView.set('content', {workStatus: test.workStatus});
+        expect(hostComponentView.get('noActionAvailable')).to.equal(test.e);
+      });
+    });
+
+  });
+
+  describe('#isActive', function() {
+
+    var tests = Em.A([
+      {passiveState: 'ACTIVE', e: true},
+      {passiveState: 'PASSIVE', e: false},
+      {passiveState: 'IMPLIED', e: false}
+    ]);
+
+    tests.forEach(function(test) {
+      it(test.workStatus, function() {
+        hostComponentView.set('content', {passiveState: test.passiveState});
+        expect(hostComponentView.get('isActive')).to.equal(test.e);
+      });
+    });
+
+  });
+
+  describe('#isImplied', function() {
+
+    var tests = Em.A([
+      {
+        content: {service: {passiveState: 'PASSIVE'}},
+        parentView: {content: {passiveState: 'PASSIVE'}},
+        m: 'service in PASSIVE, host in PASSIVE',
+        e: true
+      },
+      {
+        content: {service: {passiveState: 'PASSIVE', serviceName:'SERVICE_NAME'}},
+        parentView: {content: {passiveState: 'ACTIVE'}},
+        m: 'service in PASSIVE, host in ACTIVE',
+        e: true
+      },
+      {
+        content: {service: {passiveState: 'ACTIVE', serviceName:'SERVICE_NAME'}},
+        parentView: {content: {passiveState: 'PASSIVE'}},
+        m: 'service in ACTIVE, host in PASSIVE',
+        e: true
+      },
+      {
+        content: {service: {passiveState: 'ACTIVE'}},
+        parentView: {content: {passiveState: 'ACTIVE'}},
+        m: 'service in ACTIVE, host in ACTIVE',
+        e: false
+      }
+    ]);
+
+    tests.forEach(function(test) {
+      it(test.m, function() {
+        hostComponentView = App.HostComponentView.create({
+          startBlinking: function(){},
+          doBlinking: function(){},
+          parentView: test.parentView,
+          content: test.content
+        });
+        expect(hostComponentView.get('isImplied')).to.equal(test.e);
+      });
+    });
+
+  });
+
+  describe('#isRestartComponentDisabled', function() {
+
+    var tests = Em.A([
+      {workStatus: 'STARTED', e: false},
+      {workStatus: 'OTHER_STATUS', e: true}
+    ]);
+
+    tests.forEach(function(test) {
+      it(test.workStatus, function() {
+        hostComponentView.set('content', {workStatus: test.workStatus});
+        expect(hostComponentView.get('isRestartComponentDisabled')).to.equal(test.e);
+      });
+    });
+
+  });
+
+  describe('#isDeleteComponentDisabled', function() {
+
+    var tests = Em.A([
+      {workStatus: 'INSTALLED', e: false},
+      {workStatus: 'UNKNOWN', e: false},
+      {workStatus: 'INSTALL_FAILED', e: false},
+      {workStatus: 'UPGRADE_FAILED', e: false},
+      {workStatus: 'OTHER_STATUS', e: true}
+    ]);
+
+    tests.forEach(function(test) {
+      it(test.workStatus, function() {
+        hostComponentView.set('content', {workStatus: test.workStatus});
+        expect(hostComponentView.get('isDeleteComponentDisabled')).to.equal(test.e);
+      });
+    });
+
+  });
+
+});

http://git-wip-us.apache.org/repos/asf/ambari/blob/e6e6a5ef/ambari-web/test/views/main/host/summary_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/views/main/host/summary_test.js b/ambari-web/test/views/main/host/summary_test.js
new file mode 100644
index 0000000..588a2b6
--- /dev/null
+++ b/ambari-web/test/views/main/host/summary_test.js
@@ -0,0 +1,386 @@
+/**
+ * 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.
+ */
+
+var App = require('app');
+require('models/host');
+require('models/service');
+require('models/host_component');
+require('mappers/server_data_mapper');
+require('views/main/host/summary');
+
+var mainHostSummaryView;
+var extendedMainHostSummaryView = App.MainHostSummaryView.extend({content: {}, addToolTip: function(){}, installedServices: []});
+
+describe('App.MainHostSummaryView', function() {
+
+  beforeEach(function() {
+    mainHostSummaryView = extendedMainHostSummaryView.create({});
+  });
+
+  describe('#sortedComponents', function() {
+
+    var tests = Em.A([
+      {
+        content: Em.Object.create({
+          hostComponents: Em.A([
+            Em.Object.create({isMaster: false, isSlave: true, componentName: 'B'}),
+            Em.Object.create({isMaster: true, isSlave: false, componentName: 'A'}),
+            Em.Object.create({isMaster: true, isSlave: false, componentName: 'C'}),
+            Em.Object.create({isMaster: false, isSlave: false, componentName: 'D'})
+          ])
+        }),
+        m: 'List of masters, slaves and clients',
+        e: ['A', 'C', 'B']
+      },
+      {
+        content: Em.Object.create({
+          hostComponents: Em.A([
+            Em.Object.create({isMaster: false, isSlave: true, componentName: 'B'}),
+            Em.Object.create({isMaster: true, isSlave: false, componentName: 'A'}),
+            Em.Object.create({isMaster: true, isSlave: false, componentName: 'C'}),
+            Em.Object.create({isMaster: true, isSlave: false, componentName: 'D'})
+          ])
+        }),
+        m: 'List of masters and slaves',
+        e: ['A', 'C', 'D', 'B']
+      },
+      {
+        content: Em.Object.create({
+          hostComponents: Em.A([
+            Em.Object.create({isMaster: true, isSlave: false, componentName: 'B'}),
+            Em.Object.create({isMaster: true, isSlave: false, componentName: 'A'}),
+            Em.Object.create({isMaster: true, isSlave: false, componentName: 'C'}),
+            Em.Object.create({isMaster: true, isSlave: false, componentName: 'D'})
+          ])
+        }),
+        m: 'List of masters',
+        e: ['B', 'A', 'C', 'D']
+      },
+      {
+        content: Em.Object.create({
+          hostComponents: Em.A([
+            Em.Object.create({isMaster: false, isSlave: true, componentName: 'B'}),
+            Em.Object.create({isMaster: false, isSlave: true, componentName: 'A'}),
+            Em.Object.create({isMaster: false, isSlave: true, componentName: 'C'}),
+            Em.Object.create({isMaster: false, isSlave: true, componentName: 'D'})
+          ])
+        }),
+        m: 'List of slaves',
+        e: ['B', 'A', 'C', 'D']
+      },
+      {
+        content: Em.Object.create({
+          hostComponents: Em.A([])
+        }),
+        m: 'Empty list',
+        e: []
+      },
+      {
+        content: Em.Object.create({
+          hostComponents: Em.A([
+            Em.Object.create({isMaster: false, isSlave: false, componentName: 'B'}),
+            Em.Object.create({isMaster: false, isSlave: false, componentName: 'A'}),
+            Em.Object.create({isMaster: false, isSlave: false, componentName: 'C'}),
+            Em.Object.create({isMaster: false, isSlave: false, componentName: 'D'})
+          ])
+        }),
+        m: 'List of clients',
+        e: []
+      }
+    ]);
+
+    tests.forEach(function(test) {
+      it(test.m, function() {
+        mainHostSummaryView.set('content', test.content);
+        expect(mainHostSummaryView.get('sortedComponents').mapProperty('componentName')).to.eql(test.e);
+      });
+    });
+
+  });
+
+  describe('#clients', function() {
+
+    var tests = Em.A([
+      {
+        content: Em.Object.create({
+          hostComponents: Em.A([
+            Em.Object.create({isMaster: false, isSlave: true, componentName: 'B'}),
+            Em.Object.create({isMaster: true, isSlave: false, componentName: 'A'}),
+            Em.Object.create({isMaster: true, isSlave: false, componentName: 'C'}),
+            Em.Object.create({isMaster: false, isSlave: false, componentName: 'D'})
+          ])
+        }),
+        m: 'List of masters, slaves and clients',
+        e: ['D']
+      },
+      {
+        content: Em.Object.create({
+          hostComponents: Em.A([
+            Em.Object.create({isMaster: false, isSlave: true, componentName: 'B'}),
+            Em.Object.create({isMaster: true, isSlave: false, componentName: 'A'}),
+            Em.Object.create({isMaster: true, isSlave: false, componentName: 'C'}),
+            Em.Object.create({isMaster: true, isSlave: false, componentName: 'D'})
+          ])
+        }),
+        m: 'List of masters and slaves',
+        e: []
+      },
+      {
+        content: Em.Object.create({
+          hostComponents: Em.A([
+            Em.Object.create({isMaster: true, isSlave: false, componentName: 'B'}),
+            Em.Object.create({isMaster: true, isSlave: false, componentName: 'A'}),
+            Em.Object.create({isMaster: true, isSlave: false, componentName: 'C'}),
+            Em.Object.create({isMaster: true, isSlave: false, componentName: 'D'})
+          ])
+        }),
+        m: 'List of masters',
+        e: []
+      },
+      {
+        content: Em.Object.create({
+          hostComponents: Em.A([
+            Em.Object.create({isMaster: false, isSlave: true, componentName: 'B'}),
+            Em.Object.create({isMaster: false, isSlave: true, componentName: 'A'}),
+            Em.Object.create({isMaster: false, isSlave: true, componentName: 'C'}),
+            Em.Object.create({isMaster: false, isSlave: true, componentName: 'D'})
+          ])
+        }),
+        m: 'List of slaves',
+        e: []
+      },
+      {
+        content: Em.Object.create({
+          hostComponents: Em.A([])
+        }),
+        m: 'Empty list',
+        e: []
+      },
+      {
+        content: Em.Object.create({
+          hostComponents: Em.A([
+            Em.Object.create({isMaster: false, isSlave: false, componentName: 'B'}),
+            Em.Object.create({isMaster: false, isSlave: false, componentName: 'A'}),
+            Em.Object.create({isMaster: false, isSlave: false, componentName: 'C'}),
+            Em.Object.create({isMaster: false, isSlave: false, componentName: 'D'})
+          ])
+        }),
+        m: 'List of clients',
+        e: ['B', 'A', 'C', 'D']
+      }
+    ]);
+
+    tests.forEach(function(test) {
+      it(test.m, function() {
+        mainHostSummaryView.set('content', test.content);
+        expect(mainHostSummaryView.get('clients').mapProperty('componentName')).to.eql(test.e);
+      });
+    });
+
+  });
+
+  describe('#areClientWithStaleConfigs', function() {
+
+    var tests = Em.A([
+      {
+        content: Em.Object.create({
+          hostComponents: Em.A([
+            Em.Object.create({isMaster: false, isSlave: false, componentName: 'D', staleConfigs: true}),
+            Em.Object.create({isMaster: false, isSlave: false, componentName: 'C', staleConfigs: false})
+          ])
+        }),
+        m: 'Some clients with stale configs',
+        e: true
+      },
+      {
+        content: Em.Object.create({
+          hostComponents: Em.A([
+            Em.Object.create({isMaster: false, isSlave: false, componentName: 'D', staleConfigs: false}),
+            Em.Object.create({isMaster: false, isSlave: false, componentName: 'C', staleConfigs: false})
+          ])
+        }),
+        m: 'No clients with stale configs',
+        e: false
+      },
+      {
+        content: Em.Object.create({
+          hostComponents: Em.A([
+            Em.Object.create({isMaster: false, isSlave: false, componentName: 'D', staleConfigs: true}),
+            Em.Object.create({isMaster: false, isSlave: false, componentName: 'C', staleConfigs: true})
+          ])
+        }),
+        m: 'All clients with stale configs',
+        e: true
+      },
+      {
+        content: Em.Object.create({
+          hostComponents: Em.A([])
+        }),
+        m: 'Empty list',
+        e: false
+      }
+    ]);
+
+    tests.forEach(function(test) {
+      it(test.m, function() {
+        mainHostSummaryView.set('content', test.content);
+        expect(mainHostSummaryView.get('areClientWithStaleConfigs')).to.equal(test.e);
+      });
+    });
+
+  });
+
+  describe('#isAddComponent', function() {
+
+    var tests = Em.A([
+      {content: {healthClass: 'health-status-DEAD-YELLOW'}, e: false},
+      {content: {healthClass: 'OTHER_VALUE'}, e: true}
+    ]);
+
+    tests.forEach(function(test) {
+      it(test.content.healthClass, function() {
+        mainHostSummaryView.set('content', test.content);
+        expect(mainHostSummaryView.get('isAddComponent')).to.equal(test.e);
+      });
+    });
+
+  });
+
+  describe('#installableClientComponents', function() {
+
+    it('delete host not supported', function() {
+      App.set('supports.deleteHost', false);
+      expect(mainHostSummaryView.get('installableClientComponents')).to.eql([]);
+      App.set('supports.deleteHost', true);
+    });
+
+    var tests = Em.A([
+      {
+        content: Em.Object.create({
+          hostComponents: Em.A([])
+        }),
+        services: ['HDFS', 'YARN', 'MAPREDUCE2'],
+        e: ['HDFS_CLIENT', 'YARN_CLIENT', 'MAPREDUCE2_CLIENT'],
+        m: 'no one client installed'
+      },
+      {
+        content: Em.Object.create({
+          hostComponents: Em.A([
+            Em.Object.create({
+              componentName: 'HDFS_CLIENT'
+            })
+          ])
+        }),
+        services: ['HDFS', 'YARN', 'MAPREDUCE2'],
+        e: ['YARN_CLIENT', 'MAPREDUCE2_CLIENT'],
+        m: 'some clients are already installed'
+      },
+      {
+        content: Em.Object.create({
+          hostComponents: Em.A([
+            Em.Object.create({
+              componentName: 'HDFS_CLIENT'
+            }),
+            Em.Object.create({
+              componentName: 'YARN_CLIENT'
+            }),
+            Em.Object.create({
+              componentName: 'MAPREDUCE2_CLIENT'
+            })
+          ])
+        }),
+        services: ['HDFS', 'YARN', 'MAPREDUCE2'],
+        e: [],
+        m: 'all clients are already installed'
+      }
+    ]);
+
+    tests.forEach(function(test) {
+      it(test.m, function() {
+        mainHostSummaryView.set('content', test.content);
+        mainHostSummaryView.set('installedServices', test.services);
+        expect(mainHostSummaryView.get('installableClientComponents')).to.include.members(test.e);
+        expect(test.e).to.include.members(mainHostSummaryView.get('installableClientComponents'));
+      });
+    });
+
+  });
+
+  describe('#addableComponents', function() {
+
+    var tests = Em.A([
+      {
+        content: Em.Object.create({
+          hostComponents: Em.A([])
+        }),
+        services: ['HDFS', 'YARN', 'MAPREDUCE2'],
+        e: ['DATANODE', 'NODEMANAGER', 'CLIENTS'],
+        m: 'no components on host (impossible IRL, but should be tested)'
+      },
+      {
+        content: Em.Object.create({
+          hostComponents: Em.A([
+            Em.Object.create({
+              componentName: 'HDFS_CLIENT'
+            }),
+            Em.Object.create({
+              componentName: 'DATANODE'
+            })
+          ])
+        }),
+        services: ['HDFS', 'YARN', 'MAPREDUCE2'],
+        e: ['NODEMANAGER', 'CLIENTS'],
+        m: 'some components are already installed'
+      },
+      {
+        content: Em.Object.create({
+          hostComponents: Em.A([
+            Em.Object.create({
+              componentName: 'HDFS_CLIENT'
+            }),
+            Em.Object.create({
+              componentName: 'YARN_CLIENT'
+            }),
+            Em.Object.create({
+              componentName: 'MAPREDUCE2_CLIENT'
+            }),
+            Em.Object.create({
+              componentName: 'NODEMANAGER'
+            })
+          ])
+        }),
+        services: ['HDFS', 'YARN', 'MAPREDUCE2'],
+        e: ['DATANODE'],
+        m: 'all clients and some other components are already installed'
+      }
+    ]);
+
+    tests.forEach(function(test) {
+      it(test.m, function() {
+        mainHostSummaryView.set('content', test.content);
+        mainHostSummaryView.set('installedServices', test.services);
+        expect(mainHostSummaryView.get('addableComponents').mapProperty('componentName')).to.include.members(test.e);
+        expect(test.e).to.include.members(mainHostSummaryView.get('addableComponents').mapProperty('componentName'));
+      });
+    });
+
+  });
+
+
+
+});