浏览代码

AMBARI-5474. Unit tests for steps 3-4. (onechiporenko)

Oleg Nechiporenko 11 年之前
父节点
当前提交
5ef841a0e2

+ 2 - 0
ambari-web/app/assets/test/tests.js

@@ -126,6 +126,8 @@ require('test/views/main/charts/heatmap/heatmap_host_test');
 require('test/views/main/charts/heatmap/heatmap_rack_test');
 require('test/views/main/service/info/config_test');
 require('test/views/common/configs/services_config_test');
+require('test/views/wizard/step3/hostLogPopupBody_view_test');
+require('test/views/wizard/step3/hostWarningPopupFooter_view_test');
 require('test/views/wizard/step0_view_test');
 require('test/views/wizard/step1_view_test');
 require('test/views/wizard/step2_view_test');

文件差异内容过多而无法显示
+ 395 - 191
ambari-web/app/controllers/wizard/step3_controller.js


+ 119 - 63
ambari-web/app/controllers/wizard/step4_controller.js

@@ -22,14 +22,24 @@ var stringUtils = require('utils/string_utils');
 App.WizardStep4Controller = Em.ArrayController.extend({
 
   name: 'wizardStep4Controller',
+
+  /**
+   * List of Services
+   * @type {Object[]}
+   */
   content: [],
 
-  isSubmitDisabled:function(){
+  /**
+   * Is Submit button disabled
+   * @type {bool}
+   */
+  isSubmitDisabled: function () {
     return this.filterProperty('isSelected', true).filterProperty('isInstalled', false).length === 0;
   }.property("@each.isSelected"),
 
   /**
    * Check whether all properties are selected
+   * @type {bool}
    */
   isAll: function () {
     return this.filterProperty('canBeSelected', true).everyProperty('isSelected', true);
@@ -37,13 +47,76 @@ App.WizardStep4Controller = Em.ArrayController.extend({
 
   /**
    * Check whether none properties(minimum) are selected
+   * @type {bool}
    */
   isMinimum: function () {
     return this.filterProperty('isDisabled', false).everyProperty('isSelected', false);
   }.property('@each.isSelected'),
 
+  /**
+   * submit checks describe dependency rules between services
+   * checkCallback - callback, which check for dependency
+   * popupParams - parameters for popup
+   * @type {{checkCallback: string, popupParams: Ember.Enumerable}[]}
+   */
+  submitChecks: [
+    {
+      checkCallback: 'needToAddMapReduce',
+      popupParams: [
+        {serviceName: 'MAPREDUCE', selected: true},
+        'mapreduceCheck'
+      ]
+    },
+    {
+      checkCallback: 'noDFSs',
+      popupParams: [
+        {serviceName: 'HDFS', selected: true},
+        'hdfsCheck'
+      ]
+    },
+    {
+      checkCallback: 'needToAddYarnMapReduce2',
+      popupParams: [
+        {serviceName: 'YARN', selected: true},
+        'yarnCheck'
+      ]
+    },
+    {
+      checkCallback: 'needToAddZooKeeper',
+      popupParams: [
+        {serviceName: 'ZOOKEEPER', selected: true},
+        'zooKeeperCheck'
+      ]
+    },
+    {
+      checkCallback: 'multipleDFSs',
+      popupParams: [
+        [
+          {serviceName: 'HDFS', selected: true},
+          {serviceName: 'GLUSTERFS', selected: false}
+        ],
+        'multipleDFS'
+      ]
+    },
+    {
+      checkCallback: 'needToAddOozie',
+      popupParams: [
+        {serviceName: 'OOZIE', selected: true},
+        'oozieCheck'
+      ]
+    },
+    {
+      checkCallback: 'needToAddTez',
+      popupParams: [
+        {serviceName: 'TEZ', selected: true},
+        'tezCheck'
+      ]
+    }
+  ],
+
   /**
    * Update hidden services. Make them to have the same status as master ones.
+   * @method checkDependencies
    */
   checkDependencies: function () {
     var services = {};
@@ -63,13 +136,15 @@ App.WizardStep4Controller = Em.ArrayController.extend({
 
   /**
    * Onclick handler for <code>select all</code> link
+   * @method selectAll
    */
   selectAll: function () {
     this.filterProperty('canBeSelected', true).setEach('isSelected', true);
   },
 
   /**
-   * onclick handler for <code>select minimum</code> link
+   * Onclick handler for <code>select minimum</code> link
+   * @method selectMinimum
    */
   selectMinimum: function () {
     this.filterProperty('isDisabled', false).setEach('isSelected', false);
@@ -79,9 +154,10 @@ App.WizardStep4Controller = Em.ArrayController.extend({
    * Check whether we should turn on <code>serviceName</code> service according to selected <code>dependentServices</code>
    * @param serviceName checked service
    * @param dependentServices list of dependent services
-   * @returns {boolean}
+   * @returns {bool}
+   * @method needAddService
    */
-  needAddService: function(serviceName, dependentServices) {
+  needAddService: function (serviceName, dependentServices) {
     if (!(dependentServices instanceof Array)) {
       dependentServices = [dependentServices];
     }
@@ -96,7 +172,8 @@ App.WizardStep4Controller = Em.ArrayController.extend({
 
   /**
    * Check whether we should turn on <code>Oozie</code> service
-   * @return {Boolean}
+   * @return {bool}
+   * @method needToAddOozie
    */
   needToAddOozie: function () {
     return this.needAddService('OOZIE', ['FALCON']);
@@ -104,28 +181,35 @@ App.WizardStep4Controller = Em.ArrayController.extend({
 
   /**
    * Check whether we should turn on <code>MapReduce</code> service
-   * @return {Boolean}
+   * @return {bool}
+   * @method needToAddMapReduce
    */
   needToAddMapReduce: function () {
     return this.needAddService('MAPREDUCE', ['PIG', 'OOZIE', 'HIVE']);
   },
+
   /**
    * Check whether we should turn on <code>MapReduce2</code> service
-   * @return {Boolean}
+   * @return {bool}
+   * @method needToAddYarnMapReduce2
    */
-  needToAddYarnMapReduce2: function() {
-    return this.needAddService('YARN', ['PIG', 'OOZIE', 'HIVE','TEZ']);
+  needToAddYarnMapReduce2: function () {
+    return this.needAddService('YARN', ['PIG', 'OOZIE', 'HIVE', 'TEZ']);
   },
+
   /**
    * Check whether we should turn on <code>Tez</code> service
-   * @return {Boolean}
+   * @return {bool}
+   * @method needToAddTez
    */
-  needToAddTez: function() {
+  needToAddTez: function () {
     return this.needAddService('TEZ', ['YARN']);
   },
+
   /**
    * Check whether we should turn on <code>ZooKeeper</code> service
-   * @return {Boolean}
+   * @return {bool}
+   * @method needToAddZooKeeper
    */
   needToAddZooKeeper: function () {
     if (App.get('isHadoop2Stack')) {
@@ -134,27 +218,31 @@ App.WizardStep4Controller = Em.ArrayController.extend({
       return this.needAddService('ZOOKEEPER', ['HBASE', 'HIVE', 'WEBHCAT', 'STORM']);
     }
   },
-  /** 
+
+  /**
    * Check whether we should turn on <code>HDFS or GLUSTERFS</code> service
-   * @return {Boolean}
+   * @return {bool}
+   * @method noDFSs
    */
   noDFSs: function () {
     return (this.findProperty('serviceName', 'HDFS').get('isSelected') === false &&
-    		(!this.findProperty('serviceName', 'GLUSTERFS') || this.findProperty('serviceName', 'GLUSTERFS').get('isSelected') === false));
+      (!this.findProperty('serviceName', 'GLUSTERFS') || this.findProperty('serviceName', 'GLUSTERFS').get('isSelected') === false));
   },
 
-  /** 
+  /**
    * Check if multiple distributed file systems were selected
-   * @return {Boolean}
+   * @return {bool}
+   * @method multipleDFSs
    */
   multipleDFSs: function () {
-	  return (this.findProperty('serviceName', 'HDFS').get('isSelected') === true &&
-	    	(this.findProperty('serviceName', 'GLUSTERFS') && this.findProperty('serviceName', 'GLUSTERFS').get('isSelected') === true));
+    return (this.findProperty('serviceName', 'HDFS').get('isSelected') === true &&
+      (this.findProperty('serviceName', 'GLUSTERFS') && this.findProperty('serviceName', 'GLUSTERFS').get('isSelected') === true));
   },
 
   /**
    * Check do we have any monitoring service turned on
-   * @return {Boolean}
+   * @return {bool}
+   * @method gangliaOrNagiosNotSelected
    */
   gangliaOrNagiosNotSelected: function () {
     return (this.findProperty('serviceName', 'GANGLIA').get('isSelected') === false || this.findProperty('serviceName', 'NAGIOS').get('isSelected') === false);
@@ -162,6 +250,7 @@ App.WizardStep4Controller = Em.ArrayController.extend({
 
   /**
    * Check whether user turned on monitoring service and go to next step
+   * @method validateMonitoring
    */
   validateMonitoring: function () {
     if (this.gangliaOrNagiosNotSelected()) {
@@ -171,47 +260,9 @@ App.WizardStep4Controller = Em.ArrayController.extend({
     }
   },
 
-  /**
-   * submit checks describe dependency rules between services
-   * @checkCallback - callback, which check for dependency
-   * @popupParams - parameters for popup
-   */
-  submitChecks: [
-    {
-      checkCallback: 'needToAddMapReduce',
-      popupParams: [{serviceName: 'MAPREDUCE', selected: true}, 'mapreduceCheck']
-    },
-    {
-      checkCallback: 'noDFSs',
-      popupParams: [{serviceName:'HDFS', selected: true}, 'hdfsCheck']
-    },
-    {
-      checkCallback: 'needToAddYarnMapReduce2',
-      popupParams: [{serviceName:'YARN', selected:true}, 'yarnCheck']
-    },
-    {
-      checkCallback: 'needToAddZooKeeper',
-      popupParams: [{serviceName:'ZOOKEEPER', selected: true}, 'zooKeeperCheck']
-    },
-    {
-      checkCallback: 'multipleDFSs',
-      popupParams: [[
-        {serviceName: 'HDFS', selected: true},
-        {serviceName: 'GLUSTERFS', selected: false}
-      ], 'multipleDFS']
-    },
-    {
-      checkCallback: 'needToAddOozie',
-      popupParams: [{serviceName:'OOZIE', selected: true}, 'oozieCheck']
-    },
-    {
-      checkCallback: 'needToAddTez',
-      popupParams: [{serviceName:'TEZ', selected: true}, 'tezCheck']
-    }
-  ],
-
   /**
    * Onclick handler for <code>Next</code> button
+   * @method submit
    */
   submit: function () {
     var submitChecks = this.get('submitChecks');
@@ -229,7 +280,7 @@ App.WizardStep4Controller = Em.ArrayController.extend({
       }
     }
   },
-  
+
   /**
    * Select/deselect services
    * @param services array of objects
@@ -242,9 +293,10 @@ App.WizardStep4Controller = Em.ArrayController.extend({
    *      ....
    *    ]
    *  </code>
-   * @param i18nSuffix
+   * @param {string} i18nSuffix
+   * @method needToAddServicePopup
    */
-  needToAddServicePopup: function(services, i18nSuffix) {
+  needToAddServicePopup: function (services, i18nSuffix) {
     if (!(services instanceof Array)) {
       services = [services];
     }
@@ -253,7 +305,7 @@ App.WizardStep4Controller = Em.ArrayController.extend({
       header: Em.I18n.t('installer.step4.' + i18nSuffix + '.popup.header'),
       body: Em.I18n.t('installer.step4.' + i18nSuffix + '.popup.body'),
       onPrimary: function () {
-        services.forEach(function(service) {
+        services.forEach(function (service) {
           self.findProperty('serviceName', service.serviceName).set('isSelected', service.selected);
         });
         this.hide();
@@ -262,6 +314,10 @@ App.WizardStep4Controller = Em.ArrayController.extend({
     });
   },
 
+  /**
+   * Show popup with info about not selected (but should be selected) services
+   * @method monitoringCheckPopup
+   */
   monitoringCheckPopup: function () {
     App.ModalPopup.show({
       header: Em.I18n.t('installer.step4.monitoringCheck.popup.header'),

+ 97 - 99
ambari-web/app/templates/wizard/step3.hbs

@@ -20,20 +20,19 @@
   <h2>{{t installer.step3.header}}</h2>
 
   <p class="alert alert-info">{{t installer.step3.body}}</p>
+
   <div class="box">
     <div class="box-header">
       <div class="button-section">
         <button class="btn btn-primary" {{bindAttr disabled="view.noHostsSelected"}}
-           {{action removeSelectedHosts target="controller" }}><i
-                class="icon-trash icon-white"></i>
+          {{action removeSelectedHosts target="controller" }}><i class="icon-trash icon-white"></i>
           {{t installer.step3.removeSelected}}
         </button>
         {{#unless isRetryDisabled}}
-        <a class="btn btn-primary decommission"
-           href="#" {{action retrySelectedHosts target="view"}}><i
-          class="icon-repeat icon-white"></i>
-          {{t installer.step3.retryFailed}}
-        </a>
+          <a class="btn btn-primary decommission"
+             href="#" {{action retrySelectedHosts target="view"}}><i class="icon-repeat icon-white"></i>
+            {{t installer.step3.retryFailed}}
+          </a>
         {{/unless}}
 
         <div id="host-filter" class="pull-right">
@@ -54,100 +53,99 @@
       </div>
     </div>
 
-      <div class="pre-scrollable" style="max-height: 440px;">
-          <table class="table table-bordered table-striped">
-              <thead>
-              <tr>
-                  <th class="tinyspan">
-                    {{view Ember.Checkbox checkedBinding="view.pageChecked"}}
-                  </th>
-                  <th class="span3">{{t common.host}}</th>
-                  <!-- retrieved from local storage initially -->
-                  <th class="span3">{{t common.progress}}</th>
-                  <th class="span2">{{t common.status}}</th>
-                  <!-- given by the parsing function that parses data from bootstrap call, dynamically assign the color -->
-                  <th class="span2">{{t common.action}}</th>
-                  <!-- trash icon -->
-                  <!-- retry icon -->
-              </tr>
-              </thead>
-              <tbody>
-              {{#if view.pageContent}}
-                {{#each host in view.pageContent}}
-                  {{#view App.WizardHostView categoryBinding="controller.category" hostInfoBinding="host"}}
-                      <td>
-                        {{view Ember.Checkbox checkedBinding="host.isChecked"}}
-                      </td>
-                      <td>
-                        {{host.name}}
-                      </td>
-                      <td>
-                          <div {{bindAttr class="host.bootBarColor host.isBootDone::progress-striped host.isBootDone::active :progress"}}>
-                              <div class="bar" style="width:100%">
-                              </div>
-                          </div>
-                      </td>
-                      <td>
-                          <a href="javascript:void(null)"
-                             data-toggle="modal" {{action hostLogPopup host target="controller"}}><span  {{bindAttr class="host.bootStatusColor"}}>{{host.bootStatusForDisplay}}</span></a>
-                      </td>
-                      <td>
-                        {{#if view.isRemovable}}<a class="btn btn-mini" {{action remove target="view"}}><i
-                                class="icon-trash"></i>
-                          {{t common.remove}}</a>{{/if}}
-                        {{#if view.isRetryable}}<a class="btn btn-mini" {{action retry target="view"}}><i
-                                class="icon-repeat"></i>
-                          {{t common.retry}}</a>{{/if}}
-                      </td>
-                  {{/view}}
-                {{/each}}
-              {{else}}
-                  <tr>
-                      <td colspan="5">
-                        {{t hosts.table.noHosts}}
-                      </td>
-                  </tr>
-              {{/if}}
-              </tbody>
-          </table>
-      </div>
-      <div id="hosts">
-          <div class="page-bar">
-              <div class="selected-hosts-info pull-left">
-                {{#if view.selectedHostsCount}}
-                  <a {{action selectedHostsPopup target="controller"}} href="#">
-                    {{view.selectedHostsCount}}
-                    {{pluralize view.selectedHostsCount singular="t:hosts.filters.selectedHostInfo" plural="t:hosts.filters.selectedHostsInfo"}}
-                  </a>
-                  -
-                  <a {{action unSelectAll target="view"}} href="#">{{t hosts.filters.clearSelection}}</a>
-              {{/if}}
-              </div>
-              <div class="items-on-page">
-                  <label>{{t common.show}}: {{view view.rowsPerPageSelectView selectionBinding="view.displayLength"}}</label>
-              </div>
-              <div class="info">{{view.paginationInfo}}</div>
-              <div class="paging_two_button">
-                {{view view.paginationFirst}}
-                {{view view.paginationLeft}}
-                {{view view.paginationRight}}
-                {{view view.paginationLast}}
-              </div>
-          </div>
-      </div>
-  </div>
-    {{#if hasMoreRegisteredHosts}}
-        <div {{bindAttr class=":alert alert-warn"}}>
-          <a href="#" {{action registeredHostsPopup target="controller"}}>{{view.registeredHostsMessage}}</a>
+    <div class="pre-scrollable" style="max-height: 440px;">
+      <table class="table table-bordered table-striped">
+        <thead>
+        <tr>
+          <th class="tinyspan">
+            {{view Ember.Checkbox checkedBinding="view.pageChecked"}}
+          </th>
+          <th class="span3">{{t common.host}}</th>
+          <!-- retrieved from local storage initially -->
+          <th class="span3">{{t common.progress}}</th>
+          <th class="span2">{{t common.status}}</th>
+          <!-- given by the parsing function that parses data from bootstrap call, dynamically assign the color -->
+          <th class="span2">{{t common.action}}</th>
+          <!-- trash icon -->
+          <!-- retry icon -->
+        </tr>
+        </thead>
+        <tbody>
+          {{#if view.pageContent}}
+            {{#each host in view.pageContent}}
+              {{#view App.WizardHostView categoryBinding="controller.category" hostInfoBinding="host"}}
+              <td>
+                {{view Ember.Checkbox checkedBinding="host.isChecked"}}
+              </td>
+              <td>
+                {{host.name}}
+              </td>
+              <td>
+                <div {{bindAttr class="host.bootBarColor host.isBootDone::progress-striped host.isBootDone::active :progress"}}>
+                  <div class="bar" style="width:100%">
+                  </div>
+                </div>
+              </td>
+              <td>
+                <a href="javascript:void(null)"
+                   data-toggle="modal" {{action hostLogPopup host target="controller"}}><span {{bindAttr class="host.bootStatusColor"}}>{{host.bootStatusForDisplay}}</span></a>
+              </td>
+              <td>
+                <a class="btn btn-mini" {{action remove target="view"}}><i class="icon-trash"></i>
+                  {{t common.remove}}</a>
+                {{#if view.isRetryable}}<a class="btn btn-mini" {{action retry target="view"}}><i
+                    class="icon-repeat"></i>
+                  {{t common.retry}}</a>{{/if}}
+              </td>
+              {{/view}}
+            {{/each}}
+          {{else}}
+          <tr>
+            <td colspan="5">
+              {{t hosts.table.noHosts}}
+            </td>
+          </tr>
+          {{/if}}
+        </tbody>
+      </table>
+    </div>
+    <div id="hosts">
+      <div class="page-bar">
+        <div class="selected-hosts-info pull-left">
+          {{#if view.selectedHostsCount}}
+            <a {{action selectedHostsPopup target="controller"}} href="#">
+              {{view.selectedHostsCount}}
+              {{pluralize view.selectedHostsCount singular="t:hosts.filters.selectedHostInfo" plural="t:hosts.filters.selectedHostsInfo"}}
+            </a>
+            -
+            <a {{action unSelectAll target="view"}} href="#">{{t hosts.filters.clearSelection}}</a>
+          {{/if}}
+        </div>
+        <div class="items-on-page">
+          <label>{{t common.show}}: {{view view.rowsPerPageSelectView selectionBinding="view.displayLength"}}</label>
+        </div>
+        <div class="info">{{view.paginationInfo}}</div>
+        <div class="paging_two_button">
+          {{view view.paginationFirst}}
+          {{view view.paginationLeft}}
+          {{view view.paginationRight}}
+          {{view view.paginationLast}}
         </div>
-    {{/if}}
-      <div {{bindAttr class=":alert view.status isWarningsBoxVisible::hidden"}}>
-        {{view.message}}
-        <a href="#" {{action hostWarningsPopup warnings target="controller"}}>{{view.linkText}}</a>
-        {{#unless isWarningsLoaded}}
-          <div class="spinner"></div>
-        {{/unless}}
       </div>
+    </div>
+  </div>
+  {{#if hasMoreRegisteredHosts}}
+    <div {{bindAttr class=":alert alert-warn"}}>
+      <a href="#" {{action registeredHostsPopup target="controller"}}>{{view.registeredHostsMessage}}</a>
+    </div>
+  {{/if}}
+  <div {{bindAttr class=":alert view.status isWarningsBoxVisible::hidden"}}>
+    {{view.message}}
+    <a href="#" {{action hostWarningsPopup warnings target="controller"}}>{{view.linkText}}</a>
+    {{#unless isWarningsLoaded}}
+      <div class="spinner"></div>
+    {{/unless}}
+  </div>
   <div class="btn-area">
     <button class="btn pull-left" {{bindAttr disabled="isRegistrationInProgress"}} {{action back}}>&larr; {{t common.back}}</button>
     <button class="btn btn-success pull-right" {{bindAttr disabled="isSubmitDisabled"}} {{action submit target="controller"}}>{{t common.next}} &rarr;</button>

+ 5 - 5
ambari-web/app/templates/wizard/step3_host_log_popup.hbs → ambari-web/app/templates/wizard/step3/step3_host_log_popup.hbs

@@ -19,13 +19,13 @@
 
 <div id="host-log">
   <div class="content-area">
-      <div class="textTrigger">{{t popup.highlight}}</div>
+    <div class="textTrigger">{{t popup.highlight}}</div>
     {{#if view.isTextArea}}
-    <div>
-      {{view view.textArea contentBinding="view.host.bootLog"}}
-    </div>
+      <div>
+        {{view view.textArea contentBinding="view.bootLog"}}
+      </div>
     {{else}}
-    <pre class="bootLog">{{view.host.bootLog}}</pre>
+      <pre class="bootLog">{{view.bootLog}}</pre>
     {{/if}}
   </div>
 </div>

+ 3 - 1
ambari-web/app/templates/wizard/step3_host_warning_popup_footer.hbs → ambari-web/app/templates/wizard/step3/step3_host_warning_popup_footer.hbs

@@ -26,7 +26,9 @@
   {{/if}}
 </div>
 {{#if view.parentView.secondary}}
-  <button type="button" class="btn btn-info" {{bindAttr disabled="view.isUpdateInProgress"}} {{action onSecondary target="view.parentView"}}><i class="icon-repeat"></i>&nbsp;{{view.parentView.secondary}}</button>
+  <button type="button"
+          class="btn btn-info" {{bindAttr disabled="view.isUpdateInProgress"}} {{action onSecondary target="view.parentView"}}>
+    <i class="icon-repeat"></i>&nbsp;{{view.parentView.secondary}}</button>
 {{/if}}
 {{#if view.parentView.primary}}
   <button type="button" class="btn" {{action onPrimary target="view.parentView"}}>{{view.parentView.primary}}</button>

+ 94 - 0
ambari-web/app/templates/wizard/step3/step3_host_warnings_popup.hbs

@@ -0,0 +1,94 @@
+{{!
+* Licensed to the Apache Software Foundation (ASF) under one
+* or more contributor license agreements.  See the NOTICE file
+* distributed with this work for additional information
+* regarding copyright ownership.  The ASF licenses this file
+* to you under the Apache License, Version 2.0 (the
+* "License"); you may not use this file except in compliance
+* with the License.  You may obtain a copy of the License at
+*
+*     http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+}}
+
+
+<div id="host-warnings">
+  <div class="notice">
+    <span>{{t installer.step3.hostWarningsPopup.checks}} <b>{{view.warningsNotice}}</b>.<br>{{t installer.step3.hostWarningsPopup.notice}}</span>
+  </div>
+  <div class="row-fluid">
+    <div class="span7">
+      {{t common.hosts}}&nbsp;{{view view.hostSelectView}}
+    </div>
+    {{#if view.categoryWarnings.length}}
+      <div class="span3 offset2">
+        <a href="javascript:void(null)" title="Show Details" {{action openWarningsInDialog target="view"}}
+           class="task-detail-open-dialog"><i
+            class="icon-external-link"></i> {{t installer.step3.hostWarningsPopup.report}}</a>
+      </div>
+    {{/if}}
+  </div>
+  <div class="accordion warnings-list" id="accordion2">
+    {{#each category in view.content}}
+      <div class="accordion-group block">
+        <div class="accordion-heading" {{action onToggleBlock category}}>
+          <i {{bindAttr class=":pull-left :accordion-toggle category.isCollapsed:icon-caret-right:icon-caret-down"}}></i>
+          {{#if category.warnings.length}}
+            <i class="pull-right accordion-toggle icon-warning-sign"></i>
+          {{else}}
+            <i class="pull-right accordion-toggle icon-ok"></i>
+          {{/if}}
+          <a class="accordion-toggle">
+            {{category.title}} ({{category.warnings.length}})
+          </a>
+        </div>
+        <div id="{{unbound category.category}}" class="accordion-body collapse in" style="display: none">
+          <div class="accordion-inner">
+            {{#if category.warnings.length}}
+              <table>
+                <thead>
+                <tr>
+                  <th colspan="2">{{{category.message}}}</th>
+                </tr>
+                <tr>
+                  <th colspan="2"><b>{{category.type}}</b></th>
+                </tr>
+                </thead>
+                <tbody>
+                  {{#each warning in category.warnings}}
+                  <tr>
+                    <td class="warning-name" {{bindAttr data-original-title="warning.command"}} >{{{warning.name}}}</td>
+                    {{#if warning.version}}
+                      <td class="package-version">{{warning.version}}</td>
+                    {{/if}}
+                    {{#if warning.target}}
+                      <td class="package-version">{{warning.target}}</td>
+                    {{/if}}
+                    <td>{{category.action}}
+                      <a href="javascript:void(null);" rel='HostsListTooltip' {{bindAttr data-original-title="warning.hostsList"}} {{action showHostsPopup warning.hosts}}>
+                        {{warning.hosts.length}}
+                        {{#if warning.onSingleHost}}
+                          {{t installer.step3.hostWarningsPopup.host}}
+                        {{else}}
+                          {{t installer.step3.hostWarningsPopup.hosts}}
+                        {{/if}}
+                      </a>
+                    </td>
+                  </tr>
+                  {{/each}}
+                </tbody>
+              </table>
+            {{else}}
+              {{t installer.step3.hostWarningsPopup.emptyMessage}} {{category.emptyName}}
+            {{/if}}
+          </div>
+        </div>
+      </div>
+    {{/each}}
+  </div>
+</div>

+ 0 - 0
ambari-web/app/templates/wizard/step3_registered_hosts_popup.hbs → ambari-web/app/templates/wizard/step3/step3_registered_hosts_popup.hbs


+ 0 - 94
ambari-web/app/templates/wizard/step3_host_warnings_popup.hbs

@@ -1,94 +0,0 @@
-{{!
-* Licensed to the Apache Software Foundation (ASF) under one
-* or more contributor license agreements.  See the NOTICE file
-* distributed with this work for additional information
-* regarding copyright ownership.  The ASF licenses this file
-* to you under the Apache License, Version 2.0 (the
-* "License"); you may not use this file except in compliance
-* with the License.  You may obtain a copy of the License at
-*
-*     http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-}}
-
-
-<div id="host-warnings">
-  <div class="notice">
-    <span>{{t installer.step3.hostWarningsPopup.checks}} <b>{{view.warningsNotice}}</b>.<br>{{t installer.step3.hostWarningsPopup.notice}}</span>
-  </div>
-  <div class="row-fluid">
-    <div class="span7">
-      {{t common.hosts}}&nbsp;{{view view.hostSelectView}}
-    </div>
-    {{#if view.categoryWarnings.length}}
-    <div class="span3 offset2">
-      <a href="javascript:void(null)" title="Show Details" {{action openWarningsInDialog target="view"}}
-         class="task-detail-open-dialog"><i
-              class="icon-external-link"></i> {{t installer.step3.hostWarningsPopup.report}}</a>
-    </div>
-    {{/if}}
-  </div>
-  <div class="accordion warnings-list" id="accordion2">
-    {{#each category in view.content}}
-    <div class="accordion-group block">
-      <div class="accordion-heading" {{action onToggleBlock category}}>
-        <i {{bindAttr class=":pull-left :accordion-toggle category.isCollapsed:icon-caret-right:icon-caret-down"}}></i>
-        {{#if category.warnings.length}}
-        <i class="pull-right accordion-toggle icon-warning-sign"></i>
-        {{else}}
-        <i class="pull-right accordion-toggle icon-ok"></i>
-        {{/if}}
-        <a class="accordion-toggle">
-          {{category.title}} ({{category.warnings.length}})
-        </a>
-      </div>
-      <div id="{{unbound category.category}}" class="accordion-body collapse in" style="display: none">
-        <div class="accordion-inner">
-          {{#if category.warnings.length}}
-          <table>
-            <thead>
-            <tr>
-              <th colspan="2">{{{category.message}}}</th>
-            </tr>
-            <tr>
-              <th colspan="2"><b>{{category.type}}</b></th>
-            </tr>
-            </thead>
-            <tbody>
-            {{#each warning in category.warnings}}
-            <tr>
-              <td class="warning-name" {{bindAttr data-original-title="warning.command"}} >{{{warning.name}}}</td>
-              {{#if warning.version}}
-                  <td class="package-version">{{warning.version}}</td>
-              {{/if}}
-              {{#if warning.target}}
-                <td class="package-version">{{warning.target}}</td>
-              {{/if}}
-              <td>{{category.action}}
-                <a href="javascript:void(null);" rel='HostsListTooltip' {{bindAttr data-original-title="warning.hostsList"}} {{action showHostsPopup warning.hosts}}>
-                  {{warning.hosts.length}}
-                  {{#if warning.onSingleHost}}
-                  {{t installer.step3.hostWarningsPopup.host}}
-                  {{else}}
-                  {{t installer.step3.hostWarningsPopup.hosts}}
-                  {{/if}}
-                </a>
-              </td>
-            </tr>
-            {{/each}}
-            </tbody>
-          </table>
-          {{else}}
-          {{t installer.step3.hostWarningsPopup.emptyMessage}} {{category.emptyName}}
-          {{/if}}
-        </div>
-      </div>
-    </div>
-    {{/each}}
-  </div>
-</div>

+ 13 - 16
ambari-web/app/templates/wizard/step4.hbs

@@ -28,9 +28,8 @@
       <th class="span3">{{t common.service}}
         <span style="margin-left:10px">
           <a href="#" {{action selectAll target="controller"}} {{bindAttr class="isAll:selected:deselected"}}>{{t all}}</a>
-                |
-                <a
-                  href="#" {{action selectMinimum target="controller"}} {{bindAttr class="isMinimum:selected:deselected"}}>{{t none}}</a>
+           |
+          <a href="#" {{action selectMinimum target="controller"}} {{bindAttr class="isMinimum:selected:deselected"}}>{{t none}}</a>
         </span>
       </th>
       <th>{{t common.version}}</th>
@@ -38,24 +37,22 @@
     </tr>
     </thead>
     <tbody>
-    {{#each controller}}
-    {{#unless isHidden}}
-    <tr {{bindAttr class="isSelected:success:"}}>
-      <td><label
-        class="checkbox">{{view Ember.Checkbox disabledBinding="isDisabled" checkedBinding="isSelected"}}{{displayName}}</label>
-      </td>
-      <td>{{version}}</td>
-      <td>{{{description}}}</td>
-    </tr>
-    {{/unless}}
-    {{/each}}
+      {{#each controller}}
+        {{#unless isHidden}}
+        <tr {{bindAttr class="isSelected:success:"}}>
+          <td><label class="checkbox">{{view Ember.Checkbox disabledBinding="isDisabled" checkedBinding="isSelected"}}{{displayName}}</label>
+          </td>
+          <td>{{version}}</td>
+          <td>{{{description}}}</td>
+        </tr>
+        {{/unless}}
+      {{/each}}
     </tbody>
   </table>
 
   <div class="btn-area">
     {{#unless view.parentView.controller.hideBackButton}}
-    <a class="btn pull-left" {{action back}}>&larr; {{t common.back}}
-    </a>
+      <a class="btn pull-left" {{action back}}>&larr; {{t common.back}}</a>
     {{/unless}}
     <a class="btn btn-success pull-right" {{bindAttr disabled="isSubmitDisabled"}} {{action submit target="controller"}}> {{t common.next}} &rarr;</a>
   </div>

+ 3 - 0
ambari-web/app/views.js

@@ -244,6 +244,9 @@ require('views/wizard/step0_view');
 require('views/wizard/step1_view');
 require('views/wizard/step2_view');
 require('views/wizard/step3_view');
+require('views/wizard/step3/hostLogPopupBody_view');
+require('views/wizard/step3/hostWarningPopupBody_view');
+require('views/wizard/step3/hostWarningPopupFooter_view');
 require('views/wizard/step4_view');
 require('views/wizard/step5_view');
 require('views/wizard/step6_view');

+ 87 - 0
ambari-web/app/views/wizard/step3/hostLogPopupBody_view.js

@@ -0,0 +1,87 @@
+/**
+ * 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');
+
+App.WizardStep3HostLogPopupBody = Em.View.extend({
+
+  templateName: require('templates/wizard/step3/step3_host_log_popup'),
+
+  /**
+   * Host's boot log
+   * @type {string}
+   */
+  bootLog: function() {
+    return this.get('parentView.host.bootLog');
+  }.property('parentView.host.bootLog'),
+
+  /**
+   * Is textarea view active
+   * @type {bool}
+   */
+  isTextArea: false,
+
+  /**
+   * Textbox with host's boot log
+   * @type {Ember.TextArea}
+   */
+  textArea: Em.TextArea.extend({
+
+    didInsertElement: function () {
+      var element = $(this.get('element'));
+      element.width($(this.get('parentView').get('element')).width() - 10);
+      element.height($(this.get('parentView').get('element')).height());
+      element.select();
+      element.css('resize', 'none');
+    },
+
+    /**
+     * Edit disabled
+     * @type {bool}
+     */
+    readOnly: true,
+
+    /**
+     * <code>parentView.bootLog</code>
+     * @type {string}
+     */
+    value: function () {
+      return this.get('content');
+    }.property('content')
+
+  }),
+
+  didInsertElement: function () {
+    var self = this;
+    var button = $(this.get('element')).find('.textTrigger');
+    button.click(function () {
+      $(this).text(self.get('isTextArea') ? Em.I18n.t('installer.step3.hostLogPopup.highlight') : Em.I18n.t('installer.step3.hostLogPopup.copy'));
+      self.set('isTextArea', !self.get('isTextArea'));
+    });
+    $(this.get('element')).find('.content-area').mouseenter(
+      function () {
+        $(this).css('border', '1px solid #dcdcdc');
+        button.css('visibility', 'visible');
+      }).mouseleave(
+      function () {
+        $(this).css('border', 'none');
+        button.css('visibility', 'hidden');
+      })
+  }
+
+});

+ 386 - 0
ambari-web/app/views/wizard/step3/hostWarningPopupBody_view.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');
+var lazyloading = require('utils/lazy_loading');
+var numberUtils = require('utils/number_utils');
+
+App.WizardStep3HostWarningPopupBody = Em.View.extend({
+
+  templateName: require('templates/wizard/step3/step3_host_warnings_popup'),
+
+  classNames: ['host-check'],
+
+  bodyControllerBinding: 'App.router.wizardStep3Controller',
+
+  /**
+   * Listbox with host filters
+   * @type {Ember.Select}
+   */
+  hostSelectView: Em.Select.extend({
+
+    selectionBinding: "parentView.category",
+
+    /**
+     * Content has default value "All Hosts" to bind selection to category
+     * @type {string[]}
+     */
+    content: ['All Hosts'],
+
+    /**
+     * List of filtered hostnames
+     * @type {string[]}
+     */
+    hosts: function () {
+      return this.get('parentView.warningsByHost').mapProperty('name');
+    }.property('parentView.warningsByHost'),
+
+    /**
+     * Is data loaded
+     * @type {bool}
+     */
+    isLoaded: false,
+
+    didInsertElement: function () {
+      this.initContent();
+    },
+
+    /**
+     * Check browser and set content for listbox
+     * @method initContent
+     */
+    initContent: function () {
+      this.set('isLoaded', false);
+      //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('hosts').length > 100;
+      this.set('isLazyLoading', isLazyLoading);
+      if (isLazyLoading) {
+        //select need at least 30 hosts to have scrollbar
+        this.set('content', this.get('hosts').slice(0, 30));
+      } else {
+        this.set('content', this.get('hosts'));
+        this.set('isLoaded', true);
+      }
+    }.observes('parentView.warningsByHost'),
+
+    /**
+     * On click start lazy loading
+     * @method click
+     */
+    click: function () {
+      if (!this.get('isLoaded') && this.get('isLazyLoading')) {
+        //filter out hosts, which already pushed in select
+        var source = this.get('hosts').filter(function (_host) {
+          return !this.get('content').contains(_host);
+        }, this).slice();
+        lazyloading.run({
+          destination: this.get('content'),
+          source: source,
+          context: this,
+          initSize: 30,
+          chunkSize: 200,
+          delay: 50
+        });
+      }
+    }
+  }),
+
+  /**
+   * List of warnings grouped by host
+   * Same to <code>bodyController.warningsByHost</code>
+   * @type {Ember.Enumerable}
+   */
+  warningsByHost: function () {
+    return this.get('bodyController.warningsByHost');
+  }.property('bodyController.warningsByHost'),
+
+  /**
+   * List of all warnings
+   * Same to <code>bodyController.warnings</code>
+   * @type {Ember.Enumerable}
+   */
+  warnings: function () {
+    return this.get('bodyController.warnings');
+  }.property('bodyController.warnings'),
+
+  /**
+   * Selected category
+   * @type {string}
+   */
+  category: 'All Hosts',
+
+  /**
+   * List of warnings for selected <code>category</code>
+   * @type {Ember.Enumerable}
+   */
+  categoryWarnings: function () {
+    return this.get('warningsByHost').findProperty('name', this.get('category')).warnings
+  }.property('warningsByHost', 'category'),
+
+  /**
+   * List of warnings grouped by <code>category</code>
+   * @type {Ember.Object[]}
+   */
+  content: function () {
+    var repoCategoryWarnings = this.get('bodyController.repoCategoryWarnings');
+    var diskCategoryWarnings = this.get('bodyController.diskCategoryWarnings');
+    var categoryWarnings = this.get('categoryWarnings');
+    return [
+      Em.Object.create({
+        warnings: diskCategoryWarnings,
+        title: Em.I18n.t('installer.step3.hostWarningsPopup.disk'),
+        message: Em.I18n.t('installer.step3.hostWarningsPopup.disk.message'),
+        type: Em.I18n.t('common.issues'),
+        emptyName: Em.I18n.t('installer.step3.hostWarningsPopup.empty.disk'),
+        action: Em.I18n.t('installer.step3.hostWarningsPopup.action.exists'),
+        category: 'disk',
+        isCollapsed: true
+      }),
+      Em.Object.create({
+        warnings: repoCategoryWarnings,
+        title: Em.I18n.t('installer.step3.hostWarningsPopup.repositories'),
+        message: Em.I18n.t('installer.step3.hostWarningsPopup.repositories.message'),
+        type: Em.I18n.t('common.issues'),
+        emptyName: Em.I18n.t('installer.step3.hostWarningsPopup.empty.repositories'),
+        action: Em.I18n.t('installer.step3.hostWarningsPopup.action.invalid'),
+        category: 'repositories',
+        isCollapsed: true
+      }),
+      Em.Object.create({
+        warnings: categoryWarnings.filterProperty('category', 'firewall'),
+        title: Em.I18n.t('installer.step3.hostWarningsPopup.firewall'),
+        message: Em.I18n.t('installer.step3.hostWarningsPopup.firewall.message'),
+        type: Em.I18n.t('common.issues'),
+        emptyName: Em.I18n.t('installer.step3.hostWarningsPopup.empty.firewall'),
+        action: Em.I18n.t('installer.step3.hostWarningsPopup.action.running'),
+        category: 'firewall',
+        isCollapsed: true
+      }),
+      Em.Object.create({
+        warnings: categoryWarnings.filterProperty('category', 'processes'),
+        title: Em.I18n.t('installer.step3.hostWarningsPopup.process'),
+        message: Em.I18n.t('installer.step3.hostWarningsPopup.processes.message'),
+        type: Em.I18n.t('common.process'),
+        emptyName: Em.I18n.t('installer.step3.hostWarningsPopup.empty.processes'),
+        action: Em.I18n.t('installer.step3.hostWarningsPopup.action.running'),
+        category: 'process',
+        isCollapsed: true
+      }),
+      Em.Object.create({
+        warnings: categoryWarnings.filterProperty('category', 'packages'),
+        title: Em.I18n.t('installer.step3.hostWarningsPopup.package'),
+        message: Em.I18n.t('installer.step3.hostWarningsPopup.packages.message'),
+        type: Em.I18n.t('common.package'),
+        emptyName: Em.I18n.t('installer.step3.hostWarningsPopup.empty.packages'),
+        action: Em.I18n.t('installer.step3.hostWarningsPopup.action.installed'),
+        category: 'package',
+        isCollapsed: true
+      }),
+      Em.Object.create({
+        warnings: categoryWarnings.filterProperty('category', 'fileFolders'),
+        title: Em.I18n.t('installer.step3.hostWarningsPopup.fileAndFolder'),
+        message: Em.I18n.t('installer.step3.hostWarningsPopup.fileFolders.message'),
+        type: Em.I18n.t('common.path'),
+        emptyName: Em.I18n.t('installer.step3.hostWarningsPopup.empty.filesAndFolders'),
+        action: Em.I18n.t('installer.step3.hostWarningsPopup.action.exists'),
+        category: 'fileFolders',
+        isCollapsed: true
+      }),
+      Em.Object.create({
+        warnings: categoryWarnings.filterProperty('category', 'services'),
+        title: Em.I18n.t('installer.step3.hostWarningsPopup.service'),
+        message: Em.I18n.t('installer.step3.hostWarningsPopup.services.message'),
+        type: Em.I18n.t('common.service'),
+        emptyName: Em.I18n.t('installer.step3.hostWarningsPopup.empty.services'),
+        action: Em.I18n.t('installer.step3.hostWarningsPopup.action.notRunning'),
+        category: 'service',
+        isCollapsed: true
+      }),
+      Em.Object.create({
+        warnings: categoryWarnings.filterProperty('category', 'users'),
+        title: Em.I18n.t('installer.step3.hostWarningsPopup.user'),
+        message: Em.I18n.t('installer.step3.hostWarningsPopup.users.message'),
+        type: Em.I18n.t('common.user'),
+        emptyName: Em.I18n.t('installer.step3.hostWarningsPopup.empty.users'),
+        action: Em.I18n.t('installer.step3.hostWarningsPopup.action.exists'),
+        category: 'user',
+        isCollapsed: true
+      }),
+      Em.Object.create({
+        warnings: categoryWarnings.filterProperty('category', 'misc'),
+        title: Em.I18n.t('installer.step3.hostWarningsPopup.misc'),
+        message: Em.I18n.t('installer.step3.hostWarningsPopup.misc.message'),
+        type: Em.I18n.t('installer.step3.hostWarningsPopup.misc.umask'),
+        emptyName: Em.I18n.t('installer.step3.hostWarningsPopup.empty.misc'),
+        action: Em.I18n.t('installer.step3.hostWarningsPopup.action.exists'),
+        category: 'misc',
+        isCollapsed: true
+      }),
+      Em.Object.create({
+        warnings: categoryWarnings.filterProperty('category', 'alternatives'),
+        title: Em.I18n.t('installer.step3.hostWarningsPopup.alternatives'),
+        message: Em.I18n.t('installer.step3.hostWarningsPopup.alternatives.message'),
+        type: Em.I18n.t('installer.step3.hostWarningsPopup.alternatives.umask'),
+        emptyName: Em.I18n.t('installer.step3.hostWarningsPopup.alternatives.emptyinstaller.step3.hostWarningsPopup.alternatives.empty'),
+        action: Em.I18n.t('installer.step3.hostWarningsPopup.action.exists'),
+        category: 'alternatives',
+        isCollapsed: true
+      })
+    ]
+  }.property('category', 'warningsByHost'),
+
+  /**
+   * Message with info about warnings
+   * @return {string}
+   */
+  warningsNotice: function () {
+    var issuesNumber = this.get('warnings.length') + this.get('bodyController.repoCategoryWarnings.length') + this.get('bodyController.diskCategoryWarnings.length');
+    var issues = issuesNumber + ' ' + (issuesNumber.length === 1 ? Em.I18n.t('installer.step3.hostWarningsPopup.issue') : Em.I18n.t('installer.step3.hostWarningsPopup.issues'));
+    var hostsCnt = this.warningHostsNamesCount();
+    var hosts = hostsCnt + ' ' + (hostsCnt === 1 ? Em.I18n.t('installer.step3.hostWarningsPopup.host') : Em.I18n.t('installer.step3.hostWarningsPopup.hosts'));
+    return Em.I18n.t('installer.step3.hostWarningsPopup.summary').format(issues, hosts);
+  }.property('warnings', 'warningsByHost'),
+
+  /**
+   * Detailed content to show it in new window
+   * @return {string}
+   */
+  contentInDetails: function () {
+    var content = this.get('content');
+    var warningsByHost = this.get('warningsByHost').slice();
+    warningsByHost.shift();
+    var newContent = '';
+    newContent += Em.I18n.t('installer.step3.hostWarningsPopup.report.header') + new Date;
+    newContent += Em.I18n.t('installer.step3.hostWarningsPopup.report.hosts');
+    newContent += warningsByHost.filterProperty('warnings.length').mapProperty('name').join(' ');
+    if (content.findProperty('category', 'firewall').warnings.length) {
+      newContent += Em.I18n.t('installer.step3.hostWarningsPopup.report.firewall');
+      newContent += content.findProperty('category', 'firewall').warnings.mapProperty('name').join('<br>');
+    }
+    if (content.findProperty('category', 'fileFolders').warnings.length) {
+      newContent += Em.I18n.t('installer.step3.hostWarningsPopup.report.fileFolders');
+      newContent += content.findProperty('category', 'fileFolders').warnings.mapProperty('name').join(' ');
+    }
+    if (content.findProperty('category', 'process').warnings.length) {
+      newContent += Em.I18n.t('installer.step3.hostWarningsPopup.report.process');
+      content.findProperty('category', 'process').warnings.forEach(function (process, i) {
+        process.hosts.forEach(function (host, j) {
+          if (!!i || !!j) {
+            newContent += ',';
+          }
+          newContent += '(' + host + ',' + process.user + ',' + process.pid + ')';
+        });
+      });
+    }
+    if (content.findProperty('category', 'package').warnings.length) {
+      newContent += Em.I18n.t('installer.step3.hostWarningsPopup.report.package');
+      newContent += content.findProperty('category', 'package').warnings.mapProperty('name').join(' ');
+    }
+    if (content.findProperty('category', 'service').warnings.length) {
+      newContent += Em.I18n.t('installer.step3.hostWarningsPopup.report.service');
+      newContent += content.findProperty('category', 'service').warnings.mapProperty('name').join(' ');
+    }
+    if (content.findProperty('category', 'user').warnings.length) {
+      newContent += Em.I18n.t('installer.step3.hostWarningsPopup.report.user');
+      newContent += content.findProperty('category', 'user').warnings.mapProperty('name').join(' ');
+    }
+    newContent += '</p>';
+    return newContent;
+  }.property('content', 'warningsByHost'),
+
+  didInsertElement: function () {
+    Em.run.next(this, function () {
+      App.tooltip(this.$("[rel='HostsListTooltip']"), {html: true, placement: "right"});
+      App.tooltip(this.$('#process .warning-name'), {html: true, placement: "top"});
+    });
+  }.observes('content'),
+
+  /**
+   * Show popup with selected hostnames
+   * @param {object} hosts
+   * @method showHostsPopup
+   */
+  showHostsPopup: function (hosts) {
+    $('.tooltip').hide();
+    App.ModalPopup.show({
+      header: Em.I18n.t('installer.step3.hostWarningsPopup.allHosts'),
+      bodyClass: Em.View.extend({
+        hosts: hosts.context,
+        template: Em.Handlebars.compile('<ul>{{#each host in view.hosts}}<li>{{host}}</li>{{/each}}</ul>')
+      }),
+      secondary: null
+    });
+  },
+
+  /**
+   * Open/Close selected category
+   * @param {object} category
+   * @method onToggleBlock
+   */
+  onToggleBlock: function (category) {
+    this.$('#' + category.context.category).toggle('blind', 500);
+    category.context.set("isCollapsed", !category.context.get("isCollapsed"));
+  },
+
+  /**
+   * Generate number of hosts which had warnings, avoid duplicated host names in different warnings.
+   * @method warningHostsNamesCount
+   * @return {number}
+   */
+  warningHostsNamesCount: function () {
+    var hostNameMap = Em.Object.create();
+    var warningsByHost = this.get('bodyController.warningsByHost').slice();
+    warningsByHost.shift();
+    warningsByHost.forEach(function (_host) {
+      if (_host.warnings.length) {
+        hostNameMap[_host.name] = true;
+      }
+    });
+    var repoCategoryWarnings = this.get('bodyController.repoCategoryWarnings');
+    var diskCategoryWarnings = this.get('bodyController.diskCategoryWarnings');
+
+    if (repoCategoryWarnings.length) {
+      repoCategoryWarnings[0].hostsNames.forEach(function (_hostName) {
+        if (!hostNameMap[_hostName]) {
+          hostNameMap[_hostName] = true;
+        }
+      });
+    }
+    if (diskCategoryWarnings.length) {
+      diskCategoryWarnings[0].hostsNames.forEach(function (_hostName) {
+        if (!hostNameMap[_hostName]) {
+          hostNameMap[_hostName] = true;
+        }
+      });
+    }
+    return Em.keys(hostNameMap).length;
+  },
+
+  /**
+   * Open new browser tab with detailed content
+   * @method openWarningsInDialog
+   */
+  openWarningsInDialog: function () {
+    var newWindow = window.open('');
+    var newDocument = newWindow.document;
+    newDocument.write(this.get('contentInDetails'));
+    newWindow.focus();
+  }
+
+});

+ 64 - 0
ambari-web/app/views/wizard/step3/hostWarningPopupFooter_view.js

@@ -0,0 +1,64 @@
+/**
+ * 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');
+
+App.WizardStep3HostWarningPopupFooter = Em.View.extend({
+
+  templateName: require('templates/wizard/step3/step3_host_warning_popup_footer'),
+
+  classNames: ['modal-footer', 'host-checks-update'],
+
+  footerControllerBinding: 'App.router.wizardStep3Controller',
+
+  progressWidth: function () {
+    return 'width:' + this.get('footerController.checksUpdateProgress') + '%';
+  }.property('footerController.checksUpdateProgress'),
+
+  isUpdateInProgress: function () {
+    return (this.get('footerController.checksUpdateProgress') > 0) &&
+      (this.get('footerController.checksUpdateProgress') < 100);
+  }.property('footerController.checksUpdateProgress'),
+
+  updateStatusClass: function () {
+    var status = this.get('footerController.checksUpdateStatus');
+    if (status === 'SUCCESS') {
+      return 'text-success';
+    }
+    else {
+      if (status === 'FAILED') {
+        return 'text-error';
+      }
+      else {
+        return null;
+      }
+    }
+  }.property('footerController.checksUpdateStatus'),
+
+  updateStatus: function () {
+    var status = this.get('footerController.checksUpdateStatus');
+    if (status === 'SUCCESS') {
+      return Em.I18n.t('installer.step3.warnings.updateChecks.success');
+    } else if (status === 'FAILED') {
+      return Em.I18n.t('installer.step3.warnings.updateChecks.failed');
+    } else {
+      return null;
+    }
+  }.property('footerController.checksUpdateStatus')
+
+});

+ 165 - 68
ambari-web/app/views/wizard/step3_view.js

@@ -23,30 +23,95 @@ App.WizardStep3View = App.TableView.extend({
 
   templateName: require('templates/wizard/step3'),
 
+  /**
+   * List of hosts
+   * Same to <code>controller.hosts</code>
+   * @type {Ember.Enumerable}
+   */
   content:function () {
     return this.get('controller.hosts');
   }.property('controller.hosts.length'),
 
+  /**
+   * Message with info about registration result
+   * @type {string}
+   */
   message:'',
+
+  /**
+   * Text to link for hosts' warnings popup
+   * @type {string}
+   */
   linkText: '',
+
+  /**
+   * Registration status
+   * @type {string}
+   */
   status: '',
 
+  /**
+   * Active category
+   * @type {string}
+   */
   selectedCategory: function() {
     return this.get('categories').findProperty('isActive');
   }.property('categories.@each.isActive'),
 
+  /**
+   * Message about other registered hosts (not included in current registration)
+   * @type {string}
+   */
   registeredHostsMessage: '',
 
+  /**
+   * Number of visible hosts on page
+   * @type {string}
+   */
   displayLength: "25",
 
+  /**
+   * All checkboxes on page are checked
+   * @type {bool}
+   */
+  pageChecked: false,
+
+  /**
+   * bootStatus category object
+   * @type {Ember.Object}
+   */
+  categoryObject: Em.Object.extend({
+    hostsCount: 0,
+    label: function () {
+      return "%@ (%@)".fmt(this.get('value'), this.get('hostsCount'));
+    }.property('value', 'hostsCount'),
+    isActive: false,
+    itemClass: function () {
+      return this.get('isActive') ? 'active' : '';
+    }.property('isActive')
+  }),
+
+  /**
+   * List of bootStatus categories
+   * @type {categoryObject[]}
+   */
+  categories: function () {
+    return [
+      this.categoryObject.create({value: Em.I18n.t('common.all'), hostsBootStatus: 'ALL', isActive: true}),
+      this.categoryObject.create({value: Em.I18n.t('installer.step3.hosts.status.installing'), hostsBootStatus: 'RUNNING'}),
+      this.categoryObject.create({value: Em.I18n.t('installer.step3.hosts.status.registering'), hostsBootStatus: 'REGISTERING'}),
+      this.categoryObject.create({value: Em.I18n.t('common.success'), hostsBootStatus: 'REGISTERED' }),
+      this.categoryObject.create({value: Em.I18n.t('common.fail'), hostsBootStatus: 'FAILED', last: true })
+    ];
+  }.property(),
+
   didInsertElement: function () {
     this.get('controller').loadStep();
   },
 
-  pageChecked: false,
-
   /**
-   * select checkboxes of hosts on page
+   * Select checkboxes of hosts on page
+   * @method onPageChecked
    */
   onPageChecked: function () {
     if (this.get('selectionInProgress')) return;
@@ -54,28 +119,37 @@ App.WizardStep3View = App.TableView.extend({
   }.observes('pageChecked'),
 
   /**
-   * select checkboxes of all hosts
+   * Select checkboxes of all hosts
+   * @method selectAll
    */
   selectAll: function () {
     this.get('content').setEach('isChecked', true);
   },
 
   /**
-   * reset checkbox of all hosts
+   * Reset checkbox of all hosts
+   * @method unSelectAll
    */
   unSelectAll: function() {
     this.get('content').setEach('isChecked', false);
   },
 
+  /**
+   * Call <code>watchSelection</code> only once
+   * @method watchSelectionOnce
+   */
   watchSelectionOnce: function () {
     Em.run.once(this, 'watchSelection');
   }.observes('content.@each.isChecked', 'pageContent'),
 
   /**
-   * watch selection and calculate such flags as:
-   * - noHostsSelected
-   * - selectedHostsCount
-   * - pageChecked
+   * Watch selection and calculate such flags as:
+   * <ul>
+   *  <li>noHostsSelected</li>
+   *  <li>selectedHostsCount</li>
+   *  <li>pageChecked</li>
+   * </ul>
+   * @method watchSelection
    */
   watchSelection: function() {
     this.set('selectionInProgress', true);
@@ -84,44 +158,35 @@ App.WizardStep3View = App.TableView.extend({
     var noHostsSelected = true;
     var selectedHostsCount = 0;
     this.get('content').forEach(function(host){
-      selectedHostsCount += ~~host.get('isChecked');
+      selectedHostsCount += host.get('isChecked') ? 1 : 0;
       noHostsSelected = (noHostsSelected) ? !host.get('isChecked') : noHostsSelected;
     });
     this.set('noHostsSelected', noHostsSelected);
     this.set('selectedHostsCount', selectedHostsCount);
   },
 
+  /**
+   * Update <code>registeredHostsMessage</code> according to <code>controller.registeredHots.length</code>
+   * @method setRegisteredHosts
+   */
   setRegisteredHosts: function(){
     this.set('registeredHostsMessage',Em.I18n.t('installer.step3.warning.registeredHosts').format(this.get('controller.registeredHosts').length));
   }.observes('controller.registeredHosts'),
 
-  categoryObject: Em.Object.extend({
-    hostsCount: 0,
-    label: function () {
-      return "%@ (%@)".fmt(this.get('value'), this.get('hostsCount'));
-    }.property('value', 'hostsCount'),
-    isActive: false,
-    itemClass: function () {
-      return this.get('isActive') ? 'active' : '';
-    }.property('isActive')
-  }),
-
-  categories: function () {
-    return [
-      this.categoryObject.create({value: Em.I18n.t('common.all'), hostsBootStatus: 'ALL', isActive: true}),
-      this.categoryObject.create({value: Em.I18n.t('installer.step3.hosts.status.installing'), hostsBootStatus: 'RUNNING'}),
-      this.categoryObject.create({value: Em.I18n.t('installer.step3.hosts.status.registering'), hostsBootStatus: 'REGISTERING'}),
-      this.categoryObject.create({value: Em.I18n.t('common.success'), hostsBootStatus: 'REGISTERED' }),
-      this.categoryObject.create({value: Em.I18n.t('common.fail'), hostsBootStatus: 'FAILED', last: true })
-    ];
-  }.property(),
-
+  /**
+   * Call filters and counters one time
+   * @method hostBootStatusObserver
+   */
   hostBootStatusObserver: function(){
-    Ember.run.once(this, 'countCategoryHosts');
-    Ember.run.once(this, 'filter');
-    Ember.run.once(this, 'monitorStatuses');
+    Em.run.once(this, 'countCategoryHosts');
+    Em.run.once(this, 'filter');
+    Em.run.once(this, 'monitorStatuses');
   }.observes('content.@each.bootStatus'),
 
+  /**
+   * Calculate host count grouped by <code>bootStatus</code>
+   * @method countCategoryHosts
+   */
   countCategoryHosts: function () {
     var counters = {
       "RUNNING": 0,
@@ -130,7 +195,7 @@ App.WizardStep3View = App.TableView.extend({
       "FAILED": 0
     };
     this.get('content').forEach(function (host) {
-      if (counters[host.get('bootStatus')] !== undefined) {
+      if (!Em.isNone(counters[host.get('bootStatus')])) {
         counters[host.get('bootStatus')]++;
       }
     }, this);
@@ -142,7 +207,8 @@ App.WizardStep3View = App.TableView.extend({
 
 
   /**
-   * filter hosts by category
+   * Filter hosts by category
+   * @method filter
    */
   filter: function () {
     var self = this;
@@ -157,13 +223,14 @@ App.WizardStep3View = App.TableView.extend({
       self.set('filteredContent', result);
     });
   }.observes('selectedCategory'),
+
   /**
    * Trigger on Category click
    * @param {Object} event
+   * @method selectCategory
    */
   selectCategory: function (event) {
     var categoryStatus = event.context.get('hostsBootStatus');
-    var self = this;
     this.get('categories').forEach(function (category) {
       category.set('isActive', (category.get('hostsBootStatus') === categoryStatus));
     });
@@ -173,6 +240,7 @@ App.WizardStep3View = App.TableView.extend({
   /**
    * Select "All" hosts category
    * run registration of failed hosts again
+   * @method retrySelectedHosts
    */
   retrySelectedHosts: function () {
     var eventObject = {context: Em.Object.create({hostsBootStatus: 'ALL'})};
@@ -180,6 +248,10 @@ App.WizardStep3View = App.TableView.extend({
     this.get('controller').retrySelectedHosts();
   },
 
+  /**
+   * Update <code>status</code>, <code>linkText</code>, <code>message</code> according to hosts statuses
+   * @method monitorStatuses
+   */
   monitorStatuses: function() {
     var hosts = this.get('controller.bootHosts');
     var failedHosts = hosts.filterProperty('bootStatus', 'FAILED').length;
@@ -188,56 +260,81 @@ App.WizardStep3View = App.TableView.extend({
       this.set('status', 'alert-warn');
       this.set('linkText', '');
       this.set('message', Em.I18n.t('installer.step3.warnings.missingHosts'));
-    } else if (!this.get('controller.isWarningsLoaded')) {
-      this.set('status', 'alert-info');
-      this.set('linkText', '');
-      this.set('message', Em.I18n.t('installer.step3.warning.loading'));
-    } else if (this.get('controller.isHostHaveWarnings') || this.get('controller.repoCategoryWarnings.length') || this.get('controller.diskCategoryWarnings.length')) {
-      this.set('status', 'alert-warn');
-      this.set('linkText', Em.I18n.t('installer.step3.warnings.linkText'));
-      this.set('message', Em.I18n.t('installer.step3.warnings.fails').format(hosts.length - failedHosts));
-    } else {
-      this.set('status', 'alert-success');
-      this.set('linkText', Em.I18n.t('installer.step3.noWarnings.linkText'));
-      if (failedHosts == 0) {
-        // all are ok
-        this.set('message', Em.I18n.t('installer.step3.warnings.noWarnings').format(hosts.length));
-      } else if (failedHosts == hosts.length) {
-        // all failed
-        this.set('status', 'alert-warn');
+    }
+    else {
+      if (!this.get('controller.isWarningsLoaded')) {
+        this.set('status', 'alert-info');
         this.set('linkText', '');
-        this.set('message', Em.I18n.t('installer.step3.warnings.allFailed').format(failedHosts));
-      } else {
-        // some failed
-        this.set('message', Em.I18n.t('installer.step3.warnings.someWarnings').format((hosts.length - failedHosts), failedHosts));
+        this.set('message', Em.I18n.t('installer.step3.warning.loading'));
+      }
+      else {
+        if (this.get('controller.isHostHaveWarnings') || this.get('controller.repoCategoryWarnings.length') || this.get('controller.diskCategoryWarnings.length')) {
+          this.set('status', 'alert-warn');
+          this.set('linkText', Em.I18n.t('installer.step3.warnings.linkText'));
+          this.set('message', Em.I18n.t('installer.step3.warnings.fails').format(hosts.length - failedHosts));
+        }
+        else {
+          this.set('status', 'alert-success');
+          this.set('linkText', Em.I18n.t('installer.step3.noWarnings.linkText'));
+          if (failedHosts == 0) {
+            // all are ok
+            this.set('message', Em.I18n.t('installer.step3.warnings.noWarnings').format(hosts.length));
+          }
+          else {
+            if (failedHosts == hosts.length) {
+              // all failed
+              this.set('status', 'alert-warn');
+              this.set('linkText', '');
+              this.set('message', Em.I18n.t('installer.step3.warnings.allFailed').format(failedHosts));
+            }
+            else {
+              // some failed
+              this.set('message', Em.I18n.t('installer.step3.warnings.someWarnings').format((hosts.length - failedHosts), failedHosts));
+            }
+          }
+        }
       }
     }
   }.observes('controller.isWarningsLoaded', 'controller.isHostHaveWarnings', 'controller.repoCategoryWarnings', 'controller.diskCategoryWarnings')
+
 });
 
 //todo: move it inside WizardStep3View
 App.WizardHostView = Em.View.extend({
 
   tagName: 'tr',
+
   classNameBindings: ['hostInfo.bootStatus'],
+
+  /**
+   * Host from parent view
+   * @type {Object}
+   */
   hostInfo: null,
 
+  /**
+   * @type {bool}
+   */
+  isRetryable: function() {
+    // return ['FAILED'].contains(this.get('hostInfo.bootStatus'));
+    return false;
+  }.property('hostInfo.bootStatus'),
+
+  /**
+   * Remove selected host
+   * @method remove
+   */
   remove: function () {
     this.get('controller').removeHost(this.get('hostInfo'));
   },
 
+  /**
+   * Retry register selected host
+   * @method retry
+   */
   retry: function() {
     this.get('controller').retryHost(this.get('hostInfo'));
-  },
-
-  isRemovable: function () {
-    return true;
-  }.property(),
-
-  isRetryable: function() {
-    // return ['FAILED'].contains(this.get('hostInfo.bootStatus'));
-    return false;
-  }.property('hostInfo.bootStatus')
+  }
 
 });
 

+ 7 - 5
ambari-web/test/controllers/wizard/stack_upgrade/step3_controller_test.js

@@ -23,11 +23,13 @@ var Ember = require('ember');
 require('models/host');
 require('controllers/wizard/stack_upgrade/step3_controller');
 
-App.router = Ember.Object.create({
-  stackUpgradeController: Ember.Object.create({
-    save: function(val) {}
-  })
-});
+if (!App.router) {
+  App.router = Em.Object.create({});
+}
+
+App.router.set('stackUpgradeController', Em.Object.create({
+  save: Em.K
+}));
 
 describe('App.StackUpgradeStep3Controller', function() {
 

+ 1151 - 123
ambari-web/test/installer/step3_test.js

@@ -19,12 +19,20 @@
 
 var Ember = require('ember');
 var App = require('app');
+var c;
 require('utils/http_client');
 require('models/host');
 require('controllers/wizard/step3_controller');
 
 describe('App.WizardStep3Controller', function () {
 
+  beforeEach(function() {
+    c = App.WizardStep3Controller.create({
+      wizardController: App.InstallerController.create(),
+      disablePreviousSteps: Em.K
+    });
+  });
+
   describe('#getAllRegisteredHostsCallback', function () {
     it('One host is already in the cluster, one host is registered', function() {
       var controller = App.WizardStep3Controller.create({
@@ -114,129 +122,6 @@ describe('App.WizardStep3Controller', function () {
 
   });
 
-  var tests = [
-    {
-      bootHosts: [
-        Em.Object.create({name:'wst3_host1', bootStatus: 'REGISTERED', isChecked: false}),
-        Em.Object.create({name:'wst3_host2', bootStatus: 'REGISTERING', isChecked: false})
-      ],
-      m: 'One registered, one registering',
-      visibleHosts: {
-        RUNNING: {
-          e: 0
-        },
-        REGISTERING: {
-          e: 1
-        },
-        REGISTERED: {
-          e: 1
-        },
-        FAILED: {
-          e: 0
-        }
-      },
-      onAllChecked: {
-        e: [true, true]
-      }
-    },
-    {
-      bootHosts: [
-        Em.Object.create({name:'wst3_host1', bootStatus: 'REGISTERED', isChecked: false}),
-        Em.Object.create({name:'wst3_host2', bootStatus: 'REGISTERED', isChecked: false})
-      ],
-      m: 'Two registered',
-      visibleHosts: {
-        RUNNING: {
-          e: 0
-        },
-        REGISTERING: {
-          e: 0
-        },
-        REGISTERED: {
-          e: 2
-        },
-        FAILED: {
-          e: 0
-        }
-      },
-      onAllChecked: {
-        e: [true, true]
-      }
-    },
-    {
-      bootHosts: [
-        Em.Object.create({name:'wst3_host1', bootStatus: 'FAILED', isChecked: false}),
-        Em.Object.create({name:'wst3_host2', bootStatus: 'REGISTERED', isChecked: false})
-      ],
-      m: 'One registered, one failed',
-      visibleHosts: {
-        RUNNING: {
-          e: 0
-        },
-        REGISTERING: {
-          e: 0
-        },
-        REGISTERED: {
-          e: 1
-        },
-        FAILED: {
-          e: 1
-        }
-      },
-      onAllChecked: {
-        e: [true, true]
-      }
-    },
-    {
-      bootHosts: [
-        Em.Object.create({name:'wst3_host1', bootStatus: 'FAILED', isChecked: false}),
-        Em.Object.create({name:'wst3_host2', bootStatus: 'FAILED', isChecked: false})
-      ],
-      m: 'Two failed',
-      visibleHosts: {
-        RUNNING: {
-          e: 0
-        },
-        REGISTERING: {
-          e: 0
-        },
-        REGISTERED: {
-          e: 0
-        },
-        FAILED: {
-          e: 2
-        }
-      },
-      onAllChecked: {
-        e: [true, true]
-      }
-    },
-    {
-      bootHosts: [
-        Em.Object.create({name:'wst3_host1', bootStatus: 'REGISTERING', isChecked: false}),
-        Em.Object.create({name:'wst3_host2', bootStatus: 'REGISTERING', isChecked: false})
-      ],
-      m: 'Two registering',
-      visibleHosts: {
-        RUNNING: {
-          e: 0
-        },
-        REGISTERING: {
-          e: 2
-        },
-        REGISTERED: {
-          e: 0
-        },
-        FAILED: {
-          e: 0
-        }
-      },
-      onAllChecked: {
-        e: [true, true]
-      }
-    }
-  ];
-
   describe('#registrationTimeoutSecs', function() {
     it('Manual install', function() {
       var controller = App.WizardStep3Controller.create({
@@ -281,4 +166,1147 @@ describe('App.WizardStep3Controller', function () {
       });
     });
   });
+
+  describe('#isWarningsBoxVisible', function() {
+    it('for testMode should be always true', function() {
+      App.testMode = true;
+      expect(c.get('isWarningsBoxVisible')).to.equal(true);
+      App.testMode = false;
+    });
+    it('for "real" mode should be based on isRegistrationInProgress', function() {
+      c.set('disablePreviousSteps', Em.K);
+      App.testMode = false;
+      c.set('isRegistrationInProgress', false);
+      expect(c.get('isWarningsBoxVisible')).to.equal(true);
+      c.set('isRegistrationInProgress', true);
+      expect(c.get('isWarningsBoxVisible')).to.equal(false);
+      App.testMode = true;
+    });
+  });
+
+  describe('#clearStep', function() {
+    it('should clear hosts', function() {
+      c.set('hosts', [{}, {}]);
+      c.clearStep();
+      expect(c.get('hosts')).to.eql([]);
+    });
+    it('should clear bootHosts', function() {
+      c.set('bootHosts', [{}, {}]);
+      c.clearStep();
+      expect(c.get('bootHosts').length).to.equal(0);
+    });
+    it('should set stopBootstrap to false', function() {
+      c.set('stopBootstrap', true);
+      c.clearStep();
+      expect(c.get('stopBootstrap')).to.equal(false);
+    });
+    it('should set wizardController DBProperty bootStatus to false', function() {
+      c.get('wizardController').setDBProperty('bootStatus', true);
+      c.clearStep();
+      expect(c.get('wizardController').getDBProperty('bootStatus')).to.equal(false);
+    });
+    it('should set isSubmitDisabled to true', function() {
+      c.set('isSubmitDisabled', false);
+      c.clearStep();
+      expect(c.get('isSubmitDisabled')).to.equal(true);
+    });
+    it('should set isSubmitDisabled to true', function() {
+      c.set('isRetryDisabled', false);
+      c.clearStep();
+      expect(c.get('isRetryDisabled')).to.equal(true);
+    });
+  });
+
+  describe('#loadStep', function() {
+    it('should set registrationStartedAt to null', function() {
+      c.set('disablePreviousSteps', Em.K);
+      c.set('registrationStartedAt', {});
+      c.loadStep();
+      expect(c.get('registrationStartedAt')).to.be.null;
+    });
+    it('should set isLoaded to false', function() {
+      c.set('disablePreviousSteps', Em.K);
+      c.set('clearStep', Em.K);
+      c.set('loadHosts', Em.K);
+      c.set('isLoaded', true);
+      c.loadStep();
+      expect(c.get('isLoaded')).to.equal(false);
+    });
+    it('should call clearStep', function() {
+      c.set('disablePreviousSteps', Em.K);
+      c.set('loadHosts', Em.K);
+      sinon.spy(c, 'clearStep');
+      c.loadStep();
+      expect(c.get('clearStep').calledOnce).to.equal(true);
+      c.clearStep.restore();
+    });
+    it('should call loadHosts', function() {
+      c.set('disablePreviousSteps', Em.K);
+      c.set('loadHosts', Em.K);
+      sinon.spy(c, 'loadHosts');
+      c.loadStep();
+      expect(c.get('loadHosts').calledOnce).to.equal(true);
+      c.loadHosts.restore();
+    });
+    it('should call disablePreviousSteps', function() {
+      c.set('disablePreviousSteps', Em.K);
+      c.set('loadHosts', Em.K);
+      sinon.spy(c, 'disablePreviousSteps');
+      c.loadStep();
+      expect(c.get('disablePreviousSteps').calledOnce).to.equal(true);
+      c.disablePreviousSteps.restore();
+    });
+  });
+
+  describe('#loadHosts', function() {
+    it('should set isLoaded to true', function() {
+      c.set('navigateStep', Em.K);
+      c.set('content', {hosts: {}});
+      c.loadHosts();
+      expect(c.get('isLoaded')).to.equal(true);
+    });
+    it('should set bootStatus REGISTERED on testMode', function() {
+      App.testMode = true;
+      c.set('navigateStep', Em.K);
+      c.set('content', {hosts: {c: {name: 'name'}}});
+      c.loadHosts();
+      expect(c.get('hosts').everyProperty('bootStatus', 'REGISTERED')).to.equal(true);
+    });
+    it('should set bootStatus DONE on "real" mode and when installOptions.manualInstall is selected', function() {
+      App.testMode = false;
+      c.set('navigateStep', Em.K);
+      c.set('content', {installOptions:{manualInstall: true}, hosts: {c: {name: 'name'}}});
+      c.loadHosts();
+      expect(c.get('hosts').everyProperty('bootStatus', 'DONE')).to.equal(true);
+      App.testMode = true;
+    });
+    it('should set bootStatus PENDING on "real" mode and when installOptions.manualInstall is not selected', function() {
+      App.testMode = false;
+      c.set('navigateStep', Em.K);
+      c.set('content', {installOptions:{manualInstall: false}, hosts: {c: {name: 'name'}}});
+      c.loadHosts();
+      expect(c.get('hosts').everyProperty('bootStatus', 'PENDING')).to.equal(true);
+      App.testMode = true;
+    });
+    it('should set bootStatus PENDING on "real" mode and when installOptions.manualInstall is not selected', function() {
+      c.set('navigateStep', Em.K);
+      c.set('content', {hosts: {c: {name: 'name'}, d: {name: 'name1'}}});
+      c.loadHosts();
+      expect(c.get('hosts').everyProperty('isChecked', false)).to.equal(true);
+    });
+  });
+
+  describe('#parseHostInfo', function() {
+
+    var tests = Em.A([
+      {
+        bootHosts: Em.A([
+          Em.Object.create({name: 'c1', bootStatus: 'REGISTERED', bootLog: ''}),
+          Em.Object.create({name: 'c2', bootStatus: 'REGISTERING', bootLog: ''}),
+          Em.Object.create({name: 'c3', bootStatus: 'RUNNING', bootLog: ''})
+        ]),
+        hostsStatusFromServer: Em.A([
+          {hostName: 'c1', status: 'REGISTERED', log: 'c1'},
+          {hostName: 'c2', status: 'REGISTERED', log: 'c2'},
+          {hostName: 'c3', status: 'RUNNING', log: 'c3'}
+        ]),
+        m: 'bootHosts not empty, hostsStatusFromServer not empty, one is RUNNING',
+        e: {
+          c: true,
+          r: true
+        }
+      },
+      {
+        bootHosts: Em.A([]),
+        hostsStatusFromServer: Em.A([
+          {hostName: 'c1', status: 'REGISTERED', log: 'c1'},
+          {hostName: 'c2', status: 'REGISTERED', log: 'c2'},
+          {hostName: 'c3', status: 'RUNNING', log: 'c3'}
+        ]),
+        m: 'bootHosts is empty',
+        e: {
+          c: false,
+          r: false
+        }
+      },
+      {
+        bootHosts: Em.A([
+          Em.Object.create({name: 'c1', bootStatus: 'REGISTERED', bootLog: ''}),
+          Em.Object.create({name: 'c2', bootStatus: 'REGISTERING', bootLog: ''}),
+          Em.Object.create({name: 'c3', bootStatus: 'REGISTERED', bootLog: ''})
+        ]),
+        hostsStatusFromServer: Em.A([
+          {hostName: 'c1', status: 'REGISTERED', log: 'c1'},
+          {hostName: 'c2', status: 'REGISTERED', log: 'c2'},
+          {hostName: 'c3', status: 'REGISTERED', log: 'c3'}
+        ]),
+        m: 'bootHosts not empty, hostsStatusFromServer not empty, no one is RUNNING',
+        e: {
+          c: true,
+          r: false
+        }
+      }
+    ]);
+
+    tests.forEach(function(test) {
+      it(test.m, function() {
+        c.set('bootHosts', test.bootHosts);
+        var r = c.parseHostInfo(test.hostsStatusFromServer);
+        expect(r).to.equal(test.e.r);
+        if (test.e.c) {
+          test.hostsStatusFromServer.forEach(function(h) {
+            var r = c.get('bootHosts').findProperty('name', h.hostName);
+            if (!['REGISTERED', 'REGISTERING'].contains(r.get('bootStatus'))) {
+              expect(r.get('bootStatus')).to.equal(h.status);
+              expect(r.get('bootLog')).to.equal(h.log);
+            }
+          });
+        }
+      });
+    });
+  });
+
+  describe('#removeHosts', function() {
+    it('should call App.showConfirmationPopup', function() {
+      sinon.spy(App, 'showConfirmationPopup');
+      c.removeHosts(Em.A([]));
+      expect(App.showConfirmationPopup.calledOnce).to.equal(true);
+      App.showConfirmationPopup.restore();
+    });
+    it('primary should disable Submit if no more hosts', function() {
+      var hosts = [{}];
+      c.set('hosts', hosts);
+      var popup = c.removeHosts(hosts);
+      popup.onPrimary();
+      expect(c.get('isSubmitDisabled')).to.equal(true);
+    });
+  });
+
+  describe('#removeHost', function() {
+    it('should call removeHosts with array as arg', function() {
+      var host = {a:''};
+      sinon.spy(c, 'removeHosts');
+      c.removeHost(host);
+      expect(c.removeHosts.calledWith([host]));
+      c.removeHosts.restore();
+    });
+  });
+
+  describe('#removeSelectedHosts', function() {
+    it('should remove selected hosts', function() {
+      c = App.WizardStep3Controller.create({
+        wizardController: App.InstallerController.create(),
+        hosts: [
+          {isChecked: true, name: 'c1'},
+          {isChecked: false, name: 'c2'}
+        ]
+      });
+      c.removeSelectedHosts().onPrimary();
+      expect(c.get('hosts').mapProperty('name')).to.eql(['c2']);
+    });
+  });
+
+  describe('#selectedHostsPopup', function() {
+    it('should show App.ModalPopup', function() {
+      sinon.spy(App.ModalPopup, 'show');
+      c.selectedHostsPopup();
+      expect(App.ModalPopup.show.calledOnce).to.equal(true);
+      App.ModalPopup.show.restore();
+    });
+  });
+
+  describe('#retryHosts', function() {
+    it('should set numPolls to 0', function() {
+      var s = sinon.stub(App.router, 'get', function() {
+        return {launchBootstrap: Em.K}
+      });
+      c.set('content', {installOptions: {}});
+      c.set('doBootstrap', Em.K);
+      c.set('numPolls', 123);
+      c.retryHosts(Em.A([]));
+      expect(c.get('numPolls')).to.equal(0);
+      s.restore();
+    });
+    it('should set registrationStartedAt to null', function() {
+      var s = sinon.stub(App.router, 'get', function() {
+        return {launchBootstrap: Em.K}
+      });
+      c.set('content', {installOptions: {}});
+      c.set('doBootstrap', Em.K);
+      c.retryHosts(Em.A([]));
+      expect(c.get('registrationStartedAt')).to.be.null;
+      s.restore();
+    });
+    it('should startRegistration if installOptions.manualInstall is true', function() {
+      var s = sinon.stub(App.router, 'get', function() {
+        return {launchBootstrap: Em.K}
+      });
+      sinon.spy(c, 'startRegistration');
+      c.set('content', {installOptions: {manualInstall: true}});
+      c.set('doBootstrap', Em.K);
+      c.retryHosts(Em.A([]));
+      expect(c.startRegistration.calledOnce).to.equal(true);
+      s.restore();
+      c.startRegistration.restore();
+    });
+    it('should doBootstrap if installOptions.manualInstall is false', function() {
+      var s = sinon.stub(App.router, 'get', function() {
+        return {launchBootstrap: Em.K}
+      });
+      c.set('content', {installOptions: {manualInstall: false}});
+      c.set('doBootstrap', Em.K);
+      sinon.spy(c, 'doBootstrap');
+      c.retryHosts(Em.A([]));
+      expect(c.doBootstrap.calledOnce).to.equal(true);
+      s.restore();
+      c.doBootstrap.restore();
+    });
+  });
+
+  describe('#retryHost', function() {
+    it('should callretryHosts with array as arg', function() {
+      var host = {n: 'c'}, s = sinon.stub(App.router, 'get', function() {
+        return {launchBootstrap: Em.K}
+      });
+      sinon.spy(c, 'retryHosts');
+      c.set('content', {installOptions: {}});
+      c.set('doBootstrap', Em.K);
+      c.retryHost(host);
+      expect(c.retryHosts.calledWith([host])).to.equal(true);
+      c.retryHosts.restore();
+      s.restore();
+    });
+  });
+
+  describe('#retrySelectedHosts', function() {
+    it('shouldn\'t do nothing if isRetryDisabled is true', function() {
+      c.set('isRetryDisabled', true);
+      sinon.spy(c, 'retryHosts');
+      c.retrySelectedHosts();
+      expect(c.retryHosts.called).to.equal(false);
+      c.retryHosts.restore();
+    });
+    it('should retry hosts with FAILED bootStatus and set isRetryDisabled to true', function() {
+      var s = sinon.stub(App.router, 'get', function() {
+        return {launchBootstrap: Em.K}
+      });
+      c = App.WizardStep3Controller.create({
+        wizardController: App.InstallerController.create(),
+        bootHosts: Em.A([{name: 'c1', bootStatus: 'FAILED'}, {name: 'c2', bootStatus: 'REGISTERED'}]),
+        content: {installOptions: {}},
+        doBootstrap: Em.K
+      });
+      sinon.spy(c, 'retryHosts');
+      c.retrySelectedHosts();
+      expect(c.retryHosts.calledWith([{name: 'c1', bootStatus: 'RUNNING'}]));
+      expect(c.get('isRetryDisabled')).to.equal(true);
+      c.retryHosts.restore();
+      s.restore();
+    });
+  });
+
+  describe('#startBootstrap', function() {
+    it('should drop numPolls and registrationStartedAt', function() {
+      c.set('numPolls', 123);
+      c.set('registrationStartedAt', 1234);
+      c.set('doBootstrap', Em.K);
+      c.startBootstrap();
+      expect(c.get('numPolls')).to.equal(0);
+      expect(c.get('registrationStartedAt')).to.be.null;
+    });
+    it('should drop numPolls and registrationStartedAt', function() {
+      var hosts = Em.A([{name: 'c1'}, {name: 'c2'}]);
+      c = App.WizardStep3Controller.create({
+        wizardController: App.InstallerController.create(),
+        doBootstrap: Em.K,
+        setRegistrationInProgressOnce: Em.K,
+        hosts: hosts
+      });
+      c.startBootstrap();
+      expect(c.get('bootHosts').mapProperty('name')).to.eql(['c1','c2']);
+    });
+  });
+
+  describe('#setRegistrationInProgressOnce', function() {
+    it('should call Ember.run.once with "setRegistrationInProgress"', function() {
+      sinon.spy(Em.run, 'once');
+      c.setRegistrationInProgressOnce();
+      expect(Em.run.once.firstCall.args[1]).to.equal('setRegistrationInProgress');
+      Em.run.once.restore();
+    });
+  });
+
+  describe('#setRegistrationInProgress', function() {
+    var tests = Em.A([
+      {
+        bootHosts: [],
+        isLoaded: false,
+        e: true,
+        m: 'no bootHosts and isLoaded is false'
+      },
+      {
+        bootHosts: [],
+        isLoaded: true,
+        e: false,
+        m: 'no bootHosts and isLoaded is true'
+      },
+      {
+        bootHosts: [
+          Em.Object.create({bootStatus: 'RUNNING'}),
+          Em.Object.create({bootStatus: 'RUNNING'})
+        ],
+        isLoaded: true,
+        e: false,
+        m: 'bootHosts without REGISTERED/FAILED and isLoaded is true'
+      },
+      {
+        bootHosts: [
+          Em.Object.create({bootStatus: 'RUNNING'}),
+          Em.Object.create({bootStatus: 'RUNNING'})
+        ],
+        isLoaded: false,
+        e: true,
+        m: 'bootHosts without REGISTERED/FAILED and isLoaded is false'
+      },
+      {
+        bootHosts: [
+          Em.Object.create({bootStatus: 'REGISTERED'}),
+          Em.Object.create({bootStatus: 'RUNNING'})
+        ],
+        isLoaded: false,
+        e: true,
+        m: 'bootHosts with one REGISTERED and isLoaded is false'
+      },
+      {
+        bootHosts: [
+          Em.Object.create({bootStatus: 'FAILED'}),
+          Em.Object.create({bootStatus: 'RUNNING'})
+        ],
+        isLoaded: false,
+        e: true,
+        m: 'bootHosts with one FAILED and isLoaded is false'
+      },
+      {
+        bootHosts: [
+          Em.Object.create({bootStatus: 'REGISTERED'}),
+          Em.Object.create({bootStatus: 'RUNNING'})
+        ],
+        isLoaded: true,
+        e: false,
+        m: 'bootHosts with one REGISTERED and isLoaded is true'
+      },
+      {
+        bootHosts: [
+          Em.Object.create({bootStatus: 'FAILED'}),
+          Em.Object.create({bootStatus: 'RUNNING'})
+        ],
+        isLoaded: true,
+        e: false,
+        m: 'bootHosts with one FAILED and isLoaded is true'
+      }
+    ]);
+
+    tests.forEach(function(test) {
+      it(test.m, function() {
+        sinon.stub(c, 'disablePreviousSteps', Em.K);
+        c.set('bootHosts', test.bootHosts);
+        c.set('isLoaded', test.isLoaded);
+        c.setRegistrationInProgress();
+        expect(c.get('isRegistrationInProgress')).to.equal(test.e);
+        c.disablePreviousSteps.restore();
+      });
+    });
+  });
+
+  describe('#doBootstrap', function() {
+    beforeEach(function() {
+      sinon.spy(App.ajax, 'send');
+    });
+    afterEach(function() {
+      App.ajax.send.restore();
+    });
+    it('shouldn\'t do nothing if stopBootstrap is true', function() {
+      c.set('stopBootstrap', true);
+      c.doBootstrap();
+      expect(App.ajax.send.called).to.equal(false);
+    });
+    it('should increment numPolls if stopBootstrap is false', function() {
+      c.set('stopBootstrap', false);
+      c.set('numPolls', 0);
+      c.doBootstrap();
+      expect(c.get('numPolls')).to.equal(1);
+    });
+    it('should do ajax call if stopBootstrap is false', function() {
+      c.set('stopBootstrap', false);
+      c.doBootstrap();
+      expect(App.ajax.send.called).to.equal(true);
+    });
+  });
+
+  describe('#startRegistration', function() {
+    it('shouldn\'t do nothing if registrationStartedAt isn\'t null', function() {
+      c.set('registrationStartedAt', 1234);
+      sinon.spy(c, 'isHostsRegistered');
+      c.startRegistration();
+      expect(c.isHostsRegistered.called).to.equal(false);
+      expect(c.get('registrationStartedAt')).to.equal(1234);
+      c.isHostsRegistered.restore();
+    });
+    it('shouldn\'t do nothing if registrationStartedAt isn\'t null', function() {
+      c.set('registrationStartedAt', null);
+      sinon.spy(c, 'isHostsRegistered');
+      c.startRegistration();
+      expect(c.isHostsRegistered.calledOnce).to.equal(true);
+      c.isHostsRegistered.restore();
+    });
+  });
+
+  describe('#isHostsRegistered', function() {
+    beforeEach(function() {
+      sinon.spy(App.ajax, 'send');
+    });
+    afterEach(function() {
+      App.ajax.send.restore();
+    });
+    it('shouldn\'t do nothing if stopBootstrap is true', function() {
+      c.set('stopBootstrap', true);
+      c.isHostsRegistered();
+      expect(App.ajax.send.called).to.equal(false);
+    });
+    it('should do ajax call if stopBootstrap is false', function() {
+      c.set('stopBootstrap', false);
+      c.isHostsRegistered();
+      expect(App.ajax.send.called).to.equal(true);
+
+    });
+  });
+
+  describe('#isHostsRegisteredSuccessCallback', function() {
+    var tests = Em.A([
+      {
+        bootHosts: Em.A([
+          Em.Object.create({bootStatus: 'DONE'})
+        ]),
+        data: {items:[]},
+        m: 'one host DONE',
+        e: {
+          bs: 'REGISTERING',
+          getHostInfoCalled: false
+        }
+      },
+      {
+        bootHosts: Em.A([
+          Em.Object.create({bootStatus: 'REGISTERING', name: 'c1'})
+        ]),
+        data: {items:[{Hosts: {host_name: 'c1'}}]},
+        m: ' one host REGISTERING',
+        e: {
+          bs: 'FAILED',
+          getHostInfoCalled: true
+        }
+      },
+      {
+        bootHosts: Em.A([
+          Em.Object.create({bootStatus: 'REGISTERING', name: 'c1'})
+        ]),
+        data: {items:[{Hosts: {host_name: 'c2'}}]},
+        m: 'one host REGISTERING but data without info about it',
+        e: {
+          bs: 'FAILED',
+          getHostInfoCalled: true
+        }
+      },
+      {
+        bootHosts: Em.A([
+          Em.Object.create({bootStatus: 'RUNNING', name: 'c1'})
+        ]),
+        data: {items:[{Hosts: {host_name: 'c1'}}]},
+        m: ' one host RUNNING',
+        e: {
+          bs: 'RUNNING',
+          getHostInfoCalled: false
+        }
+      }
+    ]);
+    tests.forEach(function(test) {
+      it(test.m, function() {
+        sinon.spy(c, 'getHostInfo');
+        c.set('bootHosts', test.bootHosts);
+        c.isHostsRegisteredSuccessCallback(test.data);
+        expect(c.get('bootHosts')[0].get('bootStatus')).to.equal(test.e.bs);
+        expect(c.getHostInfo.called).to.equal(test.e.getHostInfoCalled);
+        c.getHostInfo.restore();
+      });
+    });
+  });
+
+  describe('#getAllRegisteredHosts', function() {
+    it('should call App.ajax.send', function() {
+      sinon.spy(App.ajax, 'send');
+      c.getAllRegisteredHosts();
+      expect(App.ajax.send.calledOnce).to.equal(true);
+      App.ajax.send.restore();
+    });
+  });
+
+  describe('#getAllRegisteredHostsCallback', function() {
+    var tests = Em.A([
+      {
+        hostsInCluster: ['c3'],
+        bootHosts: [{name:'c1'},{name:'c2'}],
+        hosts: Em.A([
+          {Hosts: {host_name:'c1'}},
+          {Hosts: {host_name:'c2'}}
+        ]),
+        m: 'No registered hosts',
+        e: {
+          hasMoreRegisteredHosts: false,
+          registeredHosts: ''
+        }
+      },
+      {
+        hostsInCluster: ['c4'],
+        bootHosts: [{name:'c3'},{name:'c5'}],
+        hosts: Em.A([
+          {Hosts: {host_name:'c1'}},
+          {Hosts: {host_name:'c2'}}
+        ]),
+        m: '2 registered hosts',
+        e: {
+          hasMoreRegisteredHosts: true,
+          registeredHosts: ['c1','c2']
+        }
+      },
+      {
+        hostsInCluster: ['c4'],
+        bootHosts: [{name:'c1'},{name:'c5'}],
+        hosts: Em.A([
+          {Hosts: {host_name:'c1'}},
+          {Hosts: {host_name:'c2'}}
+        ]),
+        m: '1 registered host',
+        e: {
+          hasMoreRegisteredHosts: true,
+          registeredHosts: ['c2']
+        }
+      },
+      {
+        hostsInCluster: ['c1'],
+        bootHosts: [{name:'c3'},{name:'c5'}],
+        hosts: Em.A([
+          {Hosts: {host_name:'c1'}},
+          {Hosts: {host_name:'c2'}}
+        ]),
+        m: '1 registered host (2)',
+        e: {
+          hasMoreRegisteredHosts: true,
+          registeredHosts: ['c2']
+        }
+      }
+    ]);
+    tests.forEach(function(test) {
+      it(test.m, function() {
+        c.reopen({hostsInCluster: test.hostsInCluster, setRegistrationInProgress: Em.K});
+        c.set('bootHosts', test.bootHosts);
+        c.getAllRegisteredHostsCallback({items:test.hosts});
+        expect(c.get('hasMoreRegisteredHosts')).to.equal(test.e.hasMoreRegisteredHosts);
+        expect(c.get('registeredHosts')).to.eql(test.e.registeredHosts);
+      });
+    });
+  });
+
+  describe('#registerErrPopup', function() {
+    it('should call App.ModalPopup.show', function() {
+      sinon.spy(App.ModalPopup, 'show');
+      c.registerErrPopup();
+      expect(App.ModalPopup.show.calledOnce).to.equal(true);
+      App.ModalPopup.show.restore();
+    });
+  });
+
+  describe('#getHostInfo', function() {
+    it('should do ajax request', function() {
+      sinon.spy(App.ajax, 'send');
+      c.getHostInfo();
+      expect(App.ajax.send.calledOnce).to.equal(true);
+      App.ajax.send.restore();
+    });
+  });
+
+  describe('#getHostInfoErrorCallback', function() {
+    it('should call registerErrPopup', function() {
+      sinon.spy(c, 'registerErrPopup');
+      c.getHostInfoErrorCallback();
+      expect(c.registerErrPopup.calledOnce).to.equal(true);
+      c.registerErrPopup.restore();
+    });
+  });
+
+  describe('#stopRegistration', function() {
+    var tests = Em.A([
+      {
+        bootHosts: [{bootStatus: 'REGISTERED'}, {bootStatus: 'RUNNING'}],
+        e: {isSubmitDisabled: false, isRetryDisabled: true}
+      },
+      {
+        bootHosts: [{bootStatus: 'FAILED'}, {bootStatus: 'RUNNING'}],
+        e: {isSubmitDisabled: true, isRetryDisabled: false}
+      },
+      {
+        bootHosts: [{bootStatus: 'FAILED'}, {bootStatus: 'REGISTERED'}],
+        e: {isSubmitDisabled: false, isRetryDisabled: false}
+      },
+      {
+        bootHosts: [{bootStatus: 'RUNNING'}, {bootStatus: 'RUNNING'}],
+        e: {isSubmitDisabled: true, isRetryDisabled: true}
+      }
+    ]);
+    tests.forEach(function(test) {
+      it(test.bootHosts.mapProperty('bootStatus').join(', '), function() {
+        c.reopen({bootHosts: test.bootHosts});
+        c.stopRegistration();
+        expect(c.get('isSubmitDisabled')).to.equal(test.e.isSubmitDisabled);
+        expect(c.get('isRetryDisabled')).to.equal(test.e.isRetryDisabled);
+      });
+    });
+  });
+
+  describe('#submit', function() {
+    it('if isHostHaveWarnings should show confirmation popup', function() {
+      c.reopen({isHostHaveWarnings: true});
+      sinon.spy(App.ModalPopup, 'show');
+      c.submit();
+      expect(App.ModalPopup.show.calledOnce).to.equal(true);
+      App.ModalPopup.show.restore();
+    });
+    it('if isHostHaveWarnings should show confirmation popup. on Primary should set bootHosts to content.hosts', function() {
+      var bootHosts = [{name: 'c1'}];
+      c.reopen({isHostHaveWarnings: true, bootHosts: bootHosts, hosts: []});
+      c.submit().onPrimary();
+      expect(c.get('content.hosts')).to.eql(bootHosts);
+    });
+    it('if isHostHaveWarnings is false should set bootHosts to content.hosts', function() {
+      var bootHosts = [{name: 'c1'}];
+      c.reopen({isHostHaveWarnings: false, bootHosts: bootHosts, hosts: []});
+      c.submit();
+      expect(c.get('content.hosts')).to.eql(bootHosts);
+    });
+  });
+
+  describe('#hostLogPopup', function() {
+    it('should show App.ModalPopup', function() {
+      sinon.spy(App.ModalPopup, 'show');
+      c.hostLogPopup({context:Em.Object.create({})});
+      expect(App.ModalPopup.show.calledOnce).to.equal(true);
+      App.ModalPopup.show.restore();
+    });
+  });
+
+  describe('#rerunChecksSuccessCallback', function() {
+    beforeEach(function() {
+      sinon.stub(c, 'parseWarnings', Em.K);
+    });
+    afterEach(function() {
+      c.parseWarnings.restore();
+    });
+    it('should set checksUpdateProgress to 100', function() {
+      c.set('checksUpdateProgress', 0);
+      c.rerunChecksSuccessCallback({});
+      expect(c.get('checksUpdateProgress')).to.equal(100);
+    });
+    it('should set checksUpdateStatus to SUCCESS', function() {
+      c.set('checksUpdateStatus', '');
+      c.rerunChecksSuccessCallback({});
+      expect(c.get('checksUpdateStatus')).to.equal('SUCCESS');
+    });
+    it('should set call parseWarnings', function() {
+      c.rerunChecksSuccessCallback({});
+      expect(c.parseWarnings.calledOnce).to.equal(true);
+    });
+  });
+
+  describe('#rerunChecksErrorCallback', function() {
+    it('should set checksUpdateProgress to 100', function() {
+      c.set('checksUpdateProgress', 0);
+      c.rerunChecksErrorCallback({});
+      expect(c.get('checksUpdateProgress')).to.equal(100);
+    });
+    it('should set checksUpdateStatus to FAILED', function() {
+      c.set('checksUpdateStatus', '');
+      c.rerunChecksErrorCallback({});
+      expect(c.get('checksUpdateStatus')).to.equal('FAILED');
+    });
+  });
+
+  describe('#filterBootHosts', function() {
+    var tests = Em.A([
+      {
+        bootHosts: [
+          Em.Object.create({name: 'c1'}),
+          Em.Object.create({name: 'c2'})
+        ],
+        data: {
+          items: [
+            {Hosts: {host_name: 'c1'}}
+          ]
+        },
+        m: 'one host',
+        e: ['c1']
+      },
+      {
+        bootHosts: [
+          Em.Object.create({name: 'c1'}),
+          Em.Object.create({name: 'c2'})
+        ],
+        data: {
+          items: [
+            {Hosts: {host_name: 'c3'}}
+          ]
+        },
+        m: 'no hosts',
+        e: []
+      },
+      {
+      bootHosts: [
+        Em.Object.create({name: 'c1'}),
+        Em.Object.create({name: 'c2'})
+      ],
+        data: {
+        items: [
+          {Hosts: {host_name: 'c1'}},
+          {Hosts: {host_name: 'c2'}}
+        ]
+      },
+      m: 'many hosts',
+        e: ['c1', 'c2']
+    }
+    ]);
+    tests.forEach(function(test) {
+      it(test.m, function() {
+        c.reopen({bootHosts: test.bootHosts});
+        var filteredData = c.filterBootHosts(test.data);
+        expect(filteredData.items.mapProperty('Hosts.host_name')).to.eql(test.e);
+      });
+    });
+  });
+
+  describe('#hostWarningsPopup', function() {
+    it('should show App.ModalPopup', function() {
+      sinon.spy(App.ModalPopup, 'show');
+      c.hostWarningsPopup();
+      expect(App.ModalPopup.show.calledOnce).to.equal(true);
+      App.ModalPopup.show.restore();
+    });
+  });
+
+  describe('#registeredHostsPopup', function() {
+    it('should show App.ModalPopup', function() {
+      sinon.spy(App.ModalPopup, 'show');
+      c.registeredHostsPopup();
+      expect(App.ModalPopup.show.calledOnce).to.equal(true);
+      App.ModalPopup.show.restore();
+    });
+  });
+
+  describe('#parseWarnings', function() {
+    it('no warnings if last_agent_env isn\'t specified', function() {
+      c.set('warnings', [{}]);
+      c.set('warningsByHost', [{},{}]);
+      c.parseWarnings({items:[{Hosts:{host_name:'c1'}}]});
+      expect(c.get('warnings')).to.eql([]);
+      expect(c.get('warningsByHost.length')).to.equal(1); // default group
+      expect(c.get('isWarningsLoaded')).to.equal(true);
+    });
+
+    Em.A([
+        {
+          m: 'parse stackFoldersAndFiles',
+          tests : Em.A([
+            {
+              items: [{Hosts:{host_name: 'c1', last_agent_env: {stackFoldersAndFiles: []}}}],
+              m: 'empty stackFoldersAndFiles',
+              e: {
+                warnings: [],
+                warningsByHost: [0]
+              }
+            },
+            {
+              items: [{Hosts:{host_name: 'c1', last_agent_env: {stackFoldersAndFiles: [{name: 'n1'}]}}}],
+              m: 'not empty stackFoldersAndFiles',
+              e: {
+                warnings: [{
+                  name: 'n1',
+                  hosts: ['c1'],
+                  onSingleHost: true,
+                  category: 'fileFolders'
+                }],
+                warningsByHost: [1]
+              }
+            },
+            {
+              items: [
+                {Hosts:{host_name: 'c1', last_agent_env: {stackFoldersAndFiles: [{name: 'n1'}]}}},
+                {Hosts:{host_name: 'c2', last_agent_env: {stackFoldersAndFiles: [{name: 'n1'}]}}}
+              ],
+              m: 'not empty stackFoldersAndFiles on two hosts',
+              e: {
+                warnings: [{
+                  name: 'n1',
+                  hosts: ['c1', 'c2'],
+                  onSingleHost: false,
+                  category: 'fileFolders'
+                }],
+                warningsByHost: [1]
+              }
+            }
+          ])
+        },
+        {
+          m: 'parse installedPackages',
+          tests : Em.A([
+            {
+              items: [{Hosts:{host_name: 'c1', last_agent_env: {installedPackages: []}}}],
+              m: 'empty installedPackages',
+              e: {
+                warnings: [],
+                warningsByHost: [0]
+              }
+            },
+            {
+              items: [{Hosts:{host_name: 'c1', last_agent_env: {installedPackages: [{name: 'n1'}]}}}],
+              m: 'not empty installedPackages',
+              e: {
+                warnings: [{
+                  name: 'n1',
+                  hosts: ['c1'],
+                  onSingleHost: true,
+                  category: 'packages'
+                }],
+                warningsByHost: [1]
+              }
+            },
+            {
+              items: [
+                {Hosts:{host_name: 'c1', last_agent_env: {installedPackages: [{name: 'n1'}]}}},
+                {Hosts:{host_name: 'c2', last_agent_env: {installedPackages: [{name: 'n1'}]}}}
+              ],
+              m: 'not empty installedPackages on two hosts',
+              e: {
+                warnings: [{
+                  name: 'n1',
+                  hosts: ['c1', 'c2'],
+                  onSingleHost: false,
+                  category: 'packages'
+                }],
+                warningsByHost: [1]
+              }
+            }
+          ])
+        },
+        {
+          m: 'parse hostHealth.liveServices',
+          tests : Em.A([
+            {
+              items: [{Hosts:{host_name: 'c1', last_agent_env: {hostHealth: []}}}],
+              m: 'empty hostHealth',
+              e: {
+                warnings: [],
+                warningsByHost: [0]
+              }
+            },
+            {
+              items: [{Hosts:{host_name: 'c1', last_agent_env: {hostHealth:{liveServices: []}}}}],
+              m: 'empty liveServices',
+              e: {
+                warnings: [],
+                warningsByHost: [0]
+              }
+            },
+            {
+              items: [{Hosts:{host_name: 'c1', last_agent_env: {hostHealth:{liveServices: [{status: 'Unhealthy', name: 'n1'}]}}}}],
+              m: 'not empty hostHealth.liveServices',
+              e: {
+                warnings: [{
+                  name: 'n1',
+                  hosts: ['c1'],
+                  onSingleHost: true,
+                  category: 'services'
+                }],
+                warningsByHost: [1]
+              }
+            },
+            {
+              items: [
+                {Hosts:{host_name: 'c1', last_agent_env: {hostHealth:{liveServices: [{status: 'Unhealthy', name: 'n1'}]}}}},
+                {Hosts:{host_name: 'c2', last_agent_env: {hostHealth:{liveServices: [{status: 'Unhealthy', name: 'n1'}]}}}}
+              ],
+              m: 'not empty hostHealth.liveServices on two hosts',
+              e: {
+                warnings: [{
+                  name: 'n1',
+                  hosts: ['c1', 'c2'],
+                  onSingleHost: false,
+                  category: 'services'
+                }],
+                warningsByHost: [1, 1]
+              }
+            }
+          ])
+        },
+        {
+          m: 'parse existingUsers',
+          tests : Em.A([
+            {
+              items: [{Hosts:{host_name: 'c1', last_agent_env: {existingUsers: []}}}],
+              m: 'empty existingUsers',
+              e: {
+                warnings: [],
+                warningsByHost: [0]
+              }
+            },
+            {
+              items: [{Hosts:{host_name: 'c1', last_agent_env: {existingUsers: [{userName: 'n1'}]}}}],
+              m: 'not empty existingUsers',
+              e: {
+                warnings: [{
+                  name: 'n1',
+                  hosts: ['c1'],
+                  onSingleHost: true,
+                  category: 'users'
+                }],
+                warningsByHost: [1]
+              }
+            },
+            {
+              items: [
+                {Hosts:{host_name: 'c1', last_agent_env: {existingUsers: [{userName: 'n1'}]}}},
+                {Hosts:{host_name: 'c2', last_agent_env: {existingUsers: [{userName: 'n1'}]}}}
+              ],
+              m: 'not empty existingUsers on two hosts',
+              e: {
+                warnings: [{
+                  name: 'n1',
+                  hosts: ['c1', 'c2'],
+                  onSingleHost: false,
+                  category: 'users'
+                }],
+                warningsByHost: [1, 1]
+              }
+            }
+          ])
+        },
+        {
+          m: 'parse alternatives',
+          tests : Em.A([
+            {
+              items: [{Hosts:{host_name: 'c1', last_agent_env: {alternatives: []}}}],
+              m: 'empty alternatives',
+              e: {
+                warnings: [],
+                warningsByHost: [0]
+              }
+            },
+            {
+              items: [{Hosts:{host_name: 'c1', last_agent_env: {alternatives: [{name: 'n1'}]}}}],
+              m: 'not empty alternatives',
+              e: {
+                warnings: [{
+                  name: 'n1',
+                  hosts: ['c1'],
+                  onSingleHost: true,
+                  category: 'alternatives'
+                }],
+                warningsByHost: [1]
+              }
+            },
+            {
+              items: [
+                {Hosts:{host_name: 'c1', last_agent_env: {alternatives: [{name: 'n1'}]}}},
+                {Hosts:{host_name: 'c2', last_agent_env: {alternatives: [{name: 'n1'}]}}}
+              ],
+              m: 'not empty alternatives on two hosts',
+              e: {
+                warnings: [{
+                  name: 'n1',
+                  hosts: ['c1', 'c2'],
+                  onSingleHost: false,
+                  category: 'alternatives'
+                }],
+                warningsByHost: [1, 1]
+              }
+            }
+          ])
+        },
+        {
+          m: 'parse hostHealth.activeJavaProcs',
+          tests : Em.A([
+            {
+              items: [{Hosts:{host_name: 'c1', last_agent_env: {hostHealth: [], javaProcs: []}}}],
+              m: 'empty hostHealth',
+              e: {
+                warnings: [],
+                warningsByHost: [0]
+              }
+            },
+            {
+              items: [{Hosts:{host_name: 'c1', last_agent_env: {hostHealth:{activeJavaProcs: []}}}}],
+              m: 'empty activeJavaProcs',
+              e: {
+                warnings: [],
+                warningsByHost: [0]
+              }
+            },
+            {
+              items: [{Hosts:{host_name: 'c1', last_agent_env: {hostHealth:{activeJavaProcs: [{pid: 'n1', command: ''}]}}}}],
+              m: 'not empty hostHealth.activeJavaProcs',
+              e: {
+                warnings: [{
+                  pid: 'n1',
+                  hosts: ['c1'],
+                  onSingleHost: true,
+                  category: 'processes'
+                }],
+                warningsByHost: [1]
+              }
+            },
+            {
+              items: [
+                {Hosts:{host_name: 'c1', last_agent_env: {hostHealth:{activeJavaProcs: [{pid: 'n1', command: ''}]}}}},
+                {Hosts:{host_name: 'c2', last_agent_env: {hostHealth:{activeJavaProcs: [{pid: 'n1', command: ''}]}}}}
+              ],
+              m: 'not empty hostHealth.activeJavaProcs on two hosts',
+              e: {
+                warnings: [{
+                  pid: 'n1',
+                  hosts: ['c1', 'c2'],
+                  onSingleHost: false,
+                  category: 'processes'
+                }],
+                warningsByHost: [1, 1]
+              }
+            }
+          ])
+        }
+    ]).forEach(function(category) {
+      describe(category.m, function() {
+        category.tests.forEach(function(test) {
+          it(test.m, function() {
+            c.parseWarnings({items: test.items});
+            c.get('warnings').forEach(function(w, i) {
+              Em.keys(test.e.warnings[i]).forEach(function(k) {
+                expect(w[k]).to.eql(test.e.warnings[i][k]);
+              });
+            });
+            for(var i in test.e.warningsByHost) {
+              if(test.e.warningsByHost.hasOwnProperty(i)) {
+                expect(c.get('warningsByHost')[i].warnings.length).to.equal(test.e.warningsByHost[i]);
+              }
+            }
+          });
+        });
+      });
+    });
+
+  });
+
 });

+ 9 - 0
ambari-web/test/installer/step4_test.js

@@ -338,4 +338,13 @@ describe('App.WizardStep4Controller', function () {
     }, this);
   });
 
+  describe('#monitoringCheckPopup', function() {
+    it('should show App.ModalPopup', function() {
+      sinon.spy(App.ModalPopup, 'show');
+      controller.monitoringCheckPopup();
+      expect(App.ModalPopup.show.calledOnce).to.equal(true);
+      App.ModalPopup.show.restore();
+    });
+  })
+
 });

+ 40 - 0
ambari-web/test/views/wizard/step3/hostLogPopupBody_view_test.js

@@ -0,0 +1,40 @@
+/**
+ * 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('views/wizard/step3/hostLogPopupBody_view');
+var view;
+describe('App.WizardStep3HostLogPopupBody', function() {
+
+  beforeEach(function() {
+    view = App.WizardStep3HostLogPopupBody.create({
+      parentView: Em.Object.create({
+        host: Em.Object.create()
+      })
+    });
+  });
+
+  describe('#bootLog', function() {
+    it('should be equal to parentView.host.bootLog', function() {
+      var log = 'i wanna play a game';
+      view.set('parentView.host.bootLog', log);
+      expect(view.get('bootLog')).to.equal(log);
+    });
+  });
+
+});

+ 89 - 0
ambari-web/test/views/wizard/step3/hostWarningPopupFooter_view_test.js

@@ -0,0 +1,89 @@
+/**
+ * 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('views/wizard/step3/hostWarningPopupFooter_view');
+var view;
+
+describe('App.WizardStep3HostWarningPopupFooter', function() {
+
+  beforeEach(function() {
+    view = App.WizardStep3HostWarningPopupFooter.create();
+    view.reopen({footerController: Em.Object.create()});
+  });
+
+  describe('#progressWidth', function() {
+    it('based on footerController.checksUpdateProgress', function() {
+      view.set('footerController.checksUpdateProgress', 42);
+      expect(view.get('progressWidth')).to.equal('width:42%');
+    });
+  });
+
+  describe('#isUpdateInProgress', function() {
+    var tests = Em.A([
+      {checksUpdateProgress: 0, e: false},
+      {checksUpdateProgress: 100, e: false},
+      {checksUpdateProgress: 50, e: true}
+    ]);
+    tests.forEach(function(test) {
+      it(test.checksUpdateProgress, function() {
+        view.set('footerController.checksUpdateProgress', test.checksUpdateProgress);
+        expect(view.get('isUpdateInProgress')).to.equal(test.e);
+      });
+    });
+  });
+
+  describe('#updateStatusClass', function() {
+    var tests = Em.A([
+      {checksUpdateStatus: 'SUCCESS', e: 'text-success'},
+      {checksUpdateStatus: 'FAILED', e: 'text-error'},
+      {checksUpdateStatus: 'PANIC', e: null}
+    ]);
+    tests.forEach(function(test) {
+      it(test.checksUpdateStatus, function() {
+        view.set('footerController.checksUpdateStatus', test.checksUpdateStatus);
+        if (Em.isNone(test.e)) {
+          expect(view.get('updateStatusClass')).to.be.null;
+        }
+        else {
+          expect(view.get('updateStatusClass')).to.equal(test.e);
+        }
+      })
+    });
+  });
+
+  describe('#updateStatus', function() {
+    var tests = Em.A([
+      {checksUpdateStatus: 'SUCCESS', e: Em.I18n.t('installer.step3.warnings.updateChecks.success')},
+      {checksUpdateStatus: 'FAILED', e: Em.I18n.t('installer.step3.warnings.updateChecks.failed')},
+      {checksUpdateStatus: 'PANIC', e: null}
+    ]);
+    tests.forEach(function(test) {
+      it(test.checksUpdateStatus, function() {
+        view.set('footerController.checksUpdateStatus', test.checksUpdateStatus);
+        if (Em.isNone(test.e)) {
+          expect(view.get('updateStatus')).to.be.null;
+        }
+        else {
+          expect(view.get('updateStatus')).to.equal(test.e);
+        }
+      })
+    });
+  });
+
+});

+ 222 - 9
ambari-web/test/views/wizard/step3_view_test.js

@@ -17,8 +17,9 @@
  */
 
 var App = require('app');
+require('messages');
 require('views/wizard/step3_view');
-
+var v;
 describe('App.WizardStep3View', function () {
   Em.run.next = function(callback){
     callback()
@@ -48,7 +49,7 @@ describe('App.WizardStep3View', function () {
     }.property('content')
   });
 
-  describe('watchSelection', function () {
+  describe('#watchSelection', function () {
     it('2 of 3 hosts selected', function () {
       view.watchSelection();
       expect(view.get('noHostsSelected')).to.equal(false);
@@ -68,22 +69,21 @@ describe('App.WizardStep3View', function () {
     });
   });
 
-
-  describe('selectAll', function () {
+  describe('#selectAll', function () {
     it('select all hosts', function () {
       view.selectAll();
       expect(view.get('content').everyProperty('isChecked', true)).to.equal(true);
     });
   });
 
-  describe('unSelectAll', function () {
+  describe('#unSelectAll', function () {
     it('unselect all hosts', function () {
       view.unSelectAll();
       expect(view.get('content').everyProperty('isChecked', false)).to.equal(true);
     });
   });
 
-  var testCases = [
+  var testCases = Em.A([
     {
       title: 'none hosts',
       content: [],
@@ -239,9 +239,9 @@ describe('App.WizardStep3View', function () {
         "FAILED": 0
       }
     }
-  ];
+  ]);
 
-  describe('countCategoryHosts', function () {
+  describe('#countCategoryHosts', function () {
     testCases.forEach(function (test) {
       it(test.title, function () {
         view.set('content', test.content);
@@ -253,7 +253,7 @@ describe('App.WizardStep3View', function () {
     }, this);
   });
 
-  describe('filter', function () {
+  describe('#filter', function () {
     testCases.forEach(function (test) {
       describe(test.title, function () {
         view.get('categories').forEach(function (category) {
@@ -268,4 +268,217 @@ describe('App.WizardStep3View', function () {
     }, this);
   });
 
+  describe('#monitorStatuses', function() {
+    var tests = Em.A([
+      {
+        controller: Em.Object.create({bootHosts: Em.A([])}),
+        m: 'Empty hosts list',
+        e: {status: 'alert-warn', linkText: ''}
+      },
+      {
+        controller: Em.Object.create({bootHosts: Em.A([{}]), isWarningsLoaded: false}),
+        m: 'isWarningsLoaded false',
+        e: {status: 'alert-info', linkText: ''}
+      },
+      {
+        controller: Em.Object.create({bootHosts: Em.A([{}]), isWarningsLoaded: true, isHostHaveWarnings: true}),
+        m: 'isWarningsLoaded true, isHostHaveWarnings true',
+        e: {status: 'alert-warn', linkText: Em.I18n.t('installer.step3.warnings.linkText')}
+      },
+      {
+        controller: Em.Object.create({bootHosts: Em.A([{}]), isWarningsLoaded: true, repoCategoryWarnings: ['']}),
+        m: 'isWarningsLoaded true, repoCategoryWarnings not empty',
+        e: {status: 'alert-warn', linkText: Em.I18n.t('installer.step3.warnings.linkText')}
+      },
+      {
+        controller: Em.Object.create({bootHosts: Em.A([{}]), isWarningsLoaded: true, diskCategoryWarnings: ['']}),
+        m: 'isWarningsLoaded true, diskCategoryWarnings not empty',
+        e: {status: 'alert-warn', linkText: Em.I18n.t('installer.step3.warnings.linkText')}
+      },
+      {
+        controller: Em.Object.create({bootHosts: Em.A([{}]), isWarningsLoaded: true, diskCategoryWarnings: [], repoCategoryWarnings: []}),
+        m: 'isWarningsLoaded true, diskCategoryWarnings is empty, repoCategoryWarnings is empty',
+        e: {status: 'alert-success', linkText: Em.I18n.t('installer.step3.noWarnings.linkText')}
+      },
+      {
+        controller: Em.Object.create({bootHosts: Em.A([{bootStatus: 'FAILED'}]), isWarningsLoaded: true, diskCategoryWarnings: [], repoCategoryWarnings: []}),
+        m: 'isWarningsLoaded true, diskCategoryWarnings is empty, repoCategoryWarnings is empty, all failed',
+        e: {status: 'alert-warn', linkText: ''}
+      }
+    ]);
+
+    tests.forEach(function(test) {
+      it(test.m, function() {
+        v = App.WizardStep3View.create({
+          controller: test.controller
+        });
+        v.monitorStatuses();
+        expect(v.get('status')).to.equal(test.e.status);
+        expect(v.get('linkText')).to.equal(test.e.linkText);
+      });
+    });
+  });
+
+  describe('#retrySelectedHosts', function() {
+    it('should set active category "All"', function() {
+      view.set('controller', Em.Object.create({retrySelectedHosts: Em.K, registeredHosts: []}));
+      view.retrySelectedHosts();
+      expect(view.get('categories').findProperty('hostsBootStatus', 'ALL').get('isActive')).to.equal(true);
+    });
+  });
+
+  describe('#selectCategory', function() {
+    var tests = Em.A(['ALL','RUNNING','REGISTERING','REGISTERED','FAILED']);
+    tests.forEach(function(test) {
+      it('should set active category "' + test + '"', function() {
+        view.set('controller', Em.Object.create({retrySelectedHosts: Em.K, registeredHosts: []}));
+        view.selectCategory({context:Em.Object.create({hostsBootStatus:test})});
+        expect(view.get('categories').findProperty('hostsBootStatus', test).get('isActive')).to.equal(true);
+      });
+    });
+  });
+
+  describe('#countCategoryHosts', function() {
+    it('should set host count for each category', function() {
+      view.set('content', Em.A([
+        Em.Object.create({bootStatus: 'RUNNING'}),
+        Em.Object.create({bootStatus: 'REGISTERING'}),
+        Em.Object.create({bootStatus: 'REGISTERED'}),
+        Em.Object.create({bootStatus: 'FAILED'})
+      ]));
+      view.countCategoryHosts();
+      expect(view.get('categories').mapProperty('hostsCount')).to.eql([4,1,1,1,1]);
+    });
+  });
+
+  describe('#hostBootStatusObserver', function() {
+    it('should call "Em.run.once" three times', function() {
+      sinon.spy(Em.run, 'once');
+      view.hostBootStatusObserver();
+      expect(Em.run.once.calledThrice).to.equal(true);
+      expect(Em.run.once.firstCall.args[1]).to.equal('countCategoryHosts');
+      expect(Em.run.once.secondCall.args[1]).to.equal('filter');
+      expect(Em.run.once.thirdCall.args[1]).to.equal('monitorStatuses');
+      Em.run.once.restore();
+    });
+  });
+
+  describe('#watchSelection', function() {
+    describe('should set "pageChecked"', function() {
+      var tests = Em.A([
+        {pageContent: Em.A([]),m:'false if empty "pageContent"', e: false},
+        {pageContent: Em.A([{isChecked: false}]),m:'false if not-empty "pageContent" and not all "isChecked" true', e: false},
+        {pageContent: Em.A([{isChecked: true}]),m:'true if not-empty "pageContent" and all "isChecked" true', e: false}
+      ]);
+      tests.forEach(function(test) {
+        it(test.m, function() {
+          view.set('pageContent', test.pageContent);
+          view.watchSelection();
+          expect(view.get('pageChecked')).to.equal(test.e);
+        });
+      });
+    });
+    describe('should set "noHostsSelected" and "selectedHostsCount"', function() {
+      var tests = Em.A([
+        {pageContent: Em.A([]),content:Em.A([]),m:' - "true", "0" if content is empty',e:{selectedHostsCount: 0, noHostsSelected: true}},
+        {pageContent: Em.A([]),content:Em.A([Em.Object.create({isChecked: false})]),m:' - "true", "0" if no one isChecked',e:{selectedHostsCount: 0, noHostsSelected: true}},
+        {pageContent: Em.A([]),content:Em.A([Em.Object.create({isChecked: true}),Em.Object.create({isChecked: false})]),m:' - "false", "1" if one isChecked',e:{selectedHostsCount: 1, noHostsSelected: false}}
+      ]);
+      tests.forEach(function(test) {
+        it(test.m, function() {
+          view.set('pageContent', test.pageContent);
+          view.set('content', test.content);
+          view.watchSelection();
+          expect(view.get('noHostsSelected')).to.equal(test.e.noHostsSelected);
+          expect(view.get('selectedHostsCount')).to.equal(test.e.selectedHostsCount);
+        });
+      });
+    });
+  });
+
+  describe('#watchSelectionOnce', function() {
+    it('should call "Em.run.once" one time', function() {
+      sinon.spy(Em.run, 'once');
+      view.watchSelectionOnce();
+      expect(Em.run.once.calledOnce).to.equal(true);
+      expect(Em.run.once.firstCall.args[1]).to.equal('watchSelection');
+      Em.run.once.restore();
+    });
+  });
+
+  describe('#selectedCategory', function() {
+    it('should equal category with isActive = true', function() {
+      view.get('categories').findProperty('hostsBootStatus', 'FAILED').set('isActive', true);
+      expect(view.get('selectedCategory.hostsBootStatus')).to.equal('FAILED');
+    });
+  });
+
+  describe('#onPageChecked', function() {
+    var tests = Em.A([
+      {
+        selectionInProgress: true,
+        pageContent: [Em.Object.create({isChecked: true}), Em.Object.create({isChecked: false})],
+        pageChecked: true,
+        m: 'shouldn\'t do nothing if selectionInProgress is true',
+        e: [true, false]
+      },
+      {
+        selectionInProgress: false,
+        pageContent: [Em.Object.create({isChecked: true}), Em.Object.create({isChecked: false})],
+        pageChecked: true,
+        m: 'should set each isChecked to pageChecked value',
+        e: [true, true]
+      }
+    ]);
+    tests.forEach(function(test) {
+      it(test.m, function() {
+        v = App.WizardStep3View.create({
+          'pageContent': test.pageContent,
+          'pageChecked': test.pageChecked,
+          'selectionInProgress': test.selectionInProgress
+        });
+        v.onPageChecked();
+        expect(v.get('pageContent').mapProperty('isChecked')).to.eql(test.e);
+      });
+    });
+  });
+
+});
+
+var wView;
+describe('App.WizardHostView', function() {
+
+  beforeEach(function() {
+    wView = App.WizardHostView.create({
+      hostInfo: {},
+      controller: Em.Object.create({
+        removeHost: Em.K,
+        retryHost: Em.K
+      })
+    });
+    sinon.spy(wView.get('controller'), 'retryHost');
+    sinon.spy(wView.get('controller'), 'removeHost');
+  });
+
+  afterEach(function() {
+    wView.get('controller').retryHost.restore();
+    wView.get('controller').removeHost.restore();
+  });
+
+  describe('#retry', function() {
+    it('should call controller.retryHost', function() {
+      wView.retry();
+      expect(wView.get('controller').retryHost.calledWith({})).to.equal(true);
+      expect(wView.get('controller').retryHost.calledOnce).to.equal(true);
+    });
+  });
+
+  describe('#remove', function() {
+    it('should call controller.removeHost', function() {
+      wView.remove();
+      expect(wView.get('controller').removeHost.calledWith({})).to.equal(true);
+      expect(wView.get('controller').removeHost.calledOnce).to.equal(true);
+    });
+  });
+
 });

部分文件因为文件数量过多而无法显示