Przeglądaj źródła

AMBARI-6046. Lags on Assign Masters step on big cluster. (onechiporenko)

Oleg Nechiporenko 11 lat temu
rodzic
commit
5d3fad2c13

+ 133 - 94
ambari-web/app/controllers/wizard/step5_controller.js

@@ -18,7 +18,6 @@
 
 var App = require('app');
 var numberUtils = require('utils/number_utils');
-var lazyLoading = require('utils/lazy_loading');
 
 App.WizardStep5Controller = Em.Controller.extend({
 
@@ -66,38 +65,18 @@ App.WizardStep5Controller = Em.Controller.extend({
    */
   multipleComponents: ['ZOOKEEPER_SERVER', 'HBASE_MASTER'],
 
+  /**
+   * Define state for submit button
+   * @type {bool}
+   */
   submitDisabled: false,
 
   /**
-   * Define state for submit button. Return true only for Reassign Master Wizard and if more than one master component was reassigned.
+   * Trigger for executing host names check for components
+   * Should de "triggered" when host changed for some component and when new multiple component is added/removed
    * @type {bool}
    */
-  isSubmitDisabled: function () {
-    if (!this.get('isReassignWizard')) {
-      this.set('submitDisabled', false);
-    } else {
-      App.ajax.send({
-        name: 'host_components.all',
-        sender: this,
-        data: {
-          clusterName: App.get('clusterName')
-        },
-        success: 'isSubmitDisabledSuccessCallBack'
-      });
-    }
-  }.observes('servicesMasters.@each.selectedHost'),
-
-  isSubmitDisabledSuccessCallBack: function (response) {
-    var reassigned = 0;
-    var arr1 = response.items.mapProperty('HostRoles').filterProperty('component_name', this.get('content.reassign.component_name')).mapProperty('host_name');
-    var arr2 = this.get('servicesMasters').mapProperty('selectedHost');
-    arr1.forEach(function (host) {
-      if (!arr2.contains(host)) {
-        reassigned++;
-      }
-    }, this);
-    this.set('submitDisabled', reassigned !== 1);
-  },
+  hostNameCheckTrigger: false,
 
   /**
    * List of hosts
@@ -106,11 +85,19 @@ App.WizardStep5Controller = Em.Controller.extend({
   hosts: [],
 
   /**
+   * Name of multiple component which host name was changed last
    * @type {Object|null}
    */
   componentToRebalance: null,
 
   /**
+   * Name of component which host was changed last
+   * @type {string}
+   */
+  lastChangedComponent: null,
+
+  /**
+   * Flag for rebalance multiple components
    * @type {number}
    */
   rebalanceComponentHostsCounter: 0,
@@ -125,6 +112,12 @@ App.WizardStep5Controller = Em.Controller.extend({
    */
   selectedServicesMasters: [],
 
+  /**
+   * Is data for current step loaded
+   * @type {bool}
+   */
+  isLoaded: false,
+
   /**
    * Check if HIVE_SERVER component exist (also checks if this is not reassign)
    * @type {bool}
@@ -141,69 +134,40 @@ App.WizardStep5Controller = Em.Controller.extend({
    *     {
    *       host_name: '',
    *       hostInfo: {},
-   *       masterServices: []
+   *       masterServices: [],
+   *       masterServicesToDisplay: [] // used only in template
    *    },
    *    ....
    *   ]
    * </code>
    * @type {Ember.Enumerable}
    */
-  masterHostMapping: [],
-
-  isLoaded: false,
-
-  /**
-   * Check if HIVE_SERVER component exist (also checks if this is not reassign)
-   * @type {bool}
-   */
-  hasHiveServer: function () {
-    return this.get('selectedServicesMasters').someProperty('component_name', 'HIVE_SERVER') && !this.get('isReassignWizard');
-  }.property('selectedServicesMasters'),
-
-  masterHostMappingObserver: function () {
-    var requestName = this.get('content.controllerName') == 'installerController' ? 'hosts.confirmed.install' : 'hosts.confirmed'
-    App.ajax.send({
-      name: requestName,
-      sender: this,
-      data: {
-        clusterName: App.get('clusterName')
-      },
-      success: 'masterHostMappingSuccessCallback'
-    });
-  }.observes('selectedServicesMasters', 'selectedServicesMasters.@each.selectedHost'),
-
-  masterHostMappingSuccessCallback: function (response) {
+  masterHostMapping: function () {
     var mapping = [], mappingObject, mappedHosts, hostObj;
     //get the unique assigned hosts and find the master services assigned to them
-    mappedHosts = this.get("selectedServicesMasters").mapProperty("selectedHost").uniq().without(undefined);
+    mappedHosts = this.get("selectedServicesMasters").mapProperty("selectedHost").uniq();
     mappedHosts.forEach(function (item) {
-      var host = response.items.mapProperty('Hosts').findProperty('host_name', item);
-      hostObj = {
-        name: host.host_name,
-        memory: host.total_mem,
-        cpu: host.cpu_count
-      };
-
+      hostObj = this.get("hosts").findProperty("host_name", item);
+      // User may input invalid host name (this is handled in hostname checker). Here we just skip it
+      if (!hostObj) return;
+      var masterServices = this.get("selectedServicesMasters").filterProperty("selectedHost", item),
+        masterServicesToDisplay = [];
+      masterServices.mapProperty('display_name').uniq().forEach(function(n) {
+        masterServicesToDisplay.pushObject(masterServices.findProperty('display_name', n));
+      });
       mappingObject = Em.Object.create({
         host_name: item,
-        hostInfo: Em.I18n.t('installer.step5.hostInfo').fmt(hostObj.name, numberUtils.bytesToSize(hostObj.memory, 1, 'parseFloat', 1024), hostObj.cpu),
-        masterServices: this.get("selectedServicesMasters").filterProperty("selectedHost", item)
+        hostInfo: hostObj.host_info,
+        masterServices: masterServices,
+        masterServicesToDisplay: masterServicesToDisplay
       });
 
       mapping.pushObject(mappingObject);
     }, this);
 
-    this.set('masterHostMapping', []);
-    lazyLoading.run({
-      initSize: 20,
-      chunkSize: 50,
-      delay: 50,
-      destination: this.get('masterHostMapping'),
-      source: mapping.sortProperty('host_name'),
-      context: Em.Object.create()
-    });
-  },
-
+    return mapping.sortProperty('host_name');
+  }.property("selectedServicesMasters.@each.selectedHost", 'selectedServicesMasters.@each.isHostNameValid'),
+  
   /**
    * Count of hosts without masters
    * @type {number}
@@ -213,9 +177,48 @@ App.WizardStep5Controller = Em.Controller.extend({
      return 0;
     } else {
       return (this.get("hosts.length") - this.get("masterHostMapping.length"));
-    };
+    }
   }.property('masterHostMapping.length', 'selectedServicesMasters.@each.selectedHost'),
 
+  /**
+   * Update submit button status
+   * @metohd getIsSubmitDisabled
+   */
+  getIsSubmitDisabled: function () {
+    if (!this.get('isReassignWizard')) {
+      this.set('submitDisabled', this.get('servicesMasters').someProperty('isHostNameValid', false));
+    }
+    else {
+      App.ajax.send({
+        name: 'host_components.all',
+        sender: this,
+        data: {
+          clusterName: App.get('clusterName')
+        },
+        success: 'getIsSubmitDisabledSuccessCallBack'
+      });
+    }
+  }.observes('servicesMasters.@each.selectedHost', 'servicesMasters.@each.isHostNameValid'),
+
+  /**
+   * Success callback for getIsSubmitDisabled method
+   * Set true for Reassign Master Wizard and if more than one master component was reassigned.
+   * For installer, addHost and addService verify that provided host names for components are valid
+   * @param {object} response
+   * @method getIsSubmitDisabledSuccessCallBack
+   */
+  getIsSubmitDisabledSuccessCallBack: function (response) {
+    var reassigned = 0;
+    var arr1 = response.items.mapProperty('HostRoles').filterProperty('component_name', this.get('content.reassign.component_name')).mapProperty('host_name');
+    var arr2 = this.get('servicesMasters').mapProperty('selectedHost');
+    arr1.forEach(function (host) {
+      if (!arr2.contains(host)) {
+        reassigned++;
+      }
+    }, this);
+    this.set('submitDisabled', reassigned !== 1);
+  },
+
   /**
    * Clear controller data (hosts, masters etc)
    * @method clearStep
@@ -291,15 +294,7 @@ App.WizardStep5Controller = Em.Controller.extend({
         }));
       }
     }
-    this.set("hosts", []);
-    lazyLoading.run({
-      initSize: 20,
-      chunkSize: 50,
-      delay: 50,
-      destination: this.get('hosts'),
-      source: result,
-      context: Em.Object.create()
-    });
+    this.set("hosts", result);
     this.sortHosts(this.get('hosts'));
     this.set('isLoaded', true);
   },
@@ -421,7 +416,7 @@ App.WizardStep5Controller = Em.Controller.extend({
         componentObj.set("showRemoveControl", showRemoveControlZk);
       }
       else {
-        if (App.supports.multipleHBaseMasters && item.component_name === "HBASE_MASTER") {
+        if (App.get('supports.multipleHBaseMasters') && item.component_name === "HBASE_MASTER") {
           componentObj.set('zId', hid++);
           componentObj.set("showRemoveControl", showRemoveControlHb);
         }
@@ -431,6 +426,7 @@ App.WizardStep5Controller = Em.Controller.extend({
           }
         }
       }
+      componentObj.set('isHostNameValid', true);
       result.push(componentObj);
     }, this);
 
@@ -559,18 +555,61 @@ App.WizardStep5Controller = Em.Controller.extend({
   },
 
   /**
-   * On change callback for selects
+   * On change callback for inputs
    * @param {string} componentName
    * @param {string} selectedHost
    * @param {number} zId
    * @method assignHostToMaster
    */
   assignHostToMaster: function (componentName, selectedHost, zId) {
-    if (selectedHost && componentName) {
+    var flag = this.isHostNameValid(componentName, selectedHost);
+    this.updateIsHostNameValidFlag(componentName, zId, flag);
+    if (zId) {
+      this.get('selectedServicesMasters').filterProperty('component_name', componentName).findProperty("zId", zId).set("selectedHost", selectedHost);
+    }
+    else {
+      this.get('selectedServicesMasters').findProperty("component_name", componentName).set("selectedHost", selectedHost);
+    }
+  },
+
+  /**
+   * Determines if hostName is valid for component:
+   * <ul>
+   *  <li>host name shouldn't be empty</li>
+   *  <li>host should exist</li>
+   *  <li>host should have only one component with <code>componentName</code></li>
+   * </ul>
+   * @param {string} componentName
+   * @param {string} selectedHost
+   * @returns {boolean} true - valid, false - invalid
+   * @method isHostNameValid
+   */
+  isHostNameValid: function(componentName, selectedHost) {
+    return (selectedHost.trim() !== '') &&
+      this.get('hosts').mapProperty('host_name').contains(selectedHost) &&
+      (this.get('selectedServicesMasters').
+        filterProperty('component_name', componentName).
+        mapProperty('selectedHost').
+        filter(function(h) {
+          return h === selectedHost;
+        }).length <= 1);
+  },
+
+  /**
+   * Update <code>isHostNameValid</code> property with <code>flag</code> value
+   * for component with name <code>componentName</code> and
+   * <code>zId</code>-property equal to <code>zId</code>-parameter value
+   * @param {string} componentName
+   * @param {number} zId
+   * @param {bool} flag
+   * @method updateIsHostNameValidFlag
+   */
+  updateIsHostNameValidFlag: function (componentName, zId, flag) {
+    if (componentName) {
       if (zId) {
-        this.get('selectedServicesMasters').filterProperty('component_name', componentName).findProperty("zId", zId).set("selectedHost", selectedHost);
+        this.get('selectedServicesMasters').filterProperty('component_name', componentName).findProperty("zId", zId).set("isHostNameValid", flag);
       } else {
-        this.get('selectedServicesMasters').findProperty("component_name", componentName).set("selectedHost", selectedHost);
+        this.get('selectedServicesMasters').findProperty("component_name", componentName).set("isHostNameValid", flag);
       }
     }
   },
@@ -648,7 +687,7 @@ App.WizardStep5Controller = Em.Controller.extend({
 
       this.set('componentToRebalance', componentName);
       this.incrementProperty('rebalanceComponentHostsCounter');
-
+      this.toggleProperty('hostNameCheckTrigger');
       return true;
     }
     return false;//if no more zookeepers can be added
@@ -682,7 +721,7 @@ App.WizardStep5Controller = Em.Controller.extend({
 
     this.set('componentToRebalance', componentName);
     this.incrementProperty('rebalanceComponentHostsCounter');
-
+    this.toggleProperty('hostNameCheckTrigger');
     return true;
   },
 
@@ -691,10 +730,10 @@ App.WizardStep5Controller = Em.Controller.extend({
    * @metohd submit
    */
   submit: function () {
-    this.isSubmitDisabled();
+    this.getIsSubmitDisabled();
     if (!this.get('submitDisabled')) {
       App.router.send('next');
     }
   }
 
-});
+});

+ 2 - 1
ambari-web/app/mixins.js

@@ -22,4 +22,5 @@
 require('mixins/common/localStorage');
 require('mixins/common/userPref');
 require('mixins/common/tableServerProvider');
-require('mixins/main/host/details/host_components/decommissionable');
+require('mixins/main/host/details/host_components/decommissionable');
+require('mixins/wizard/selectHost');

+ 179 - 0
ambari-web/app/mixins/wizard/selectHost.js

@@ -0,0 +1,179 @@
+/**
+ * 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');
+
+/**
+ * Mixin for host select view on step 5 install wizard
+ * Implements base login for select, input types
+ * Each view which use this mixin should redefine method <code>changeHandler</code> (with
+ * observing <code>controller.hostNameCheckTrigger</code>)
+ *
+ * @type {Ember.Mixin}
+ */
+App.SelectHost = Em.Mixin.create({
+
+  /**
+   * Element of <code>controller.servicesMasters</code>
+   * Binded from template
+   * @type {object}
+   */
+  component: null,
+
+  /**
+   * List of avaiable host names
+   * @type {string[]}
+   */
+  content: [],
+
+  /**
+   * Host component name
+   * @type {string}
+   */
+  componentName: null,
+
+  /**
+   * Handler for selected value change
+   * Triggers <code>changeHandler</code> execution
+   * @method change
+   */
+  change: function () {
+    if ('destroyed' === this.get('state')) return;
+    this.set('controller.lastChangedComponent', this.get('component.component_name'));
+    this.get('controller').toggleProperty('hostNameCheckTrigger');
+  },
+
+  /**
+   * Add or remove <code>error</code> class from parent div-element
+   * @param {bool} flag true - add class, false - remove
+   * @method updateErrorStatus
+   */
+  updateErrorStatus: function(flag) {
+    var parentBlock = this.$().parent('div');
+    /* istanbul ignore next */
+    if (flag) {
+      parentBlock.removeClass('error');
+    }
+    else {
+      parentBlock.addClass('error');
+    }
+  },
+
+  /**
+   * If component is multiple (defined in <code>controller.multipleComponents</code>),
+   * should update <code>controller.componentToRebalance</code> and trigger rebalancing
+   * @method tryTriggerRebalanceForMultipleComponents
+   */
+  tryTriggerRebalanceForMultipleComponents: function() {
+    var componentIsMultiple = this.get('controller.multipleComponents').contains(this.get("component.component_name"));
+    if(componentIsMultiple) {
+      this.get('controller').set('componentToRebalance', this.get("component.component_name"));
+      this.get('controller').incrementProperty('rebalanceComponentHostsCounter');
+    }
+  },
+
+  /**
+   * Handler for value changing
+   * Should be redeclared in child views
+   * Each redeclarion should contains code:
+   * <code>
+   *   if (!this.shouldChangeHandlerBeCalled()) return;
+   * </code>
+   * @method changeHandler
+   */
+  changeHandler: function() {}.observes('controller.hostNameCheckTrigger'),
+
+  /**
+   * If view is destroyed or not current component's host name was changed we should do nothing
+   * This method should be called from <code>changeHandler</code>:
+   * <code>
+   *   changeHandler: function() {
+   *     if (!this.shouldChangeHandlerBeCalled()) return;
+   *     // your code
+   *   }.observes(...)
+   * </code>
+   * @returns {boolean}
+   * @method shouldChangeHandlerBeCalled
+   */
+  shouldChangeHandlerBeCalled: function() {
+    return !(('destroyed' === this.get('state')) || (this.get('controller.lastChangedComponent') !== this.get("component.component_name")));
+  },
+
+  /**
+   * If <code>component.isHostNameValid</code> was changed,
+   * error status should be updated according to new value
+   * @method isHostNameValidObs
+   */
+  isHostNameValidObs: function() {
+    this.updateErrorStatus(this.get('component.isHostNameValid'));
+  }.observes('component.isHostNameValid'),
+
+  /**
+   * Recalculate available hosts
+   * This should be done only once per Ember loop
+   * @method rebalanceComponentHosts
+   */
+  rebalanceComponentHosts: function () {
+    Em.run.once(this, 'rebalanceComponentHostsOnce');
+  }.observes('controller.rebalanceComponentHostsCounter'),
+
+  /**
+   * Recalculate available hosts
+   * @method rebalanceComponentHostsOnce
+   */
+  rebalanceComponentHostsOnce: function() {
+    if (this.get('component.component_name') === this.get('controller.componentToRebalance')) {
+      this.initContent();
+    }
+  },
+
+  /**
+   * Get available hosts
+   * multipleComponents component can be assigned to multiple hosts,
+   * shared hosts among the same component should be filtered out
+   * @return {string[]}
+   * @method getAvailableHosts
+   */
+  getAvailableHosts: function () {
+    var hosts = this.get('controller.hosts').slice(),
+      componentName = this.get('component.component_name'),
+      multipleComponents = this.get('controller.multipleComponents'),
+      occupiedHosts = this.get('controller.selectedServicesMasters')
+        .filterProperty('component_name', componentName)
+        .mapProperty('selectedHost')
+        .without(this.get('component.selectedHost'));
+
+    if (multipleComponents.contains(componentName)) {
+      return hosts.filter(function (host) {
+        return !occupiedHosts.contains(host.get('host_name'));
+      }, this);
+    }
+    return hosts;
+  },
+
+  /**
+   * Extract hosts from controller,
+   * filter out available to selection and
+   * push them into form element content
+   * @method initContent
+   */
+  initContent: function () {
+    this.set("content", this.getAvailableHosts());
+  }
+
+});

+ 4 - 0
ambari-web/app/styles/application.less

@@ -79,6 +79,10 @@ wbr {
   display: inline-block;
 }
 
+ul.typeahead.dropdown-menu {
+  z-index: 3000000 !important;
+}
+
 #wrapper {
   min-height: 100%;
 }

+ 15 - 11
ambari-web/app/templates/wizard/step5.hbs

@@ -61,21 +61,25 @@
                         {{selectedHost}}<i class="icon-asterisks">&#10037;</i>
                       </div>
                     {{else}}
-                      {{view App.SelectHostView
-                      optionValuePath="content.host_name"
-                      optionLabelPath="content.host_info"
-                      selectedHostBinding="selectedHost"
-                      componentNameBinding="component_name"
-                      class="host-select"
-                      zIdBinding="zId"
-                      disabledBinding="isInstalled"
-                      }}
+                    <div class="control-group">
+                      {{#if view.shouldUseInputs}}
+                        {{view App.InputHostView
+                        componentBinding="this"
+                        disabledBinding="isInstalled" }}
+                      {{else}}
+                        {{view App.SelectHostView
+                        componentBinding="this"
+                        disabledBinding="isInstalled"
+                        optionValuePath="content.host_name"
+                        optionLabelPath="content.host_info" }}
+                      {{/if}}
                       {{#if showAddControl}}
                         {{view App.AddControlView componentNameBinding="component_name"}}
                       {{/if}}
                       {{#if showRemoveControl}}
                         {{view App.RemoveControlView componentNameBinding="component_name" zIdBinding="zId"}}
                       {{/if}}
+                      </div>
                     {{/if}}
                   </div>
                 </div>
@@ -90,7 +94,7 @@
       {{#each masterHostMapping}}
         <div class="mapping-box round-corners well">
           <div class="hostString"><span>{{hostInfo}}</span></div>
-          {{#each masterServices}}
+          {{#each masterServicesToDisplay}}
             <span {{bindAttr class="isInstalled:assignedService:newService :round-corners"}}>{{display_name}}</span>
           {{/each}}
         </div>
@@ -109,4 +113,4 @@
 <div class="btn-area">
   <a class="btn pull-left" {{action back href="true"}}>&larr; {{t common.back}}</a>
   <a class="btn btn-success pull-right" {{bindAttr disabled="submitDisabled"}} {{action submit target="controller"}}>{{t common.next}} &rarr;</a>
-</div>
+</div>

+ 76 - 116
ambari-web/app/views/wizard/step5_view.js

@@ -16,160 +16,120 @@
  * limitations under the License.
  */
 
-
 var App = require('app');
-var lazyloading = require('utils/lazy_loading');
 
 App.WizardStep5View = Em.View.extend({
 
   templateName: require('templates/wizard/step5'),
 
+  /**
+   * If install more than 25 hosts, should use App.InputHostView for hosts selection
+   * Othervise - App.SelectHostView
+   * @type {bool}
+   */
+  shouldUseInputs: function() {
+    return this.get('controller.hosts.length') > 25;
+  }.property('controller.hosts.length'),
+
   didInsertElement: function () {
     this.get('controller').loadStep();
   }
 
 });
 
-App.SelectHostView = Em.Select.extend({
+App.InputHostView = Em.TextField.extend(App.SelectHost, {
+
+  attributeBindings: ['disabled'],
 
   /**
-   * List of avaiable host names
-   * @type {string[]}
+   * Saved typeahead component
+   * @type {$}
    */
-  content: [],
+  typeahead: null,
 
   /**
-   * Index for multiple component (like ZOOKEEPER_SERVER)
-   * @type {number|null}
+   * When <code>value</code> (host_info) is changed this method is triggered
+   * If new hostname is valid, this host is assigned to master component
+   * @method changeHandler
    */
-  zId: null,
+  changeHandler: function() {
+    if (!this.shouldChangeHandlerBeCalled()) return;
+    var host = this.get('controller.hosts').findProperty('host_info', this.get('value'));
+    if (Em.isNone(host)) {
+      this.get('controller').updateIsHostNameValidFlag(this.get("component.component_name"), this.get("component.zId"), false);
+      return;
+    }
+    this.get('controller').assignHostToMaster(this.get("component.component_name"), host.get('host_name'), this.get("component.zId"));
+    this.tryTriggerRebalanceForMultipleComponents();
+  }.observes('controller.hostNameCheckTrigger'),
+
+  didInsertElement: function () {
+    this.initContent();
+    var value = this.get('content').findProperty('host_name', this.get('component.selectedHost')).get('host_info');
+    this.set("value", value);
+    var content = this.get('content').mapProperty('host_info'),
+      self = this,
+      typeahead = this.$().typeahead({items: 10, source: content, minLength: 0});
+    typeahead.on('blur', function() {
+      self.change();
+    }).on('keyup', function(e) {
+        self.set('value', $(e.currentTarget).val());
+        self.change();
+      });
+    this.set('typeahead', typeahead);
+  },
 
   /**
-   * Selected host name for host component
-   * @type {string}
+   * Extract hosts from controller,
+   * filter out available to selection and
+   * push them into Em.Select content
+   * @method initContent
    */
-  selectedHost: null,
+  initContent: function () {
+    this._super();
+    this.updateTypeaheadData(this.get('content').mapProperty('host_info'));
+  },
 
   /**
-   * Host component name
-   * @type {string}
+   * Update <code>source</code> property of <code>typeahead</code> with a new list of hosts
+   * @param {string[]} hosts
+   * @method updateTypeaheadData
    */
-  componentName: null,
+  updateTypeaheadData: function(hosts) {
+    if (this.get('typeahead')) {
+      this.get('typeahead').data('typeahead').source = hosts;
+    }
+  }
 
-  attributeBindings: ['disabled'],
+});
 
-  /**
-   * Is data loaded
-   * @type {bool}
-   */
-  isLoaded: false,
+App.SelectHostView = Em.Select.extend(App.SelectHost, {
 
-  /**
-   * Is lazy loading used
-   * @type {bool}
-   */
-  isLazyLoading: false,
+  attributeBindings: ['disabled'],
 
-  /**
-   * Handler for selected value change
-   * @method change
-   */
-  change: function () {
-    this.get('controller').assignHostToMaster(this.get("componentName"), this.get("value"), this.get("zId"));
-    this.set('selectedHost', this.get('value'));
-    this.get('controller').set('componentToRebalance', this.get("componentName"));
-    this.get('controller').incrementProperty('rebalanceComponentHostsCounter');
+  didInsertElement: function () {
+    this.initContent();
+    this.set("value", this.get("component.selectedHost"));
   },
 
   /**
-   * Recalculate available hosts
-   * @method rebalanceComponentHosts
-   */
-  rebalanceComponentHosts: function () {
-    if (this.get('componentName') === this.get('controller.componentToRebalance')) {
-      this.get('content').clear();
-      this.set('isLoaded', false);
-      this.initContent();
-    }
-  }.observes('controller.rebalanceComponentHostsCounter'),
-
-  /**
-   * Get available hosts
-   * multipleComponents component can be assigned to multiple hosts,
-   * shared hosts among the same component should be filtered out
-   * @return {string[]}
-   * @method getAvailableHosts
+   * Handler for selected value change
+   * @method change
    */
-  getAvailableHosts: function () {
-    var hosts = this.get('controller.hosts').slice(),
-      componentName = this.get('componentName'),
-      multipleComponents = this.get('controller.multipleComponents'),
-      occupiedHosts = this.get('controller.selectedServicesMasters')
-        .filterProperty('component_name', componentName)
-        .mapProperty('selectedHost')
-        .without(this.get('selectedHost'));
-
-    if (multipleComponents.contains(componentName)) {
-      return hosts.filter(function (host) {
-        return !occupiedHosts.contains(host.get('host_name'));
-      }, this);
-    }
-    return hosts;
-  },
+  changeHandler: function () {
+    if (!this.shouldChangeHandlerBeCalled()) return;
+    this.get('controller').assignHostToMaster(this.get("component.component_name"), this.get("value"), this.get("component.zId"));
+    this.tryTriggerRebalanceForMultipleComponents();
+  }.observes('controller.hostNameCheckTrigger'),
 
   /**
-   * On click start lazy loading
+   * On click handler
    * @method click
    */
   click: function () {
-    var source = [];
-    var availableHosts = this.getAvailableHosts();
-
-    if (!this.get('isLoaded') && this.get('isLazyLoading')) {
-      //filter out hosts, which already pushed in select
-      source = availableHosts.filter(function (_host) {
-        return !this.get('content').someProperty('host_name', _host.host_name);
-      }, this).slice();
-      lazyloading.run({
-        destination: this.get('content'),
-        source: source,
-        context: this,
-        initSize: 30,
-        chunkSize: 200,
-        delay: 50
-      });
-    }
-  },
-
-  didInsertElement: function () {
-    //The lazy loading for select elements supported only by Firefox and Chrome
-    var isBrowserSupported = $.browser.mozilla || ($.browser.safari && navigator.userAgent.indexOf('Chrome') !== -1);
-    var isLazyLoading = isBrowserSupported && this.get('controller.hosts').length > 100;
-    this.set('isLazyLoading', isLazyLoading);
     this.initContent();
-    this.set("value", this.get("selectedHost"));
-  },
-
-  /**
-   * Extract hosts from controller,
-   * filter out available to selection and
-   * push them into Em.Select content
-   * @method initContent
-   */
-  initContent: function () {
-    var hosts = this.getAvailableHosts();
-    if (this.get('isLazyLoading')) {
-      //select need at least 30 hosts to have scrollbar
-      var initialHosts = hosts.slice(0, 30);
-      if (!initialHosts.someProperty('host_name', this.get('selectedHost'))) {
-        initialHosts.unshift(hosts.findProperty('host_name', this.get('selectedHost')));
-      }
-      this.set("content", initialHosts);
-    }
-    else {
-      this.set("content", hosts);
-    }
   }
+
 });
 
 App.AddControlView = Em.View.extend({
@@ -224,4 +184,4 @@ App.RemoveControlView = Em.View.extend({
   click: function () {
     this.get('controller').removeComponent(this.get('componentName'), this.get("zId"));
   }
-});
+});

+ 154 - 108
ambari-web/test/views/wizard/step5_view_test.js

@@ -18,7 +18,6 @@
 
 
 var App = require('app');
-var lazyloading = require('utils/lazy_loading');
 require('views/wizard/step5_view');
 var view;
 
@@ -39,59 +38,176 @@ describe('App.WizardStep5View', function() {
     });
   });
 
+  describe('#shouldUseInputs', function() {
+    it('should based on hosts count', function() {
+      view.set('controller.hosts', d3.range(0, 25).map(function() {return {};}));
+      expect(view.get('shouldUseInputs')).to.be.false;
+      view.set('controller.hosts', d3.range(0, 26).map(function() {return {};}));
+      expect(view.get('shouldUseInputs')).to.be.true;
+      view.set('controller.hosts', d3.range(0, 24).map(function() {return {};}));
+      expect(view.get('shouldUseInputs')).to.be.false;
+    });
+  });
+
 });
 
 describe('App.SelectHostView', function() {
 
   beforeEach(function() {
     view = App.SelectHostView.create({
-      controller: App.WizardStep5Controller.create({})
+      controller: App.WizardStep5Controller.create({}),
+      $: function() {return {typeahead: function(){return {on: Em.K}}}},
+      updateErrorStatus: Em.K
+    });
+  });
+
+  describe('#click', function() {
+
+    beforeEach(function() {
+      sinon.stub(view, 'initContent', Em.K);
+    });
+
+    afterEach(function() {
+      view.initContent.restore();
     });
+
+    it('should call initContent', function() {
+      view.click();
+      expect(view.initContent.calledOnce).to.be.true;
+    });
+
   });
 
   describe('#didInsertElement', function() {
+
+    it('should set value', function() {
+      view.set('value', '');
+      view.set('component', {selectedHost: 'h1'});
+      view.didInsertElement();
+      expect(view.get('value')).to.equal('h1');
+    });
+
+  });
+
+  describe('#changeHandler', function() {
+
+    beforeEach(function() {
+      view.set('component', {component_name: 'ZOOKEEPER_SERVER', zId: 1});
+      view.set('controller.hosts', [Em.Object.create({host_info: 'h1 info', host_name: 'h1'})]);
+      view.set('value', 'h1 info');
+      view.set('controller.rebalanceComponentHostsCounter', 0);
+      view.set('controller.componentToRebalance', '');
+      sinon.stub(view.get('controller'), 'assignHostToMaster', Em.K);
+      sinon.stub(view.get('controller'), 'updateIsHostNameValidFlag', Em.K);
+      sinon.stub(view, 'shouldChangeHandlerBeCalled', function() {return true;});
+    });
+
+    afterEach(function() {
+      view.get('controller').assignHostToMaster.restore();
+      view.get('controller').updateIsHostNameValidFlag.restore();
+      view.shouldChangeHandlerBeCalled.restore();
+    });
+
+    it('shouldn\'t do nothing if view is destroyed', function() {
+      view.set('state', 'destroyed');
+      expect(view.get('controller').assignHostToMaster.called).to.be.false;
+    });
+
+    it('should call assignHostToMaster', function() {
+      view.changeHandler();
+      expect(view.get('controller').assignHostToMaster.calledWith('ZOOKEEPER_SERVER', 'h1', 1));
+    });
+
+    it('should increment rebalanceComponentHostsCounter if component it is multiple', function() {
+      view.set('component', {component_name: 'ZOOKEEPER_SERVER'});
+      view.changeHandler();
+      expect(view.get('controller.rebalanceComponentHostsCounter')).to.equal(1);
+    });
+
+    it('should set componentToRebalance', function() {
+      view.changeHandler();
+      expect(view.get('controller.componentToRebalance')).to.equal('ZOOKEEPER_SERVER');
+    });
+
+  });
+
+});
+
+describe('App.InputHostView', function() {
+
+  beforeEach(function() {
+    view = App.InputHostView.create({
+      controller: App.WizardStep5Controller.create({}),
+      $: function() {return {typeahead: function(){return {on: Em.K}}}},
+      updateErrorStatus: Em.K
+    });
+  });
+
+  describe('#didInsertElement', function() {
+
     beforeEach(function() {
       sinon.stub(view, 'initContent', Em.K);
+      view.set('content', [Em.Object.create({host_name: 'h1', host_info: 'h1 info'})]);
+      view.set('component', {selectedHost: 'h1'});
     });
+
     afterEach(function() {
       view.initContent.restore();
     });
+
     it('should call initContent', function() {
       view.didInsertElement();
       expect(view.initContent.calledOnce).to.equal(true);
     });
-    it('should set selectedHost to value', function() {
-      view.set('selectedHost', 'h1');
+
+    it('should set selectedHost host_info to value', function() {
       view.set('value', '');
       view.didInsertElement();
-      expect(view.get('value')).to.equal('h1');
+      expect(view.get('value')).to.equal('h1 info');
     });
+
   });
 
-  describe('#change', function() {
+  describe('#changeHandler', function() {
+
     beforeEach(function() {
-      view.set('componentName', 'ZOOKEEPER_SERVER');
-      view.set('value', 'h1');
-      view.set('zId', 1);
+      view.set('component', {component_name: 'ZOOKEEPER_SERVER', zId: 1});
+      view.set('controller.hosts', [Em.Object.create({host_info: 'h1 info', host_name: 'h1'})]);
+      view.set('value', 'h1 info');
       view.set('controller.rebalanceComponentHostsCounter', 0);
       view.set('controller.componentToRebalance', '');
       sinon.stub(view.get('controller'), 'assignHostToMaster', Em.K);
+      sinon.stub(view.get('controller'), 'updateIsHostNameValidFlag', Em.K);
+      sinon.stub(view, 'shouldChangeHandlerBeCalled', function() {return true;});
     });
+
     afterEach(function() {
       view.get('controller').assignHostToMaster.restore();
+      view.get('controller').updateIsHostNameValidFlag.restore();
+      view.shouldChangeHandlerBeCalled.restore();
+    });
+
+    it('shouldn\'t do nothing if view is destroyed', function() {
+      view.set('state', 'destroyed');
+      expect(view.get('controller').assignHostToMaster.called).to.be.false;
     });
+
     it('should call assignHostToMaster', function() {
-      view.change();
+      view.changeHandler();
       expect(view.get('controller').assignHostToMaster.calledWith('ZOOKEEPER_SERVER', 'h1', 1));
     });
-    it('should increment rebalanceComponentHostsCounter', function() {
-      view.change();
+
+    it('should increment rebalanceComponentHostsCounter if component it is multiple', function() {
+      view.set('component', {component_name: 'ZOOKEEPER_SERVER'});
+      view.changeHandler();
       expect(view.get('controller.rebalanceComponentHostsCounter')).to.equal(1);
     });
+
     it('should set componentToRebalance', function() {
-      view.change();
+      view.changeHandler();
       expect(view.get('controller.componentToRebalance')).to.equal('ZOOKEEPER_SERVER');
     });
+
   });
 
   describe('#getAvailableHosts', function() {
@@ -149,7 +265,7 @@ describe('App.SelectHostView', function() {
     tests.forEach(function(test) {
       it(test.m, function() {
         view.set('controller.hosts', test.hosts);
-        view.set('componentName', test.componentName);
+        view.set('component', {component_name: test.componentName});
         view.set('controller.selectedServicesMasters', test.selectedServicesMasters);
         var r = view.getAvailableHosts();
         expect(r.mapProperty('host_name')).to.eql(test.e);
@@ -157,45 +273,36 @@ describe('App.SelectHostView', function() {
     });
   });
 
-  describe('#rebalanceComponentHosts', function() {
+  describe('#rebalanceComponentHostsOnce', function() {
     var tests = Em.A([
       {
         componentName: 'c1',
         componentToRebalance: 'c2',
-        isLoaded: true,
         content: [{}],
         m: 'componentName not equal to componentToRebalance',
         e: {
-          initContent: false,
-          isLoaded: true,
-          content: 1
+          initContent: false
         }
       },
       {
         componentName: 'c2',
         componentToRebalance: 'c2',
-        isLoaded: true,
         content: [{}],
         m: 'componentName equal to componentToRebalance',
         e: {
-          initContent: true,
-          isLoaded: false,
-          content: 0
+          initContent: true
         }
       }
     ]);
 
     tests.forEach(function(test) {
       it(test.m, function() {
-        view.set('isLoaded', test.isLoaded);
         view.set('content', test.content);
-        view.set('componentName', test.componentName);
+        view.set('component', {component_name: test.componentName});
         view.set('controller.componentToRebalance', test.componentToRebalance);
         sinon.stub(view, 'initContent', Em.K);
-        view.rebalanceComponentHosts();
+        view.rebalanceComponentHostsOnce();
         expect(view.initContent.calledOnce).to.equal(test.e.initContent);
-        expect(view.get('isLoaded')).to.equal(test.e.isLoaded);
-        expect(view.get('content.length')).to.equal(test.e.content);
         view.initContent.restore();
       });
     });
@@ -204,43 +311,15 @@ describe('App.SelectHostView', function() {
   describe('#initContent', function() {
     var tests = Em.A([
       {
-        isLazyLoading: false,
         hosts: 25,
         m: 'not lazy loading, 25 hosts, no selected host',
         e: 25
       },
       {
-        isLazyLoading: false,
         hosts: 25,
         h: 4,
         m: 'not lazy loading, 25 hosts, one selected host',
         e: 25
-      },
-      {
-        isLazyLoading: true,
-        hosts: 25,
-        h: 4,
-        m: 'lazy loading, 25 hosts, one selected host',
-        e: 25
-      },
-      {
-        isLazyLoading: true,
-        hosts: 25,
-        m: 'lazy loading, 25 hosts, no selected host',
-        e: 26
-      },
-      {
-        isLazyLoading: true,
-        hosts: 100,
-        h: 4,
-        m: 'lazy loading, 100 hosts, one selected host',
-        e: 30
-      },
-      {
-        isLazyLoading: true,
-        hosts: 100,
-        m: 'lazy loading, 100 hosts, no selected host',
-        e: 31
       }
     ]);
     tests.forEach(function(test) {
@@ -249,68 +328,35 @@ describe('App.SelectHostView', function() {
         if (test.h) {
           view.set('selectedHost', test.h);
         }
-        view.set('isLazyLoading', test.isLazyLoading);
         view.initContent();
         expect(view.get('content.length')).to.equal(test.e);
       });
     });
   });
 
-  describe('#click', function() {
+  describe('#change', function() {
+
     beforeEach(function() {
-      sinon.stub(lazyloading, 'run', Em.K);
+      sinon.stub(view, 'changeHandler', Em.K);
     });
+
     afterEach(function() {
-      lazyloading.run.restore();
-    });
-    Em.A([
-        {
-          isLoaded: true,
-          isLazyLoading: true,
-          e: false
-        },
-        {
-          isLoaded: true,
-          isLazyLoading: false,
-          e: false
-        },
-        {
-          isLoaded: false,
-          isLazyLoading: true,
-          e: true
-        },
-        {
-          isLoaded: false,
-          isLazyLoading: false,
-          e: false
-        }
-      ]).forEach(function(test) {
-      it('isLoaded = ' + test.isLoaded.toString() + ', isLazyLoading = ' + test.isLazyLoading.toString(), function() {
-        view.reopen({
-          isLazyLoading: test.isLazyLoading,
-          isLoaded: test.isLoaded
-        });
-        view.click();
-        if(test.e) {
-          expect(lazyloading.run.calledOnce).to.equal(true);
-        }
-        else {
-          expect(lazyloading.run.called).to.equal(false);
-        }
-      });
+      view.changeHandler.restore();
     });
-    it('check lazyLoading parameters', function() {
-      view.reopen({
-        isLoaded: false,
-        isLazyLoading: true,
-        content: [{host_name: 'host1'}, {host_name: 'host2'}]
-      });
-      var availableHosts = d3.range(1, 100).map(function(i) {return {host_name: 'host' + i.toString()};});
-      sinon.stub(view, 'getAvailableHosts', function() {return availableHosts;});
-      view.click();
-      expect(lazyloading.run.args[0][0].source.length).to.equal(97); // 99-2
-      view.getAvailableHosts.restore();
+
+    it('shouldn\'t do nothing if view is destroyed', function() {
+      view.set('controller.hostNameCheckTrigger', false);
+      view.set('state', 'destroyed');
+      view.change();
+      expect(view.get('controller.hostNameCheckTrigger')).to.equal(false);
+    });
+
+    it('should toggle hostNameCheckTrigger', function() {
+      view.set('controller.hostNameCheckTrigger', false);
+      view.change();
+      expect(view.get('controller.hostNameCheckTrigger')).to.equal(true);
     });
+
   });
 
 });
@@ -366,4 +412,4 @@ describe('App.AddControlView', function() {
 
   });
 
-});
+});