You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ambari.apache.org by rl...@apache.org on 2018/01/05 23:04:45 UTC

[49/50] [abbrv] ambari git commit: AMBARI-22670 Ambari 3.0: Implement new design for Admin View: Integrate visual-search box. (atkach)

AMBARI-22670 Ambari 3.0: Implement new design for Admin View: Integrate visual-search box. (atkach)


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

Branch: refs/heads/branch-feature-AMBARI-20859
Commit: 72657b6f3888654266a4359089bf9ea8b6c97b6b
Parents: b171ae3
Author: Andrii Tkach <at...@apache.org>
Authored: Tue Dec 19 14:53:08 2017 +0200
Committer: Robert Levas <rl...@hortonworks.com>
Committed: Fri Jan 5 17:54:17 2018 -0500

----------------------------------------------------------------------
 .../main/resources/ui/admin-web/app/index.html  |   1 +
 .../controllers/ambariViews/ViewsListCtrl.js    | 104 +++--
 .../remoteClusters/RemoteClustersListCtrl.js    |   3 +-
 .../stackVersions/StackVersionsListCtrl.js      |   1 +
 .../app/scripts/directives/comboSearch.js       | 455 +++++++++++++++++++
 .../ui/admin-web/app/styles/combo-search.css    | 164 +++++++
 .../resources/ui/admin-web/app/styles/main.css  |  36 ++
 .../app/views/ambariViews/viewsList.html        |  45 +-
 .../app/views/directives/comboSearch.html       |  63 +++
 .../app/views/remoteClusters/list.html          |   2 +-
 .../admin-web/app/views/stackVersions/list.html |   2 +-
 .../ambariViews/ViewsListCtrl_test.js           | 167 +++++++
 .../test/unit/directives/comboSearch_test.js    | 242 ++++++++++
 13 files changed, 1201 insertions(+), 84 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/ambari/blob/72657b6f/ambari-admin/src/main/resources/ui/admin-web/app/index.html
----------------------------------------------------------------------
diff --git a/ambari-admin/src/main/resources/ui/admin-web/app/index.html b/ambari-admin/src/main/resources/ui/admin-web/app/index.html
index bf033e6..a1346ed 100644
--- a/ambari-admin/src/main/resources/ui/admin-web/app/index.html
+++ b/ambari-admin/src/main/resources/ui/admin-web/app/index.html
@@ -150,6 +150,7 @@
 <script src="scripts/directives/PasswordVerify.js"></script>
 <script src="scripts/directives/disabledTooltip.js"></script>
 <script src="scripts/directives/editableList.js"></script>
+<script src="scripts/directives/comboSearch.js"></script>
 <script src="scripts/services/Utility.js"></script>
 <script src="scripts/services/UserConstants.js"></script>
 <script src="scripts/services/User.js"></script>

http://git-wip-us.apache.org/repos/asf/ambari/blob/72657b6f/ambari-admin/src/main/resources/ui/admin-web/app/scripts/controllers/ambariViews/ViewsListCtrl.js
----------------------------------------------------------------------
diff --git a/ambari-admin/src/main/resources/ui/admin-web/app/scripts/controllers/ambariViews/ViewsListCtrl.js b/ambari-admin/src/main/resources/ui/admin-web/app/scripts/controllers/ambariViews/ViewsListCtrl.js
index 8b37dca..8c61a25 100644
--- a/ambari-admin/src/main/resources/ui/admin-web/app/scripts/controllers/ambariViews/ViewsListCtrl.js
+++ b/ambari-admin/src/main/resources/ui/admin-web/app/scripts/controllers/ambariViews/ViewsListCtrl.js
@@ -24,6 +24,29 @@ angular.module('ambariAdminConsole')
   $scope.isLoading = false;
   $scope.minInstanceForPagination = Settings.minRowsToShowPagination;
 
+  $scope.filters = [
+    {
+      key: 'short_url_name',
+      label: $t('common.name'),
+      options: []
+    },
+    {
+      key: 'url',
+      label: $t('urls.url'),
+      options: []
+    },
+    {
+      key: 'view_name',
+      label: $t('views.table.viewType'),
+      options: []
+    },
+    {
+      key: 'instance_name',
+      label: $t('urls.viewInstance'),
+      options: []
+    }
+  ];
+
   function checkViewVersionStatus(view, versionObj, versionNumber) {
     var deferred = View.checkViewVersionStatus(view.view_name, versionNumber);
 
@@ -66,27 +89,13 @@ angular.module('ambariAdminConsole')
           $scope.instances.push(instance.ViewInstanceInfo);
         });
       });
-      initTypeFilter();
+      $scope.initFilterOptions();
       $scope.filterInstances();
     }).catch(function (data) {
       Alert.error($t('views.alerts.cannotLoadViews'), data.data.message);
     });
   }
 
