Browse Source

AMBARI-5237 Implement pagination on "Confirm Hosts" step. (atkach)

atkach 11 years ago
parent
commit
74ef96200a

+ 2 - 2
ambari-web/app/controllers/wizard/step2_controller.js

@@ -309,8 +309,8 @@ App.WizardStep2Controller = Em.Controller.extend({
         this.hide();
       },
       bodyClass: Ember.View.extend({
-        templateName: require('templates/wizard/step2_host_name_pattern_popup'),
-        hostNames: hostNames
+        templateName: require('templates/common/items_list_popup'),
+        items: hostNames
       })
     });
   },

+ 34 - 115
ambari-web/app/controllers/wizard/step3_controller.js

@@ -38,68 +38,6 @@ App.WizardStep3Controller = Em.Controller.extend({
   stopBootstrap: false,
   isSubmitDisabled: true,
 
-  categoryObject: Em.Object.extend({
-    hostsCount: function () {
-      var category = this;
-      var hosts = this.get('controller.hosts').filter(function(_host) {
-        if (_host.get('bootStatus') == category.get('hostsBootStatus')) {
-          return true;
-        } else {
-          return (_host.get('bootStatus') == 'DONE' && category.get('hostsBootStatus') == 'REGISTERING');
-        }
-      }, this);
-      return hosts.get('length');
-    }.property('controller.hosts.@each.bootStatus'), // 'hosts.@each.bootStatus'
-    label: function () {
-      return "%@ (%@)".fmt(this.get('value'), this.get('hostsCount'));
-    }.property('value', 'hostsCount')
-  }),
-  getCategory: function(field, value){
-    return this.get('categories').find(function(item){
-      return item.get(field) == value;
-    });
-  },
-
-  categories: function () {
-    var self = this;
-    self.categoryObject.reopen({
-      controller: self,
-      isActive: function(){
-        return this.get('controller.category') == this;
-      }.property('controller.category'),
-      itemClass: function(){
-        return this.get('isActive') ? 'active' : '';
-      }.property('isActive')
-    });
-
-    var categories = [
-      self.categoryObject.create({value: Em.I18n.t('common.all'), hostsCount: function () {
-        return this.get('controller.hosts.length');
-      }.property('controller.hosts.length') }),
-      self.categoryObject.create({value: Em.I18n.t('installer.step3.hosts.status.installing'), hostsBootStatus: 'RUNNING'}),
-      self.categoryObject.create({value: Em.I18n.t('installer.step3.hosts.status.registering'), hostsBootStatus: 'REGISTERING'}),
-      self.categoryObject.create({value: Em.I18n.t('common.success'), hostsBootStatus: 'REGISTERED' }),
-      self.categoryObject.create({value: Em.I18n.t('common.fail'), hostsBootStatus: 'FAILED', last: true })
-    ];
-
-    this.set('category', categories.get('firstObject'));
-
-    return categories;
-  }.property(),
-
-  category: false,
-
-  allChecked: false,
-
-  onAllChecked: function () {
-    var hosts = this.get('visibleHosts');
-    hosts.setEach('isChecked', this.get('allChecked'));
-  }.observes('allChecked'),
-
-  noHostsSelected: function () {
-    return !(this.hosts.someProperty('isChecked', true));
-  }.property('hosts.@each.isChecked'),
-
   isRetryDisabled: true,
   isLoaded: false,
 
@@ -160,20 +98,8 @@ App.WizardStep3Controller = Em.Controller.extend({
 
       hosts.pushObject(hostInfo);
     }
-
-    if(hosts.length > 200) {
-      lazyloading.run({
-        destination: this.get('hosts'),
-        source: hosts,
-        context: this,
-        initSize: 100,
-        chunkSize: 150,
-        delay: 50
-      });
-    } else {
-      this.set('hosts', hosts);
-      this.set('isLoaded', true);
-    }
+    this.set('hosts', hosts);
+    this.set('isLoaded', true);
   },
 
   /**
@@ -195,22 +121,6 @@ App.WizardStep3Controller = Em.Controller.extend({
     return this.get('bootHosts').length != 0 && this.get('bootHosts').someProperty('bootStatus', 'RUNNING');
   },
 
-  filterByCategory: function () {
-    var category = this.get('category.hostsBootStatus');
-    if (category) {
-      this.get('hosts').forEach(function (host) {
-        host.set('isVisible', (category === host.get('bootStatus')));
-      });
-    } else { // if (this.get('category') === 'All Hosts')
-      this.get('hosts').setEach('isVisible', true);
-    }
-  }.observes('category', 'hosts.@each.bootStatus'),
-
-  /* Returns the current set of visible hosts on view (All, Succeeded, Failed) */
-  visibleHosts: function () {
-    return this.get('hosts').filterProperty('isVisible');
-  }.property('hosts.@each.isVisible'),
-
   removeHosts: function (hosts) {
     var self = this;
     App.showConfirmationPopup(function() {
@@ -229,7 +139,7 @@ App.WizardStep3Controller = Em.Controller.extend({
 
   removeSelectedHosts: function () {
     if (!this.get('noHostsSelected')) {
-      var selectedHosts = this.get('visibleHosts').filterProperty('isChecked', true);
+      var selectedHosts = this.get('hosts').filterProperty('isChecked', true);
       selectedHosts.forEach(function (_hostInfo) {
         console.log('Removing:  ' + _hostInfo.name);
       });
@@ -237,6 +147,24 @@ App.WizardStep3Controller = Em.Controller.extend({
     }
   },
 
+  /**
+   * show popup with the list of hosts which are selected
+   */
+  selectedHostsPopup: function () {
+    var selectedHosts = this.get('hosts').filterProperty('isChecked').mapProperty('name');
+    App.ModalPopup.show({
+      header: Em.I18n.t('installer.step3.selectedHosts.popup.header'),
+      onPrimary: function () {
+        this.hide();
+      },
+      secondary: null,
+      bodyClass: Ember.View.extend({
+        items: selectedHosts,
+        templateName: require('templates/common/items_list_popup')
+      })
+    });
+  },
+
   retryHost: function (hostInfo) {
     this.retryHosts([hostInfo]);
   },
@@ -287,7 +215,7 @@ App.WizardStep3Controller = Em.Controller.extend({
   setRegistrationInProgress: function () {
     var bootHosts = this.get('bootHosts');
     //if hosts aren't loaded yet then registration should be in progress
-    var result = (bootHosts.length === 0);
+    var result = (bootHosts.length === 0 && !this.get('isLoaded'));
     for (var i = 0, l = bootHosts.length; i < l; i++) {
       if (bootHosts[i].get('bootStatus') !== 'REGISTERED' && bootHosts[i].get('bootStatus') !== 'FAILED') {
         result = true;
@@ -679,20 +607,18 @@ App.WizardStep3Controller = Em.Controller.extend({
   },
 
   submit: function () {
-    if (!this.get('isSubmitDisabled')) {
-        if(this.get('isHostHaveWarnings')) {
-            var self = this;
-            App.showConfirmationPopup(
-                function(){
-                    self.set('content.hosts', self.get('bootHosts'));
-                    App.router.send('next');
-                },
-                Em.I18n.t('installer.step3.hostWarningsPopup.hostHasWarnings'));
-        }
-        else {
-              this.set('content.hosts', this.get('bootHosts'));
-              App.router.send('next');
-        }
+    if (this.get('isHostHaveWarnings')) {
+      var self = this;
+      App.showConfirmationPopup(
+        function () {
+          self.set('content.hosts', self.get('bootHosts'));
+          App.router.send('next');
+        },
+        Em.I18n.t('installer.step3.hostWarningsPopup.hostHasWarnings'));
+    }
+    else {
+      this.set('content.hosts', this.get('bootHosts'));
+      App.router.send('next');
     }
   },
 
@@ -1369,13 +1295,6 @@ App.WizardStep3Controller = Em.Controller.extend({
         registeredHosts: self.get('registeredHosts')
       })
     })
-  },
-
-  back: function () {
-    if (this.get('isRegistrationInProgress')) {
-      return;
-    }
-    App.router.send('back');
   }
 
 });

+ 2 - 0
ambari-web/app/messages.js

@@ -531,6 +531,8 @@ Em.I18n.translations = {
   'installer.step3.hosts.bootLog.registering':'\nRegistering with the server...',
   'installer.step3.hostLogPopup.highlight':'click to highlight',
   'installer.step3.hostLogPopup.copy':'press CTRL+C',
+  'installer.step3.hostsTable.selectAll':'Select All Hosts',
+  'installer.step3.selectedHosts.popup.header':'Selected Hosts',
 
   'installer.step4.header':'Choose Services',
   'installer.step4.body':'Choose which services you want to install on your cluster.',

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

@@ -414,6 +414,19 @@ h1 {
     }
   }
   #confirm-hosts {
+    table {
+      margin-bottom: 0;
+      td {
+        input[type="checkbox"] {
+          margin-left: 15px;
+        }
+      }
+      th {
+        input[type="checkbox"] {
+          margin: 0;
+        }
+      }
+    }
     #host-filter {
       margin-top: 3px;
       ul {
@@ -437,6 +450,12 @@ h1 {
         }
       }
     }
+    .page-bar {
+      padding: 5px;
+      .selected-hosts-info {
+        margin: 0;
+      }
+    }
     .progress {
       margin-bottom: 0;
     }

+ 3 - 3
ambari-web/app/templates/wizard/step2_host_name_pattern_popup.hbs → ambari-web/app/templates/common/items_list_popup.hbs

@@ -16,6 +16,6 @@
 * limitations under the License.
 }}
 
-{{#each host in view.hostNames}}
-  <p>{{host}}</p>
-{{/each}}
+{{#each item in view.items}}
+    <p>{{item}}</p>
+{{/each}}

+ 95 - 63
ambari-web/app/templates/wizard/step3.hbs

@@ -23,11 +23,11 @@
   <div class="box">
     <div class="box-header">
       <div class="button-section">
-        <a class="btn btn-primary" {{bindAttr disabled="noHostsSelected"}}
-           href="#" {{action removeSelectedHosts target="controller" }}><i
+        <button class="btn btn-primary" {{bindAttr disabled="view.noHostsSelected"}}
+           {{action removeSelectedHosts target="controller" }}><i
                 class="icon-trash icon-white"></i>
           {{t installer.step3.removeSelected}}
-        </a>
+        </button>
         {{#unless isRetryDisabled}}
         <a class="btn btn-primary decommission"
            href="#" {{action retrySelectedHosts target="controller"}}><i
@@ -39,9 +39,9 @@
         <div id="host-filter" class="pull-right">
           <ul class="clearfix">
             <li class="first">{{t common.show}}:</li>
-            {{#each category in controller.categories}}
+            {{#each category in view.categories}}
               <li {{bindAttr class="category.itemClass"}}>
-                <a {{action selectCategory category target="controller"}} href="#">
+                <a {{action selectCategory category target="view"}} href="#">
                   {{category.label}}
                 </a>
               </li>
@@ -54,64 +54,96 @@
       </div>
     </div>
 
-    <div class="pre-scrollable" style="max-height: 440px;">
-      <table class="table table-bordered table-striped">
-        <thead>
-        <tr>
-          <th class="span1">{{view Ember.Checkbox checkedBinding="allChecked"}}</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="span3">{{t common.action}}</th>
-          <!-- trash icon -->
-          <!-- retry icon -->
-        </tr>
-        </thead>
-
-        <tbody>
-        {{#each host in hosts}}
-        {{#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 class="pre-scrollable" style="max-height: 440px;">
+          <table class="table table-bordered table-striped">
+              <thead>
+              <tr>
+                  <th class="span1">
+                      <div class="btn-group">
+                          <a class="btn">
+                            {{view Ember.Checkbox checkedBinding="view.pageChecked"}}
+                          </a>
+                          <button class="btn dropdown-toggle" data-toggle="dropdown">
+                              <span class="caret"></span>
+                          </button>
+                          <ul class="dropdown-menu">
+                              <li>
+                                  <a {{action selectAll target="view"}}>{{t installer.step3.hostsTable.selectAll}}</a>
+                              </li>
+                          </ul>
+                      </div>
+                  </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="span3">{{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 span6">
+                {{#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="info">{{view.paginationInfo}}</div>
+              <div class="paging_two_button">
+                {{view view.paginationFirst}}
+                {{view view.paginationLeft}}
+                {{view view.paginationRight}}
+                {{view view.paginationLast}}
+              </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}}
-        {{#unless visibleHosts.length}}
-        <tr>
-            <td colspan="5"><p>{{t installer.step3.hosts.noHosts}}</p></td>
-        </tr>
-        {{/unless}}
-
-        </tbody>
-
-      </table>
-    </div>
-    <div class="box-footer">
-      <hr/>
-      <div class="footer-pagination">
       </div>
-    </div>
   </div>
     {{#if hasMoreRegisteredHosts}}
         <div {{bindAttr class=":alert alert-warn"}}>
@@ -126,7 +158,7 @@
         {{/unless}}
       </div>
   <div class="btn-area">
-    <a class="btn pull-left" {{bindAttr disabled="isRegistrationInProgress"}} {{action back target="controller"}}>&larr; {{t common.back}}</a>
-    <a class="btn btn-success pull-right" {{bindAttr disabled="isSubmitDisabled"}} {{action submit target="controller"}}>{{t common.next}} &rarr;</a>
+    <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>
   </div>
 </div>

+ 80 - 13
ambari-web/app/views/common/table_view.js

@@ -46,6 +46,21 @@ App.TableView = Em.View.extend(App.UserPref, {
    */
   displayLengthOnLoad: null,
 
+  /**
+   * The number of rows to show on every page
+   * The value should be a number converted into string type in order to support select element API
+   * Example: "10", "25"
+   * @type {String}
+   */
+  displayLength: null,
+
+  /**
+   * default value of display length
+   * The value should be a number converted into string type in order to support select element API
+   * Example: "10", "25"
+   */
+  defaultDisplayLength: "10",
+
   /**
    * Do filtering, using saved in the local storage filter conditions
    */
@@ -54,12 +69,14 @@ App.TableView = Em.View.extend(App.UserPref, {
     var name = this.get('controller.name');
 
     this.set('startIndexOnLoad', App.db.getStartIndex(name));
-    if (App.db.getDisplayLength(name)) {
-      this.set('displayLength', App.db.getDisplayLength(name));
-    } else {
-      self.dataLoading().done(function (initValue) {
-        self.set('displayLength', initValue);
-      });
+    if (!this.get('displayLength')) {
+      if (App.db.getDisplayLength(name)) {
+        this.set('displayLength', App.db.getDisplayLength(name));
+      } else {
+        self.dataLoading().done(function (initValue) {
+          self.set('displayLength', initValue);
+        });
+      }
     }
 
     var filterConditions = App.db.getFilterConditions(name);
@@ -130,7 +147,7 @@ App.TableView = Em.View.extend(App.UserPref, {
   getUserPrefErrorCallback: function () {
     // this user is first time login
     console.log('Persist did NOT find the key');
-    var displayLengthDefault = "10";
+    var displayLengthDefault = this.get('defaultDisplayLength');
     this.set('displayLengthOnLoad', displayLengthDefault);
     if (App.get('isAdmin')) {
       this.postUserPref(this.displayLengthKey(), displayLengthDefault);
@@ -173,7 +190,9 @@ App.TableView = Em.View.extend(App.UserPref, {
     }.property("parentView.startIndex", 'filteredContent.length'),
 
     click: function () {
-      this.get('parentView').previousPage();
+      if (this.get('class') === "paginate_previous") {
+        this.get('parentView').previousPage();
+      }
     }
   }),
 
@@ -189,7 +208,45 @@ App.TableView = Em.View.extend(App.UserPref, {
     }.property("parentView.endIndex", 'filteredContent.length'),
 
     click: function () {
-      this.get('parentView').nextPage();
+      if (this.get('class') === "paginate_next") {
+        this.get('parentView').nextPage();
+      }
+    }
+  }),
+
+  paginationFirst: Ember.View.extend({
+    tagName: 'a',
+    template: Ember.Handlebars.compile('<i class="icon-step-backward"></i>'),
+    classNameBindings: ['class'],
+    class: function () {
+      if ((this.get("parentView.endIndex")) > parseInt(this.get("parentView.displayLength"))) {
+        return "paginate_previous";
+      }
+      return "paginate_disabled_previous";
+    }.property("parentView.endIndex", 'filteredContent.length'),
+
+    click: function () {
+      if (this.get('class') === "paginate_previous") {
+        this.get('parentView').firstPage();
+      }
+    }
+  }),
+
+  paginationLast: Ember.View.extend({
+    tagName: 'a',
+    template: Ember.Handlebars.compile('<i class="icon-step-forward"></i>'),
+    classNameBindings: ['class'],
+    class: function () {
+      if (this.get("parentView.endIndex") !== this.get("parentView.filteredContent.length")) {
+        return "paginate_next";
+      }
+      return "paginate_disabled_next";
+    }.property("parentView.endIndex", 'filteredContent.length'),
+
+    click: function () {
+      if (this.get('class') === "paginate_next") {
+        this.get('parentView').lastPage();
+      }
     }
   }),
 
@@ -233,12 +290,22 @@ App.TableView = Em.View.extend(App.UserPref, {
       this.set('startIndex', result);
     }
   },
-
   /**
-   * The number of rows to show on every page
-   * @type {Number}
+   * Onclick handler for first page button on the page
    */
-  displayLength: null,
+  firstPage: function () {
+    this.set('startIndex', 1);
+  },
+  /**
+   * Onclick handler for last page button on the page
+   */
+  lastPage: function () {
+    var pagesCount = this.get('filteredContent.length') / parseInt(this.get('displayLength'));
+    var startIndex = (this.get('filteredContent.length') % parseInt(this.get('displayLength')) === 0) ?
+      (pagesCount - 1) * parseInt(this.get('displayLength')) :
+      Math.floor(pagesCount) * parseInt(this.get('displayLength'));
+    this.set('startIndex', ++startIndex);
+  },
 
   /**
    * Calculates default value for startIndex property after applying filter or changing displayLength

+ 128 - 5
ambari-web/app/views/wizard/step3_view.js

@@ -19,25 +19,148 @@
 
 var App = require('app');
 
-App.WizardStep3View = Em.View.extend({
+App.WizardStep3View = App.TableView.extend({
 
   templateName: require('templates/wizard/step3'),
-  category: '',
 
-  didInsertElement: function () {
-    this.get('controller').loadStep();
-  },
+  content:function () {
+    return this.get('controller.hosts');
+  }.property('controller.hosts.length'),
 
   message:'',
   linkText: '',
   status: '',
 
+  selectedCategory: function() {
+    return this.get('categories').findProperty('isActive');
+  }.property('categories.@each.isActive'),
+
   registeredHostsMessage: '',
 
+  displayLength: "20",
+
+  didInsertElement: function () {
+    this.get('controller').loadStep();
+  },
+
+  pageChecked: false,
+
+  /**
+   * select checkboxes of hosts on page
+   */
+  onPageChecked: function () {
+    if (this.get('selectionInProgress')) return;
+    this.get('pageContent').setEach('isChecked', this.get('pageChecked'));
+  }.observes('pageChecked'),
+
+  /**
+   * select checkboxes of all hosts
+   */
+  selectAll: function () {
+    this.get('content').setEach('isChecked', true);
+  },
+
+  /**
+   * reset checkbox of all hosts
+   */
+  unSelectAll: function() {
+    this.get('content').setEach('isChecked', false);
+  },
+
+  watchSelectionOnce: function(){
+    Em.run.once(this, 'watchSelection');
+  }.observes('content.@each.isChecked'),
+
+  /**
+   * watch selection and calculate flags as:
+   * - noHostsSelected
+   * - selectedHostsCount
+   * - pageChecked
+   */
+  watchSelection: function() {
+    this.set('selectionInProgress', true);
+    this.set('pageChecked', !!this.get('pageContent.length') && this.get('pageContent').everyProperty('isChecked', true));
+    this.set('selectionInProgress', false);
+    var noHostsSelected = true;
+    var selectedHostsCount = 0;
+    this.get('content').forEach(function(host){
+      selectedHostsCount += ~~host.get('isChecked');
+      noHostsSelected = (noHostsSelected) ? !host.get('isChecked') : noHostsSelected;
+    });
+    this.set('noHostsSelected', noHostsSelected);
+    this.set('selectedHostsCount', selectedHostsCount);
+  },
+
   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(),
+
+  countCategoryHosts: function () {
+    var counters = {
+      "RUNNING": 0,
+      "REGISTERING": 0,
+      "REGISTERED": 0,
+      "FAILED": 0
+    };
+    this.get('content').forEach(function (host) {
+      if (counters[host.get('bootStatus')] !== undefined) {
+        counters[host.get('bootStatus')]++;
+      }
+    }, this);
+    counters["ALL"] = this.get('content.length');
+    this.get('categories').forEach(function(category) {
+      category.set('hostsCount', counters[category.get('hostsBootStatus')]);
+    }, this);
+  }.observes('content.@each.bootStatus'),
+
+
+  /**
+   * filter hosts by category
+   */
+  filter: function () {
+    var result = [];
+    var selectedCategory = this.get('selectedCategory');
+    if (!selectedCategory || selectedCategory.get('hostsBootStatus') === 'ALL') {
+      result = this.get('content');
+    } else {
+      result = this.get('content').filterProperty('bootStatus', this.get('selectedCategory.hostsBootStatus'));
+    }
+    this.set('filteredContent', result);
+  }.observes('content.@each.bootStatus', 'selectedCategory'),
+  /**
+   * Trigger on Category click
+   * @param {Object} event
+   */
+  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));
+    });
+    this.watchSelection();
+  },
+
   monitorStatuses: function() {
     var hosts = this.get('controller.bootHosts');
     var failedHosts = hosts.filterProperty('bootStatus', 'FAILED').length;