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/30d89a33 Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/30d89a33 Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/30d89a33 Branch: refs/heads/branch-feature-AMBARI-21674 Commit: 30d89a3306dddf24fb2206ff22bc85c40458debd Parents: 90554f3 Author: Andrii Tkach <atk...@apache.org> Authored: Tue Dec 19 14:53:08 2017 +0200 Committer: Andrii Tkach <atk...@apache.org> Committed: Tue Dec 19 14:53:08 2017 +0200 ---------------------------------------------------------------------- .../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/30d89a33/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/30d89a33/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/30d89a33/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/30d89a33/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/30d89a33/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/30d89a33/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/30d89a33/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/30d89a33/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">×</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">×</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/30d89a33/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/30d89a33/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/30d89a33/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/30d89a33/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/30d89a33/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'] + } + ]); + }); + }); + +});