-  function initTypeFilter() {
-    var uniqTypes = $.unique($scope.instances.map(function(instance) {
-      return instance.view_name;
-    }));
-    $scope.typeFilterOptions = [ { label: $t('common.all'), value: '*'} ]
-      .concat(uniqTypes.map(function(type) {
-        return {
-          label: type,
-          value: type
-        };
-      }));
-    $scope.instanceTypeFilter = $scope.typeFilterOptions[0];
-  }
-
   function showInstancesOnPage() {
     var startIndex = ($scope.currentPage - 1) * $scope.instancesPerPage + 1;
     var endIndex = $scope.currentPage * $scope.instancesPerPage;
@@ -110,11 +119,7 @@ angular.module('ambariAdminConsole')
   $scope.instances = [];
   $scope.instancesPerPage = 10;
   $scope.currentPage = 1;
-  $scope.instanceNameFilter = '';
-  $scope.instanceUrlFilter = '';
   $scope.maxVisiblePages = 10;
-  $scope.isNotEmptyFilter = true;
-  $scope.instanceTypeFilter = '';
   $scope.tableInfo = {
     filtered: 0,
     showed: 0
@@ -122,25 +127,46 @@ angular.module('ambariAdminConsole')
 
   loadViews();
 
-  $scope.filterInstances = function() {
+  $scope.initFilterOptions = function() {
+    $scope.filters.forEach(function(filter) {
+      filter.options = $.unique($scope.instances.map(function(instance) {
+        if (filter.key === 'url') {
+          return '/main/view/' + instance.view_name + '/' + instance.short_url;
+        }
+        return instance[filter.key];
+      })).map(function(item) {
+        return {
+          key: item,
+          label: item
+        }
+      });
+    });
+  };
+
+  $scope.filterInstances = function(appliedFilters) {
     var filteredCount = 0;
     angular.forEach($scope.instances, function(instance) {
-      if ($scope.instanceNameFilter && instance.short_url_name.indexOf($scope.instanceNameFilter) === -1) {
-        return instance.isFiltered = false;
-      }
-      if ($scope.instanceUrlFilter && ('/main/view/'+ instance.view_name + '/' + instance.short_url).indexOf($scope.instanceUrlFilter) === -1) {
-        return instance.isFiltered = false;
-      }
-      if ($scope.instanceTypeFilter.value !== '*' && instance.view_name.indexOf($scope.instanceTypeFilter.value) === -1) {
-        return instance.isFiltered = false;
-      }
-      filteredCount++;
-      instance.isFiltered = true;
+      instance.isFiltered = !(appliedFilters && appliedFilters.length > 0 && appliedFilters.some(function(filter) {
+        if (filter.key === 'url') {
+          return filter.values.every(function(value) {
+            return ('/main/view/' + instance.view_name + '/' + instance.short_url).indexOf(value) === -1;
+          });
+        }
+        return filter.values.every(function(value) {
+          return instance[filter.key].indexOf(value) === -1;
+        });
+      }));
+
+      filteredCount += ~~instance.isFiltered;
     });
     $scope.tableInfo.filtered = filteredCount;
     $scope.resetPagination();
   };
 
+  $scope.toggleSearchBox = function() {
+    $('.search-box-button .popup-arrow-up, .search-box-row').toggleClass('hide');
+  };
+
   $scope.pageChanged = function() {
     showInstancesOnPage();
   };
@@ -150,22 +176,6 @@ angular.module('ambariAdminConsole')
     showInstancesOnPage();
   };
 
-  $scope.clearFilters = function () {
-    $scope.instanceNameFilter = '';
-    $scope.instanceUrlFilter = '';
-    $scope.instanceTypeFilter = $scope.typeFilterOptions[0];
-    $scope.resetPagination();
-  };
-
-  $scope.$watch(
-    function (scope) {
-      return Boolean(scope.instanceNameFilter || scope.instanceUrlFilter || (scope.instanceTypeFilter && scope.instanceTypeFilter.value !== '*'));
-    },
-    function (newValue, oldValue, scope) {
-      scope.isNotEmptyFilter = newValue;
-    }
-  );
-
   $scope.cloneInstance = function(instanceClone) {
     $scope.createInstance(instanceClone);
   };

http://git-wip-us.apache.org/repos/asf/ambari/blob/72657b6f/ambari-admin/src/main/resources/ui/admin-web/app/scripts/controllers/remoteClusters/RemoteClustersListCtrl.js
----------------------------------------------------------------------
diff --git a/ambari-admin/src/main/resources/ui/admin-web/app/scripts/controllers/remoteClusters/RemoteClustersListCtrl.js b/ambari-admin/src/main/resources/ui/admin-web/app/scripts/controllers/remoteClusters/RemoteClustersListCtrl.js
index 9d47307..4726357 100644
--- a/ambari-admin/src/main/resources/ui/admin-web/app/scripts/controllers/remoteClusters/RemoteClustersListCtrl.js
+++ b/ambari-admin/src/main/resources/ui/admin-web/app/scripts/controllers/remoteClusters/RemoteClustersListCtrl.js
@@ -18,9 +18,10 @@
 'use strict';
 
 angular.module('ambariAdminConsole')
-.controller('RemoteClustersListCtrl', ['$scope', '$routeParams', '$translate', 'RemoteCluster', function ($scope, $routeParams, $translate, RemoteCluster) {
+.controller('RemoteClustersListCtrl', ['$scope', '$routeParams', '$translate', 'RemoteCluster', 'Settings', function ($scope, $routeParams, $translate, RemoteCluster, Settings) {
   var $t = $translate.instant;
 
+  $scope.minInstanceForPagination = Settings.minRowsToShowPagination;
   $scope.clusterName = $routeParams.clusterName;
   $scope.isLoading = false;
 

http://git-wip-us.apache.org/repos/asf/ambari/blob/72657b6f/ambari-admin/src/main/resources/ui/admin-web/app/scripts/controllers/stackVersions/StackVersionsListCtrl.js
----------------------------------------------------------------------
diff --git a/ambari-admin/src/main/resources/ui/admin-web/app/scripts/controllers/stackVersions/StackVersionsListCtrl.js b/ambari-admin/src/main/resources/ui/admin-web/app/scripts/controllers/stackVersions/StackVersionsListCtrl.js
index 003d472..ae00978 100644
--- a/ambari-admin/src/main/resources/ui/admin-web/app/scripts/controllers/stackVersions/StackVersionsListCtrl.js
+++ b/ambari-admin/src/main/resources/ui/admin-web/app/scripts/controllers/stackVersions/StackVersionsListCtrl.js
@@ -23,6 +23,7 @@ angular.module('ambariAdminConsole')
     $scope.getConstant = function (key) {
       return $t(key).toLowerCase();
     };
+    $scope.minInstanceForPagination = Settings.minRowsToShowPagination;
     $scope.isLoading = false;
     $scope.clusterName = $routeParams.clusterName;
     $scope.filter = {

http://git-wip-us.apache.org/repos/asf/ambari/blob/72657b6f/ambari-admin/src/main/resources/ui/admin-web/app/scripts/directives/comboSearch.js
----------------------------------------------------------------------
diff --git a/ambari-admin/src/main/resources/ui/admin-web/app/scripts/directives/comboSearch.js b/ambari-admin/src/main/resources/ui/admin-web/app/scripts/directives/comboSearch.js
new file mode 100644
index 0000000..af25167
--- /dev/null
+++ b/ambari-admin/src/main/resources/ui/admin-web/app/scripts/directives/comboSearch.js
@@ -0,0 +1,455 @@
+/**
+ * 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.
+ */
+'use strict';
+
+angular.module('ambariAdminConsole')
+.directive('comboSearch', function() {
+  return {
+    restrict: 'E',
+    templateUrl: 'views/directives/comboSearch.html',
+    scope: {
+      suggestions: '=',
+      filterChange: '=',
+      placeholder: '@',
+      supportCategories: '@'
+    },
+    controller: ['$scope', function($scope) {
+      return {
+        suggestions: $scope.suggestions,
+        placeholder: $scope.placeholder,
+        filterChange: $scope.filterChange,
+        supportCategories: $scope.supportCategories === "true"
+      }
+    }],
+    link: function($scope, $elem, $attr, $ctrl) {
+      var idCounter = 1;
+      var suggestions = $ctrl.suggestions;
+      var supportCategories = $ctrl.supportCategories;
+      var mainInputElement = $elem.find('.main-input.combo-search-input');
+      $scope.paceholder = $ctrl.placeholder;
+      $scope.searchFilterInput = '';
+      $scope.filterSuggestions = [];
+      $scope.showAutoComplete = false;
+      $scope.appliedFilters = [];
+
+      attachInputWidthSetter(mainInputElement);
+      initKeyHandlers();
+      initBlurHandler();
+
+      $scope.$watch(function () {
+        return $scope.appliedFilters.length;
+      }, function () {
+        attachInputWidthSetter($elem.find('.combo-search-input'));
+      });
+
+      $scope.removeFilter = function(filter) {
+        $scope.appliedFilters = $scope.appliedFilters.filter(function(item) {
+          return filter.id !== item.id;
+        });
+        $scope.observeSearchFilterInput(event);
+        mainInputElement.focus();
+        $scope.updateFilters($scope.appliedFilters);
+      };
+
+      $scope.clearFilters = function() {
+        $scope.appliedFilters = [];
+        $scope.updateFilters($scope.appliedFilters);
+      };
+
+      $scope.selectFilter = function(filter, event) {
+        var newAppliedFilter = {
+          id: 'filter_' + idCounter++,
+          currentOption: null,
+          filteredOptions: [],
+          searchOptionInput: '',
+          key: filter.key,
+          label: filter.label,
+          options: filter.options || [],
+          showAutoComplete: false
+        };
+        $scope.appliedFilters.push(newAppliedFilter);
+        if (event) {
+          event.stopPropagation();
+          event.preventDefault();
+        }
+        $scope.isEditing = false;
+        $scope.showAutoComplete = false;
+        $scope.searchFilterInput = '';
+        _.debounce(function() {
+          $('input[name=' + newAppliedFilter.id + ']').focus().width(4);
+        }, 100)();
+      };
+
+      $scope.selectOption = function(event, option, filter) {
+        $('input[name=' + filter.id + ']').val(option.label).trigger('input');
+        filter.showAutoComplete = false;
+        mainInputElement.focus();
+        $scope.observeSearchFilterInput(event);
+        filter.currentOption = option;
+        $scope.updateFilters($scope.appliedFilters);
+      };
+
+      $scope.hideAutocomplete = function(filter) {
+        _.debounce(function() {
+          if (filter) {
+            filter.showAutoComplete = false;
+          } else {
+            if (!$scope.isEditing) {
+              $scope.showAutoComplete = false;
+            }
+          }
+          $scope.$apply();
+        }, 100)();
+      };
+
+      $scope.forceFocus = function(event, filter) {
+        $(event.currentTarget).find('.combo-search-input').focus();
+        $scope.showAutoComplete = false;
+        $scope.observeSearchOptionInput(filter);
+        event.stopPropagation();
+        event.preventDefault();
+      };
+
+      $scope.makeActive = function(active, all) {
+        if (active.isCategory) {
+          return false;
+        }
+        all.forEach(function(item) {
+          item.active = active.key === item.key;
+        });
+      };
+
+      $scope.observeSearchFilterInput = function(event) {
+        if (event) {
+          mainInputElement.focus();
+          $scope.isEditing = true;
+          event.stopPropagation();
+          event.preventDefault();
+        }
+
+        var filteredSuggestions = suggestions.filter(function(item) {
+          return (!$scope.searchFilterInput || item.label.toLowerCase().indexOf($scope.searchFilterInput.toLowerCase()) !== -1);
+        });
+        if (filteredSuggestions.length > 0) {
+          $scope.makeActive(filteredSuggestions[0], filteredSuggestions);
+          $scope.showAutoComplete = true;
+        } else {
+          $scope.showAutoComplete = false;
+        }
+        $scope.filterSuggestions = supportCategories ? formatCategorySuggestions(filteredSuggestions) : filteredSuggestions;
+      };
+
+      $scope.observeSearchOptionInput = function(filter) {
+        var appliedOptions = {};
+        $scope.appliedFilters.forEach(function(item) {
+          if (item.key === filter.key && item.currentOption) {
+            appliedOptions[item.currentOption.key] = true;
+          }
+        });
+
+        if (filter.currentOption && filter.currentOption.key !== filter.searchOptionInput) {
+          filter.currentOption = null;
+        }
+        filter.filteredOptions = filter.options.filter(function(option) {
+          return !(option.key === '' || option.key === undefined || appliedOptions[option.key])
+            && (!filter.searchOptionInput || option.label.toLowerCase().indexOf(filter.searchOptionInput.toLowerCase()) !== -1);
+        });
+        filter.showAutoComplete = filter.filteredOptions.length > 0;
+        if (filter.filteredOptions.length > 0) {
+          $scope.makeActive(filter.filteredOptions[0], filter.filteredOptions);
+        }
+      };
+
+      $scope.extractFilters = function(filters) {
+        var map = {};
+        var result = [];
+
+        filters.forEach(function(filter) {
+          if (filter.currentOption) {
+            if (!map[filter.key]) {
+              map[filter.key] = [];
+            }
+            map[filter.key].push(filter.currentOption.key);
+          }
+        });
+        for(var key in map) {
+          result.push({
+            key: key,
+            values: map[key]
+          });
+        }
+        return result;
+      };
+
+      $scope.updateFilters = function(appliedFilters) {
+        $ctrl.filterChange($scope.extractFilters(appliedFilters));
+      };
+
+      function formatCategorySuggestions(suggestions) {
+        var categories = {};
+        var result = [];
+        suggestions.forEach(function(item) {
+          if (!item.category) {
+            item.category = 'default';
+          }
+          if (!categories[item.category]) {
+            categories[item.category] = [];
+          }
+          categories[item.category].push(item);
+        });
+
+        for(var cat in categories) {
+          result.push({
+            key: cat,
+            label: cat,
+            isCategory: true,
+            isDefault: cat === 'default'
+          });
+          result = result.concat(categories[cat]);
+        }
+        return result;
+      }
+
+      function initBlurHandler() {
+        $(document).click(function() {
+          $scope.isEditing = false;
+          $scope.hideAutocomplete();
+        });
+      }
+
+      function findActiveByName(array, name) {
+        for (var i = 0; i < array.length; i++) {
+          if (array[i].id === name) {
+            return i;
+          }
+        }
+        return null;
+      }
+
+      function findActiveByProperty(array) {
+        for (var i = 0; i < array.length; i++) {
+          if (array[i].active) {
+            return i;
+          }
+        }
+        return 0;
+      }
+
+      function focusInput(filter) {
+        $('input[name=' + filter.id + ']').focus();
+        $scope.showAutoComplete = false;
+        $scope.observeSearchOptionInput(filter);
+      }
+
+      function initKeyHandlers() {
+        $(document).keydown(function(event) {
+          if (event.which === 13) { // "Enter" key
+            enterKeyHandler();
+            $scope.$apply();
+          }
+          if (event.which === 8) { // "Backspace" key
+            backspaceKeyHandler(event);
+            $scope.$apply();
+          }
+          if (event.which === 38) { // "Up" key
+            upKeyHandler();
+            $scope.$apply();
+          }
+          if (event.which === 40) { // "Down" key
+            downKeyHandler();
+            $scope.$apply();
+          }
+          if (event.which === 39) { // "Right Arrow" key
+            rightArrowKeyHandler();
+            $scope.$apply();
+          }
+          if (event.which === 37) { // "Left Arrow" key
+            leftArrowKeyHandler();
+            $scope.$apply();
+          }
+        });
+      }
+
+      function leftArrowKeyHandler() {
+        var activeElement = $(document.activeElement);
+        if (activeElement.is('input') && activeElement[0].selectionStart === 0) {
+          if (activeElement.hasClass('main-input')) {
+            focusInput($scope.appliedFilters[$scope.appliedFilters.length - 1]);
+          } else {
+            var activeIndex = findActiveByName($scope.appliedFilters, activeElement.attr('name'));
+            if (activeIndex !== null && activeIndex > 0) {
+              focusInput($scope.appliedFilters[activeIndex - 1]);
+            }
+          }
+        }
+      }
+
+      function rightArrowKeyHandler() {
+        var activeElement = $(document.activeElement);
+        if (activeElement.is('input') && activeElement[0].selectionStart === activeElement.val().length) {
+          if (!activeElement.hasClass('main-input')) {
+            var activeIndex = findActiveByName($scope.appliedFilters, activeElement.attr('name'));
+            if (activeIndex !== null) {
+              if (activeIndex === $scope.appliedFilters.length - 1) {
+                mainInputElement.focus();
+                $scope.observeSearchFilterInput();
+              } else {
+                focusInput($scope.appliedFilters[activeIndex + 1]);
+              }
+            }
+          }
+        }
+      }
+
+      function downKeyHandler() {
+        var activeIndex = 0;
+        var nextIndex = null;
+
+        if ($scope.showAutoComplete) {
+          activeIndex = findActiveByProperty($scope.filterSuggestions);
+          if (activeIndex < $scope.filterSuggestions.length - 1) {
+            if ($scope.filterSuggestions[activeIndex + 1].isCategory && activeIndex + 2 < $scope.filterSuggestions.length) {
+              nextIndex = activeIndex + 2;
+            } else {
+              nextIndex = activeIndex + 1;
+            }
+          } else {
+            nextIndex = ($scope.filterSuggestions[0].isCategory) ? 1 : 0;
+          }
+          if (nextIndex !== null) {
+            $scope.makeActive($scope.filterSuggestions[nextIndex], $scope.filterSuggestions);
+          }
+        } else {
+          var activeAppliedFilters = $scope.appliedFilters.filter(function(item) {
+            return item.showAutoComplete;
+          });
+          if (activeAppliedFilters.length > 0) {
+            var filteredOptions = activeAppliedFilters[0].filteredOptions;
+            activeIndex = findActiveByProperty(filteredOptions);
+            nextIndex = (activeIndex < filteredOptions.length - 1) ? activeIndex + 1 : 0;
+          }
+          if (nextIndex !== null) {
+            $scope.makeActive(filteredOptions[nextIndex], filteredOptions);
+          }
+        }
+      }
+
+      function upKeyHandler() {
+        var activeIndex = 0;
+        var nextIndex = null;
+
+        if ($scope.showAutoComplete) {
+          activeIndex = findActiveByProperty($scope.filterSuggestions);
+          if (activeIndex > 0) {
+            if ($scope.filterSuggestions[activeIndex - 1].isCategory) {
+              nextIndex = (activeIndex - 2 > 0) ? activeIndex - 2 : $scope.filterSuggestions.length - 1;
+            } else {
+              nextIndex = activeIndex - 1;
+            }
+          } else {
+            nextIndex = $scope.filterSuggestions.length - 1;
+          }
+          if (nextIndex !== null) {
+            $scope.makeActive($scope.filterSuggestions[nextIndex], $scope.filterSuggestions);
+          }
+        } else {
+          var activeAppliedFilters = $scope.appliedFilters.filter(function(item) {
+            return item.showAutoComplete;
+          });
+          if (activeAppliedFilters.length > 0) {
+            var filteredOptions = activeAppliedFilters[0].filteredOptions;
+            activeIndex = findActiveByProperty(filteredOptions);
+            nextIndex = (activeIndex > 0) ?  activeIndex - 1 : filteredOptions.length - 1;
+          }
+          if (nextIndex !== null) {
+            $scope.makeActive(filteredOptions[nextIndex], filteredOptions);
+          }
+        }
+      }
+
+      function enterKeyHandler() {
+        if ($scope.showAutoComplete) {
+          var activeFilters = $scope.filterSuggestions.filter(function(item) {
+            return item.active;
+          });
+          if (activeFilters.length > 0) {
+            $scope.selectFilter(activeFilters[0]);
+          }
+        } else {
+          var activeAppliedFilters = $scope.appliedFilters.filter(function(item) {
+            return item.showAutoComplete;
+          });
+          if (activeAppliedFilters.length > 0) {
+            var activeOptions = activeAppliedFilters[0].filteredOptions.filter(function(item) {
+              return item.active;
+            });
+            if (activeOptions.length > 0) {
+              $scope.selectOption(null, activeOptions[0], activeAppliedFilters[0]);
+            }
+          } else {
+            $scope.appliedFilters.filter(function(item) {
+              return !item.currentOption;
+            }).forEach(function(item) {
+              if (item.searchOptionInput !== '') {
+                $scope.selectOption(null, {
+                  key: item.searchOptionInput,
+                  label: item.searchOptionInput
+                }, item);
+              }
+            });
+          }
+        }
+      }
+
+      function backspaceKeyHandler (event) {
+        if ($(document.activeElement).is('input') && $(document.activeElement)[0].selectionStart === 0) {
+          if ($(document.activeElement).hasClass('main-input') && $scope.appliedFilters.length > 0) {
+            var lastFilter = $scope.appliedFilters[$scope.appliedFilters.length - 1];
+            focusInput(lastFilter);
+            event.stopPropagation();
+            event.preventDefault();
+          } else {
+            var name = $(document.activeElement).attr('name');
+            var activeFilter = $scope.appliedFilters.filter(function(item) {
+              return name === item.id;
+            })[0];
+            if (activeFilter) {
+              $scope.removeFilter(activeFilter);
+            }
+          }
+        }
+      }
+
+      function attachInputWidthSetter(element) {
+        var textPadding = 4;
+        element.on('input', function() {
+          var inputWidth = $(this).textWidth();
+          $(this).css({
+            width: inputWidth + textPadding
+          })
+        }).trigger('input');
+      }
+    }
+  };
+});
+
+$.fn.textWidth = function(text, font) {
+  if (!$.fn.textWidth.fakeEl) $.fn.textWidth.fakeEl = $('<span>').hide().appendTo(document.body);
+  $.fn.textWidth.fakeEl.text(text || this.val() || this.text() || this.attr('placeholder')).css('font', font || this.css('font'));
+  return $.fn.textWidth.fakeEl.width();
+};

http://git-wip-us.apache.org/repos/asf/ambari/blob/72657b6f/ambari-admin/src/main/resources/ui/admin-web/app/styles/combo-search.css
----------------------------------------------------------------------
diff --git a/ambari-admin/src/main/resources/ui/admin-web/app/styles/combo-search.css b/ambari-admin/src/main/resources/ui/admin-web/app/styles/combo-search.css
new file mode 100644
index 0000000..ee9909c
--- /dev/null
+++ b/ambari-admin/src/main/resources/ui/admin-web/app/styles/combo-search.css
@@ -0,0 +1,164 @@
+/**
+ * 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.
+ */
+
+.combo-search .combo-search-inner {
+  position: relative;
+  border: 1px solid #ccc;
+  border-radius: 4px;
+  box-shadow: 0 0 1px #fff inset;
+  min-height: 34px;
+  line-height: 1em;
+  cursor: text;
+  padding-right: 30px;
+}
+
+.combo-search .combo-search-close {
+  font-size: 13px;
+  color: #999;
+  position: absolute;
+  right: 10px;
+  top: 30%;
+  cursor: pointer;
+}
+.combo-search .combo-search-close:hover {
+  color: #333;
+}
+.combo-search .combo-search-input {
+  background: transparent;
+  display: inline-block;
+  min-width: 4px;
+  width: 4px;
+  line-height: 10px;
+  height: 100%;
+  border: none;
+  outline: none;
+  margin-left: 1px;
+}
+
+.combo-search .combo-search-input-wrapper {
+  display: inline-block;
+  position: relative;
+  height: 32px;
+  margin-left: 5px;
+}
+
+.combo-search .combo-search-content {
+  display: inline-block;
+}
+
+.combo-search .combo-search-dropdown {
+  position: absolute;
+  border: 1px solid #ccc;
+  border-radius: 4px;
+  background-color: #fff;
+  cursor: pointer;
+  z-index: 10;
+  padding: 0;
+  margin: 0;
+  width: auto;
+  min-width: 80px;
+  max-width: 220px;
+  max-height: 240px;
+  overflow-y: auto;
+  overflow-x: hidden;
+  font-size: 13px;
+  top: 30px;
+  left: 5px;
+  box-shadow: 3px 4px 5px -2px rgba(0, 0, 0, 0.5);
+}
+
+.combo-search .combo-search-dropdown ul {
+  max-height: 250px;
+  list-style: none;
+  padding: 0;
+  margin: 0;
+}
+
+.combo-search .filter a {
+  display: block;
+  width: auto;
+  text-decoration: none;
+  color: initial;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  background: none;
+  border: none;
+  padding: 3px 10px 5px 5px;
+}
+
+.combo-search .category a {
+  display: block;
+  width: auto;
+  text-decoration: none;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  padding: 3px 10px 5px 5px;
+  text-transform: capitalize;
+  font-size: 11px;
+  font-weight: bold;
+  color: white;
+  cursor: default;
+  border-bottom: 1px solid #A2A2A2;
+  background-color: #B7B7B7;
+  text-shadow: 0 -1px 0 #999;
+}
+
+.combo-search .filter a.active {
+  background-color: #ddd;
+}
+
+.combo-search .combo-search-applied-filter {
+  position: relative;
+  display: inline-block;
+  padding: 0 3px 0 18px;
+  background-color: #dddddd;
+  border-radius: 4px;
+  margin: 4px 0 0 4px;
+  vertical-align: top;
+  border: 1px solid #d2d2d2;
+}
+
+.combo-search .combo-search-applied-filter i {
+  position: absolute;
+  left: 5px;
+  font-size: 12px;
+  top: 5px;
+  color: #999;
+  cursor: pointer;
+}
+
+.combo-search .combo-search-applied-filter i:hover {
+  color: #333;
+}
+
+.combo-search .combo-search-applied-filter span,
+.combo-search .combo-search-applied-filter input {
+  color: #666;
+  font-size: 11px;
+}
+
+.combo-search .combo-search-applied-filter span {
+  font-weight: bold;
+}
+
+.combo-search .combo-search-applied-filter .combo-search-input-wrapper {
+  height: 22px;
+  margin-left: 0;
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ambari/blob/72657b6f/ambari-admin/src/main/resources/ui/admin-web/app/styles/main.css
----------------------------------------------------------------------
diff --git a/ambari-admin/src/main/resources/ui/admin-web/app/styles/main.css b/ambari-admin/src/main/resources/ui/admin-web/app/styles/main.css
index b4aa558..8f693a8 100644
--- a/ambari-admin/src/main/resources/ui/admin-web/app/styles/main.css
+++ b/ambari-admin/src/main/resources/ui/admin-web/app/styles/main.css
@@ -1331,3 +1331,39 @@ th.entity-actions {
 .entity-actions a:focus:hover {
   text-decoration: none;
 }
+
+.search-box-button {
+  position: relative;
+  margin-right: 5px;
+}
+
+.search-box-button .btn {
+  padding: 10px;
+}
+
+.search-box-row {
+  padding-top: 15px;
+  padding-bottom: 5px;
+}
+
+.popup-arrow-up {
+  background: inherit;
+  z-index: 1;
+  left: 6px;
+  position: absolute;
+  width: 24px;
+  height: 16px;
+  overflow: hidden;
+}
+
+.popup-arrow-up:after {
+  content: "";
+  position: absolute;
+  width: 20px;
+  height: 20px;
+  background: #fff;
+  transform: rotate(45deg);
+  top: 10px;
+  left: 2px;
+  box-shadow: -2px -2px 10px -3px rgba(0, 0, 0, 0.5);
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/72657b6f/ambari-admin/src/main/resources/ui/admin-web/app/views/ambariViews/viewsList.html
----------------------------------------------------------------------
diff --git a/ambari-admin/src/main/resources/ui/admin-web/app/views/ambariViews/viewsList.html b/ambari-admin/src/main/resources/ui/admin-web/app/views/ambariViews/viewsList.html
index ae57b86..9e9cb55 100644
--- a/ambari-admin/src/main/resources/ui/admin-web/app/views/ambariViews/viewsList.html
+++ b/ambari-admin/src/main/resources/ui/admin-web/app/views/ambariViews/viewsList.html
@@ -24,11 +24,21 @@
                 {{'views.create' | translate}}
             </button>
         </div>
+        <div class="search-box-button pull-right">
+            <button class="btn btn-default" ng-click="toggleSearchBox()">
+                <i class="fa fa-filter" aria-hidden="true"></i>
+            </button>
+            <div class="popup-arrow-up hide"></div>
+        </div>
+    </div>
+
+    <div class="search-box-row hide">
+        <combo-search suggestions="filters" filter-change="filterInstances" placeholder="Search"></combo-search>
     </div>
 
     <table class="table table-striped table-hover">
         <thead>
-        <tr class="fix-bottom">
+        <tr>
             <th class="col-md-2">
                 <span>{{'common.name' | translate}}</span>
             </th>
@@ -45,39 +55,6 @@
                 <span>{{'common.actions' | translate}}</span>
             </th>
         </tr>
-        <tr class="fix-top">
-            <th>
-                <div class="search-container">
-                    <input type="text" class="form-control" placeholder="{{'common.any' | translate}}"
-                           ng-model="instanceNameFilter" ng-change="filterInstances()">
-                    <button type="button" class="close clearfilter" ng-show="instanceNameFilter"
-                            ng-click="instanceNameFilter=''; filterInstances()">
-                        <span aria-hidden="true">&times;</span>
-                        <span class="sr-only">{{'common.controls.close' | translate}}</span>
-                    </button>
-                </div>
-            </th>
-            <th>
-                <div class="search-container">
-                    <input type="text" class="form-control" placeholder="{{'common.any' | translate}}"
-                           ng-model="instanceUrlFilter" ng-change="filterInstances()">
-                    <button type="button" class="close clearfilter" ng-show="instanceUrlFilter"
-                            ng-click="instanceUrlFilter=''; filterInstances()">
-                        <span aria-hidden="true">&times;</span>
-                        <span class="sr-only">{{'common.controls.close' | translate}}</span>
-                    </button>
-                </div>
-            </th>
-            <th>
-                <select class="form-control typefilter v-small-input"
-                        ng-model="instanceTypeFilter"
-                        ng-options="item.label for item in typeFilterOptions"
-                        ng-change="filterInstances()">
-                </select>
-            </th>
-            <th></th>
-            <th></th>
-        </tr>
         </thead>
 
         <tbody>

http://git-wip-us.apache.org/repos/asf/ambari/blob/72657b6f/ambari-admin/src/main/resources/ui/admin-web/app/views/directives/comboSearch.html
----------------------------------------------------------------------
diff --git a/ambari-admin/src/main/resources/ui/admin-web/app/views/directives/comboSearch.html b/ambari-admin/src/main/resources/ui/admin-web/app/views/directives/comboSearch.html
new file mode 100644
index 0000000..a4fdfc2
--- /dev/null
+++ b/ambari-admin/src/main/resources/ui/admin-web/app/views/directives/comboSearch.html
@@ -0,0 +1,63 @@
+<!--
+* Licensed to the Apache Software Foundation (ASF) under one
+* or more contributor license agreements.  See the NOTICE file
+* distributed with this work for additional information
+* regarding copyright ownership.  The ASF licenses this file
+* to you under the Apache License, Version 2.0 (the
+* "License"); you may not use this file except in compliance
+* with the License.  You may obtain a copy of the License at
+*
+*     http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+-->
+
+<div class="combo-search">
+  <div class="combo-search-inner" ng-click="observeSearchFilterInput($event)">
+    <div class="combo-search-applied-filter" ng-repeat="filter in appliedFilters" ng-click="forceFocus($event, filter)">
+      <i class="fa fa-times-circle" ng-click="removeFilter(filter)"></i>
+      <span>{{filter.label}}:</span>
+      <div class="combo-search-input-wrapper">
+        <input type="text"
+               autocomplete="off"
+               ng-attr-name="{{filter.id}}"
+               class="combo-search-input"
+               ng-model="filter.searchOptionInput"
+               ng-change="observeSearchOptionInput(filter)"
+               ng-blur="hideAutocomplete(filter)"/>
+        <div class="combo-search-dropdown" ng-show="filter.showAutoComplete">
+          <ul>
+            <li ng-repeat="item in filter.filteredOptions" class="filter">
+              <a ng-click="selectOption($event, item, filter)"
+                 ng-class="{active: item.active}"
+                 ng-mouseover="makeActive(item, filter.filteredOptions)">{{item.label}}</a>
+            </li>
+          </ul>
+        </div>
+      </div>
+    </div>
+    <div class="combo-search-input-wrapper">
+      <input type="text"
+             autocomplete="off"
+             placeholder="{{appliedFilters.length === 0 ? placeholder : ''}}"
+             class="combo-search-input main-input"
+             ng-model="searchFilterInput"
+             ng-change="observeSearchFilterInput()"/>
+      <div class="combo-search-dropdown" ng-show="showAutoComplete">
+        <ul>
+          <li ng-repeat="item in filterSuggestions"
+              ng-class="{category: item.isCategory, filter: !item.isCategory, hide: (item.isCategory && item.isDefault)}">
+            <a ng-click="selectFilter(item, $event)"
+               ng-class="{active: item.active}"
+               ng-mouseover="makeActive(item, filterSuggestions)">{{item.label}}</a>
+          </li>
+        </ul>
+      </div>
+    </div>
+    <i class="combo-search-close fa fa-times-circle" ng-show="appliedFilters.length" ng-click="clearFilters()"></i>
+  </div>
+</div>

http://git-wip-us.apache.org/repos/asf/ambari/blob/72657b6f/ambari-admin/src/main/resources/ui/admin-web/app/views/remoteClusters/list.html
----------------------------------------------------------------------
diff --git a/ambari-admin/src/main/resources/ui/admin-web/app/views/remoteClusters/list.html b/ambari-admin/src/main/resources/ui/admin-web/app/views/remoteClusters/list.html
index 67d650e..7a8e6f4 100644
--- a/ambari-admin/src/main/resources/ui/admin-web/app/views/remoteClusters/list.html
+++ b/ambari-admin/src/main/resources/ui/admin-web/app/views/remoteClusters/list.html
@@ -61,7 +61,7 @@
   <div class="alert empty-table-alert col-sm-12" ng-show="!remoteClusters.length && !isLoading">
     {{'common.alerts.noRemoteClusterDisplay' | translate}}
   </div>
-  <div class="col-sm-12 table-bar">
+  <div class="col-sm-12 table-bar" ng-show="tableInfo.total >= minInstanceForPagination">
     <div class="pull-left filtered-info">
       <span>{{'common.filterInfo' | translate:{showed: tableInfo.showed, total: tableInfo.total, term: constants.groups} }}</span>
       <span ng-show="isNotEmptyFilter">- <a href ng-click="clearFilters()">{{'common.controls.clearFilters' | translate}}</a></span>

http://git-wip-us.apache.org/repos/asf/ambari/blob/72657b6f/ambari-admin/src/main/resources/ui/admin-web/app/views/stackVersions/list.html
----------------------------------------------------------------------
diff --git a/ambari-admin/src/main/resources/ui/admin-web/app/views/stackVersions/list.html b/ambari-admin/src/main/resources/ui/admin-web/app/views/stackVersions/list.html
index 279343b..9d81543 100644
--- a/ambari-admin/src/main/resources/ui/admin-web/app/views/stackVersions/list.html
+++ b/ambari-admin/src/main/resources/ui/admin-web/app/views/stackVersions/list.html
@@ -123,7 +123,7 @@
   <div class="alert empty-table-alert col-sm-12" ng-show="!repos.length && !isLoading">
     {{'common.alerts.nothingToDisplay' | translate:{term: getConstant("common.version")} }}
   </div>
-  <div class="col-sm-12 table-bar">
+  <div class="col-sm-12 table-bar" ng-show="tableInfo.total >= minInstanceForPagination">
     <div class="pull-left filtered-info">
       <span>{{'common.filterInfo' | translate:{showed: tableInfo.showed, total: tableInfo.total, term: getConstant("common.versions")} }}</span>
       <span ng-show="isNotEmptyFilter">- <a href ng-click="clearFilters()">{{'common.controls.clearFilters' | translate}}</a></span>

http://git-wip-us.apache.org/repos/asf/ambari/blob/72657b6f/ambari-admin/src/main/resources/ui/admin-web/test/unit/controllers/ambariViews/ViewsListCtrl_test.js
----------------------------------------------------------------------
diff --git a/ambari-admin/src/main/resources/ui/admin-web/test/unit/controllers/ambariViews/ViewsListCtrl_test.js b/ambari-admin/src/main/resources/ui/admin-web/test/unit/controllers/ambariViews/ViewsListCtrl_test.js
new file mode 100644
index 0000000..362b94a
--- /dev/null
+++ b/ambari-admin/src/main/resources/ui/admin-web/test/unit/controllers/ambariViews/ViewsListCtrl_test.js
@@ -0,0 +1,167 @@
+/**
+ * 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.
+ */
+
+describe('#Cluster', function () {
+  describe('ViewsListCtrl', function() {
+    var scope, ctrl;
+
+    beforeEach(function () {
+      module('ambariAdminConsole');
+      inject(function($rootScope, $controller) {
+        scope = $rootScope.$new();
+        ctrl = $controller('ViewsListCtrl', {$scope: scope});
+      });
+      scope.instances = [
+        {
+          short_url_name: 'sun1',
+          url: 'url1',
+          view_name: 'vn1',
+          instance_name: 'in1',
+          short_url: 'su1'
+        },
+        {
+          short_url_name: 'sun2',
+          url: 'url2',
+          view_name: 'vn2',
+          instance_name: 'in2',
+          short_url: 'su2'
+        }
+      ];
+    });
+
+    describe('#initFilterOptions()', function () {
+      beforeEach(function() {
+        scope.initFilterOptions();
+      });
+
+      it('should fill short_url_name options', function() {
+        expect(scope.filters[0].options).toEqual([
+          {
+            key: 'sun1',
+            label: 'sun1'
+          },
+          {
+            key: 'sun2',
+            label: 'sun2'
+          }
+        ]);
+      });
+
+      it('should fill url options', function() {
+        expect(scope.filters[1].options).toEqual([
+          {
+            key: '/main/view/vn1/su1',
+            label: '/main/view/vn1/su1'
+          },
+          {
+            key: '/main/view/vn2/su2',
+            label: '/main/view/vn2/su2'
+          }
+        ]);
+      });
+
+      it('should fill view_name options', function() {
+        expect(scope.filters[2].options).toEqual([
+          {
+            key: 'vn1',
+            label: 'vn1'
+          },
+          {
+            key: 'vn2',
+            label: 'vn2'
+          }
+        ]);
+      });
+
+      it('should fill instance_name options', function() {
+        expect(scope.filters[3].options).toEqual([
+          {
+            key: 'in1',
+            label: 'in1'
+          },
+          {
+            key: 'in2',
+            label: 'in2'
+          }
+        ]);
+      });
+    });
+
+
+    describe('#filterInstances', function() {
+      beforeEach(function() {
+        spyOn(scope, 'resetPagination');
+      });
+
+      it('all should be filtered when filters not applied', function() {
+        scope.filterInstances();
+        expect(scope.tableInfo.filtered).toEqual(2);
+        scope.filterInstances([]);
+        expect(scope.tableInfo.filtered).toEqual(2);
+      });
+
+      it('resetPagination should be called', function() {
+        scope.filterInstances();
+        expect(scope.resetPagination).toHaveBeenCalled();
+      });
+
+      it('one view should be filtered', function() {
+        var appliedFilters = [
+          {
+            key: 'view_name',
+            values: ['vn1']
+          }
+        ];
+        scope.filterInstances(appliedFilters);
+        expect(scope.tableInfo.filtered).toEqual(1);
+        expect(scope.instances[0].isFiltered).toBeTruthy();
+        expect(scope.instances[1].isFiltered).toBeFalsy();
+      });
+
+      it('two views should be filtered', function() {
+        var appliedFilters = [
+          {
+            key: 'view_name',
+            values: ['vn1', 'vn2']
+          }
+        ];
+        scope.filterInstances(appliedFilters);
+        expect(scope.tableInfo.filtered).toEqual(2);
+        expect(scope.instances[0].isFiltered).toBeTruthy();
+        expect(scope.instances[1].isFiltered).toBeTruthy();
+      });
+
+      it('one views should be filtered with combo filter', function() {
+        var appliedFilters = [
+          {
+            key: 'view_name',
+            values: ['vn1', 'vn2']
+          },
+          {
+            key: 'instance_name',
+            values: ['in2']
+          }
+        ];
+        scope.filterInstances(appliedFilters);
+        expect(scope.tableInfo.filtered).toEqual(1);
+        expect(scope.instances[0].isFiltered).toBeFalsy();
+        expect(scope.instances[1].isFiltered).toBeTruthy();
+      });
+    });
+  });
+});

http://git-wip-us.apache.org/repos/asf/ambari/blob/72657b6f/ambari-admin/src/main/resources/ui/admin-web/test/unit/directives/comboSearch_test.js
----------------------------------------------------------------------
diff --git a/ambari-admin/src/main/resources/ui/admin-web/test/unit/directives/comboSearch_test.js b/ambari-admin/src/main/resources/ui/admin-web/test/unit/directives/comboSearch_test.js
new file mode 100644
index 0000000..9bc7083
--- /dev/null
+++ b/ambari-admin/src/main/resources/ui/admin-web/test/unit/directives/comboSearch_test.js
@@ -0,0 +1,242 @@
+/**
+ * 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.
+ */
+
+describe('#comboSearch', function () {
+  var scope, element;
+
+  beforeEach(module('ambariAdminConsole'));
+  beforeEach(module('views/directives/comboSearch.html'));
+
+  beforeEach(inject(function($rootScope, $compile) {
+    scope = $rootScope.$new();
+
+    var preCompiledElement = '<combo-search suggestions="filters" filter-change="filterItems" placeholder="Search"></combo-search>';
+
+    scope.filters = [
+      {
+        key: 'f1',
+        label: 'filter1',
+        options: []
+      },
+      {
+        key: 'f2',
+        label: 'filter2',
+        options: []
+      }
+    ];
+    scope.filterItems = angular.noop;
+    spyOn(scope, 'filterItems');
+
+
+    element = $compile(preCompiledElement)(scope);
+    scope.$digest();
+  }));
+
+  afterEach(function() {
+    element.remove();
+  });
+
+
+  describe('#removeFilter', function() {
+    it('should remove filter by id', function () {
+      var isoScope = element.isolateScope();
+      isoScope.appliedFilters.push({
+        id: 1
+      });
+      spyOn(isoScope, 'observeSearchFilterInput');
+      spyOn(isoScope, 'updateFilters');
+
+      isoScope.removeFilter({id: 1});
+
+      expect(isoScope.appliedFilters).toEqual([]);
+      expect(isoScope.observeSearchFilterInput).toHaveBeenCalled();
+      expect(isoScope.updateFilters).toHaveBeenCalledWith([]);
+    });
+  });
+
+  describe('#clearFilters', function() {
+    it('should empty appliedFilters', function () {
+      var isoScope = element.isolateScope();
+      isoScope.appliedFilters.push({
+        id: 1
+      });
+      spyOn(isoScope, 'updateFilters');
+
+      isoScope.clearFilters();
+
+      expect(isoScope.appliedFilters).toEqual([]);
+      expect(isoScope.updateFilters).toHaveBeenCalledWith([]);
+    });
+  });
+
+  describe('#selectFilter', function() {
+    it('should add new filter to appliedFilters', function () {
+      var isoScope = element.isolateScope();
+
+      isoScope.selectFilter({
+        key: 'f1',
+        label: 'filter1',
+        options: []
+      });
+
+      expect(isoScope.appliedFilters[0]).toEqual({
+        id: 'filter_1',
+        currentOption: null,
+        filteredOptions: [],
+        searchOptionInput: '',
+        key: 'f1',
+        label: 'filter1',
+        options: [],
+        showAutoComplete: false
+      });
+      expect(isoScope.isEditing).toBeFalsy();
+      expect(isoScope.showAutoComplete).toBeFalsy();
+      expect(isoScope.searchFilterInput).toEqual('');
+    });
+  });
+
+  describe('#selectOption', function() {
+    it('should set value to appliedFilter', function () {
+      var isoScope = element.isolateScope();
+      var filter = {};
+
+      spyOn(isoScope, 'observeSearchFilterInput');
+      spyOn(isoScope, 'updateFilters');
+
+      isoScope.selectOption(null, {
+        key: 'o1',
+        label: 'option1'
+      }, filter);
+
+      expect(filter.currentOption).toEqual({
+        key: 'o1',
+        label: 'option1'
+      });
+      expect(filter.showAutoComplete).toBeFalsy();
+      expect(isoScope.observeSearchFilterInput).toHaveBeenCalled();
+      expect(isoScope.updateFilters).toHaveBeenCalled();
+    });
+  });
+
+  describe('#hideAutocomplete', function() {
+
+    it('showAutoComplete should be false when filter passed', function () {
+      var isoScope = element.isolateScope();
+      var filter = {
+        showAutoComplete: true
+      };
+      jasmine.Clock.useMock();
+
+      isoScope.hideAutocomplete(filter);
+
+      jasmine.Clock.tick(101);
+      expect(filter.showAutoComplete).toBeFalsy();
+    });
+
+    it('showAutoComplete should be false when isEditing = false', function () {
+      var isoScope = element.isolateScope();
+      jasmine.Clock.useMock();
+
+      isoScope.isEditing = false;
+      isoScope.showAutoComplete = true;
+      isoScope.hideAutocomplete();
+
+      jasmine.Clock.tick(101);
+      expect(isoScope.showAutoComplete).toBeFalsy();
+    });
+
+    it('showAutoComplete should be false when isEditing = true', function () {
+      var isoScope = element.isolateScope();
+      jasmine.Clock.useMock();
+
+      isoScope.isEditing = true;
+      isoScope.showAutoComplete = true;
+      isoScope.hideAutocomplete();
+
+      jasmine.Clock.tick(101);
+      expect(isoScope.showAutoComplete).toBeTruthy();
+    });
+  });
+
+  describe('#makeActive', function() {
+    it('category option can not be active', function () {
+      var isoScope = element.isolateScope();
+      var active = {
+        key: 'o1',
+        isCategory: true,
+        active: false
+      };
+
+      isoScope.makeActive(active, [active]);
+
+      expect(active.active).toBeFalsy();
+    });
+
+    it('value option can be active', function () {
+      var isoScope = element.isolateScope();
+      var active = {
+        key: 'o1',
+        isCategory: false,
+        active: false
+      };
+
+      isoScope.makeActive(active, [active]);
+
+      expect(active.active).toBeTruthy();
+    });
+  });
+
+  describe('#updateFilters', function() {
+    it('filter function from parent scope should be called', function () {
+      var isoScope = element.isolateScope();
+      spyOn(isoScope, 'extractFilters').andReturn([{}]);
+
+      isoScope.updateFilters([{}]);
+
+      expect(scope.filterItems).toHaveBeenCalledWith([{}]);
+    });
+  });
+
+  describe('#extractFilters', function() {
+    it('should extract filters', function () {
+      var isoScope = element.isolateScope();
+      var filters = [
+        {
+          currentOption: { key: 'o1'},
+          key: 'f1'
+        },
+        {
+          currentOption: { key: 'o2'},
+          key: 'f1'
+        },
+        {
+          currentOption: null,
+          key: 'f2'
+        }
+      ];
+
+      expect(isoScope.extractFilters(filters)).toEqual([
+        {
+          key: 'f1',
+          values: ['o1', 'o2']
+        }
+      ]);
+    });
+  });
+
+});