Merge pull request #2518 from w33ble/pinned-filters

Pinned filters
This commit is contained in:
Lukas Olson 2015-05-06 16:14:49 -07:00
commit 0eda612c0a
50 changed files with 1941 additions and 747 deletions

View file

@ -300,7 +300,7 @@ define(function (require) {
* @returns {object}
*/
var cleanFilter = function (filter) {
return _.omit(filter, ['$$hashKey', 'meta']);
return _.omit(filter, ['meta']);
};
// switch to filtered query if there are filters

View file

@ -1,9 +1,9 @@
define(function (require) {
return function CourierFetchRequestStatus() {
return {
ABORTED: {},
DUPLICATE: {},
INCOMPLETE: {}
ABORTED: { CourierFetchRequestStatus: 'aborted' },
DUPLICATE: { CourierFetchRequestStatus: 'duplicate' },
INCOMPLETE: { CourierFetchRequestStatus: 'incomplete' }
};
};
});

View file

@ -241,6 +241,7 @@ define(function (require) {
return self.id;
});
};
return docSource.doCreate(source)
.then(finish)
.catch(function (err) {

View file

@ -12,21 +12,25 @@
<div class="bar" ng-show="filters.length">
<div class="filter" ng-class="{ negate: filter.meta.negate, disabled: filter.meta.disabled }" ng-repeat="filter in filters">
<div class="filter-description">
<!--<span><i class="fa" ng-class="{'fa-plus': !filter.meta.negate, 'fa-minus': filter.meta.negate}"></i></span>-->
<span ng-if="filter.$state.store == 'globalState'"><i class="fa fa-fw fa-thumb-tack pinned"></i></span>
<span>{{ filter.meta.key }}:</span>
<span>"{{ filter.meta.value }}"</span>
</div>
<div class="filter-actions">
<a class="action filter-toggle" ng-click="toggleFilter(filter)">
<i ng-show="filter.meta.disabled" class="fa fa-square-o"></i>
<i ng-hide="filter.meta.disabled" class="fa fa-check-square-o"></i>
<i ng-show="filter.meta.disabled" class="fa fa-fw fa-square-o disabled"></i>
<i ng-hide="filter.meta.disabled" class="fa fa-fw fa-check-square-o enabled"></i>
</a>
<a class="action filter-pin" ng-click="pinFilter(filter)">
<i ng-show="filter.$state.store == 'globalState'" class="fa fa-fw fa-thumb-tack pinned"></i>
<i ng-hide="filter.$state.store == 'globalState'" class="fa fa-fw fa-thumb-tack fa-rotate-270 unpinned"></i>
</a>
<a class="action filter-invert" ng-click="invertFilter(filter)">
<i ng-show="filter.meta.negate" class="fa fa-search-plus"></i>
<i ng-hide="filter.meta.negate" class="fa fa-search-minus"></i>
<i ng-show="filter.meta.negate" class="fa fa-fw fa-search-plus negative"></i>
<i ng-hide="filter.meta.negate" class="fa fa-fw fa-search-minus positive"></i>
</a>
<a class="action filter-remove" ng-click="removeFilter(filter)">
<i class="fa fa-trash"></i>
<i class="fa fa-fw fa-trash"></i>
</a>
</div>
</div>
@ -52,6 +56,12 @@
<div class="filter-link">
<div class="filter-description"><a ng-click="toggleAll(true)">Disable</a></div>
</div>
<div class="filter-link">
<div class="filter-description"><a ng-click="pinAll(true)">Pin</a></div>
</div>
<div class="filter-link">
<div class="filter-description"><a ng-click="pinAll(false)">Unpin</a></div>
</div>
<div class="filter-link">
<div class="filter-description"><a ng-click="invertAll()">Invert</a></div>
</div>

View file

@ -4,34 +4,43 @@ define(function (require) {
var template = require('text!components/filter_bar/filter_bar.html');
var moment = require('moment');
var toggleFilter = require('components/filter_bar/lib/toggleFilter');
var toggleAll = require('components/filter_bar/lib/toggleAll');
var invertFilter = require('components/filter_bar/lib/invertFilter');
var invertAll = require('components/filter_bar/lib/invertAll');
var removeFilter = require('components/filter_bar/lib/removeFilter');
var removeAll = require('components/filter_bar/lib/removeAll');
var filterAppliedAndUnwrap = require('components/filter_bar/lib/filterAppliedAndUnwrap');
module.directive('filterBar', function (Private, Promise) {
module.directive('filterBar', function (Private, Promise, getAppState) {
var mapAndFlattenFilters = Private(require('components/filter_bar/lib/mapAndFlattenFilters'));
var mapFlattenAndWrapFilters = Private(require('components/filter_bar/lib/mapFlattenAndWrapFilters'));
var extractTimeFilter = Private(require('components/filter_bar/lib/extractTimeFilter'));
var filterOutTimeBasedFilter = Private(require('components/filter_bar/lib/filterOutTimeBasedFilter'));
var filterAppliedAndUnwrap = require('components/filter_bar/lib/filterAppliedAndUnwrap');
var changeTimeFilter = Private(require('components/filter_bar/lib/changeTimeFilter'));
var queryFilter = Private(require('components/filter_bar/query_filter'));
return {
restrict: 'E',
template: template,
scope: {
state: '='
},
scope: {},
link: function ($scope, $el, attrs) {
// bind query filter actions to the scope
[
'addFilters',
'toggleFilter',
'toggleAll',
'pinFilter',
'pinAll',
'invertFilter',
'invertAll',
'removeFilter',
'removeAll'
].forEach(function (method) {
$scope[method] = queryFilter[method];
});
$scope.state = getAppState();
$scope.applyFilters = function (filters) {
var newFilters = filterAppliedAndUnwrap(filters);
$scope.state.filters = _.union($scope.state.filters, newFilters);
// add new filters
$scope.addFilters(filterAppliedAndUnwrap(filters));
$scope.newFilters = [];
// change time filter
if ($scope.changeTimeFilter && $scope.changeTimeFilter.meta && $scope.changeTimeFilter.meta.apply) {
changeTimeFilter($scope.changeTimeFilter);
}
@ -42,10 +51,20 @@ define(function (require) {
$scope.changeTimeFilter = null;
};
// update the scope filter list on filter changes
$scope.$listen(queryFilter, 'update', function () {
updateFilters();
});
// when appState changes, update scope's state
$scope.$watch(getAppState, function (appState) {
$scope.state = appState;
});
$scope.$watch('state.$newFilters', function (filters) {
if (!filters) return;
// If the filters is not undefined and the length is greater then
// If filters is not undefined and the length is greater than
// one we need to set the newFilters attribute and allow the
// users to decide what they want to apply.
if (filters.length > 1) {
@ -72,24 +91,22 @@ define(function (require) {
return filters;
})
.then(filterOutTimeBasedFilter)
.then(function (filters) {
$scope.state.filters = _.union($scope.state.filters, filters);
});
.then($scope.addFilters);
}
});
$scope.$watch('state.filters', function (filters) {
function updateFilters() {
var filters = queryFilter.getFilters();
mapAndFlattenFilters(filters).then(function (results) {
$scope.filters = results;
// used to display the current filters in the state
$scope.filters = _.sortBy(results, function (filter) {
return !filter.meta.pinned;
});
$scope.$emit('filterbar:updated');
});
});
}
$scope.toggleFilter = toggleFilter($scope);
$scope.toggleAll = toggleAll($scope);
$scope.invertFilter = invertFilter($scope);
$scope.invertAll = invertAll($scope);
$scope.removeFilter = removeFilter($scope);
$scope.removeAll = removeAll($scope);
updateFilters();
}
};
});

View file

@ -99,8 +99,8 @@ filter-bar .bar {
}
> .filter-actions {
font-size: 1.3em;
line-height: 1.1em;
font-size: 1.1em;
line-height: 1.4em;
position: absolute;
padding: 4px 8px;
top: 0;
@ -109,18 +109,22 @@ filter-bar .bar {
display: none;
text-align: center;
white-space: nowrap;
}
> .filter-actions > .action {
border-right: 1px solid rgba(255, 255, 255, 0.4);
padding-right: 6px;
margin-right: 8px;
}
> * {
border-right: 1px solid rgba(255, 255, 255, 0.4);
padding-right: 0;
margin-right: 5px;
> .filter-actions > .action:last-child {
border-right: 0;
padding-right: 0;
margin-right: 0;
&:last-child {
border-right: 0;
padding-right: 0;
margin-right: 0;
}
.unpinned {
.opacity(.7);
}
}
}
&.negate {

View file

@ -29,8 +29,7 @@ define(function (require) {
if (!filters.length) return;
filters = uniqFilters(filters);
filters = dedupFilters($state.filters, filters);
filters = dedupFilters($state.filters, uniqFilters(filters));
// We need to add a bunch of filter deduping here.
$state.$newFilters = filters;
}

View file

@ -0,0 +1,33 @@
define(function (require) {
var _ = require('lodash');
var angular = require('angular');
var excludedAttributes;
var comparators;
/**
* Compare two filters to see if they match
* @param {object} first The first filter to compare
* @param {object} second The second filter to compare
* @param {object} comparatorOptions Parameters to use for comparison
* @returns {bool} Filters are the same
*/
return function (first, second, comparatorOptions) {
excludedAttributes = ['$$hashKey', 'meta'];
comparators = _.defaults(comparatorOptions || {}, {
state: false,
negate: false,
disabled: false,
});
if (!comparators.state) excludedAttributes.push('$state');
return _.isEqual(mapFilter(first), mapFilter(second));
};
function mapFilter(filter) {
var cleaned = _.omit(filter, excludedAttributes);
if (comparators.negate) cleaned.negate = filter.meta && !!filter.meta.negate;
if (comparators.disabled) cleaned.disabled = filter.meta && !!filter.meta.disabled;
return cleaned;
}
});

View file

@ -1,12 +1,22 @@
define(function (require) {
var _ = require('lodash');
var excludedAttributes = ['meta', '$$hashKey'];
return function (existing, filters) {
filters = _.filter(filters, function (item) {
return !_.find(existing, function (existingFilter) {
return _.isEqual(_.omit(existingFilter, excludedAttributes), _.omit(item, excludedAttributes));
var angular = require('angular');
var compareFilters = require('components/filter_bar/lib/compareFilters');
/**
* Combine 2 filter collections, removing duplicates
* @param {object} existing The filters to compare to
* @param {object} filters The filters being added
* @param {object} comparatorOptions Parameters to use for comparison
* @returns {object} An array of filters that were not in existing
*/
return function (existingFilters, filters, comparatorOptions) {
if (!_.isArray(filters)) filters = [filters];
return _.filter(filters, function (filter) {
return !_.find(existingFilters, function (existingFilter) {
return compareFilters(existingFilter, filter, comparatorOptions);
});
});
return filters;
};
});

View file

@ -1,17 +0,0 @@
define(function (require) {
return function ($scope) {
var invertFilter = require('components/filter_bar/lib/invertFilter')($scope);
/**
* Removes all filters
* @returns {void}
*/
return function () {
$scope.filters.forEach(function (filter) {
invertFilter(filter);
});
$scope.state.filters = $scope.filters;
};
};
});

View file

@ -1,19 +0,0 @@
define(function (require) {
var _ = require('lodash');
return function ($scope) {
/**
* Inverts the nagate value on the filter
* @param {object} filter The filter to toggle
& @param {boolean} force disabled true/false
* @returns {void}
*/
return function (filter) {
// Toggle the negate meta state
filter.meta.negate = !filter.meta.negate;
// Save the filters back to the searchSource
$scope.state.filters = $scope.filters;
return filter;
};
};
});

View file

@ -5,8 +5,13 @@ define(function (require) {
return _.deepGet(filter, 'meta.disabled');
};
return function (newFitlers, oldFilters) {
var diff = _.difference(oldFilters, newFitlers);
return (diff.length && _.every(diff, pluckDisabled));
/**
* Checks to see if only disabled filters have been changed
* @returns {bool} Only disabled filters
*/
return function (newFilters, oldFilters) {
return _.every(newFilters.concat(oldFilters), function (newFilter) {
return pluckDisabled(newFilter);
});
};
});

View file

@ -0,0 +1,19 @@
define(function (require) {
var _ = require('lodash');
var makeComparable = function (filter) {
return _.omit(filter, ['$state', '$$hashKey']);
};
/**
* Checks to see if only disabled filters have been changed
* @returns {bool} Only disabled filters
*/
return function (newFilters, oldFilters) {
var comparableOldFilters = _.map(oldFilters, makeComparable);
return _.every(newFilters, function (newFilter, i) {
var match = _.find(comparableOldFilters, makeComparable(newFilter));
return !!match;
});
};
});

View file

@ -1,11 +0,0 @@
define(function (require) {
return function ($scope) {
/**
* Removes all filters
* @returns {void}
*/
return function () {
$scope.state.filters = [];
};
};
});

View file

@ -1,20 +0,0 @@
define(function (require) {
var _ = require('lodash');
return function ($scope) {
/**
* Removes the filter from the searchSource
* @param {object} filter The filter to remove
* @returns {void}
*/
return function (invalidFilter) {
// Remove the filter from the the scope $filters and map it back
// to the original format to save in searchSource
$scope.state.filters = _($scope.filters)
.filter(function (filter) {
return filter !== invalidFilter;
})
.value();
};
};
});

View file

@ -1,19 +0,0 @@
define(function (require) {
var _ = require('lodash');
return function ($scope) {
var toggleFilter = require('components/filter_bar/lib/toggleFilter')($scope);
/**
* Disables all filters
* @params {boolean} force disable/enable all filters
* @returns {void}
*/
return function (force) {
$scope.filters.forEach(function (filter) {
toggleFilter(filter, force);
});
$scope.state.filters = $scope.filters;
};
};
});

View file

@ -1,20 +0,0 @@
define(function (require) {
var _ = require('lodash');
return function ($scope) {
/**
* Toggles the filter between enabled/disabled.
* @param {object} filter The filter to toggle
& @param {boolean} force disabled true/false
* @returns {void}
*/
return function (filter, force) {
// Toggle the disabled flag
var disabled = _.isUndefined(force) ? !filter.meta.disabled : force;
filter.meta.disabled = disabled;
// Save the filters back to the searchSource
$scope.state.filters = $scope.filters;
return filter;
};
};
});

View file

@ -1,10 +1,16 @@
define(function (require) {
var _ = require('lodash');
var dedupFilters = require('components/filter_bar/lib/dedupFilters');
return function (filters) {
/**
* Remove duplicate filters from an array of filters
* @param {array} filters The filters to remove duplicates from
* @returns {object} The original filters array with duplicates removed
*/
return function (filters, comparatorOptions) {
var results = [];
_.each(filters, function (filter) {
results = _.union(results, dedupFilters(results, [filter]));
results = _.union(results, dedupFilters(results, [filter], comparatorOptions));
});
return results;
};

View file

@ -1,24 +0,0 @@
define(function (require) {
return function watchFiltersProvider(Private, Promise, Notifier) {
var _ = require('lodash');
var onlyDisabled = require('components/filter_bar/lib/onlyDisabled');
var EventEmitter = Private(require('factories/events'));
var notify = new Notifier({ location: 'Fitler Bar' });
return function ($scope, handlers) {
var emitter = new EventEmitter();
$scope.$watch('state.filters', function (newFilters, oldFilters) {
if (newFilters === oldFilters) return;
return emitter.emit('update')
.then(function () {
if (onlyDisabled(newFilters, oldFilters)) return;
return emitter.emit('fetch');
});
});
return emitter;
};
};
});

View file

@ -0,0 +1,329 @@
define(function (require) {
var _ = require('lodash');
return function (Private, $rootScope, getAppState, globalState) {
var EventEmitter = Private(require('factories/events'));
var onlyDisabled = require('components/filter_bar/lib/onlyDisabled');
var onlyStateChanged = require('components/filter_bar/lib/onlyStateChanged');
var uniqFilters = require('components/filter_bar/lib/uniqFilters');
var compareFilters = require('components/filter_bar/lib/compareFilters');
var queryFilter = new EventEmitter();
queryFilter.getFilters = function () {
var compareOptions = { disabled: true, negate: true };
var appFilters = queryFilter.getAppFilters();
var globalFilters = queryFilter.getGlobalFilters();
return uniqFilters(globalFilters.concat(appFilters), compareOptions);
};
queryFilter.getAppFilters = function () {
var appState = getAppState();
if (!appState || !appState.filters) return [];
return (appState.filters) ? _.map(appState.filters, appendStoreType('appState')) : [];
};
queryFilter.getGlobalFilters = function () {
if (!globalState.filters) return [];
return _.map(globalState.filters, appendStoreType('globalState'));
};
/**
* Adds new filters to the scope and state
* @param {object|array} fitlers Filter(s) to add
* @param {bool} global Should be added to global state
* @returns {object} Resulting new filter list
*/
queryFilter.addFilters = function (filters, global) {
var appState = getAppState();
var state = (global) ? globalState : appState;
if (!_.isArray(filters)) {
filters = [filters];
}
if (global) {
// simply concat global filters, they will be deduped
globalState.filters = globalState.filters.concat(filters);
} else if (appState) {
if (!appState.filters) appState.filters = [];
var mergeOptions = { disabled: true, negate: false };
var appFilters = appState.filters.concat(filters);
var merged = mergeAndMutateFilters(globalState.filters, appFilters, mergeOptions);
globalState.filters = merged[0];
appState.filters = merged[1];
}
return saveState();
};
/**
* Removes the filter from the proper state
* @param {object} matchFilter The filter to remove
* @returns {object} Resulting new filter list
*/
queryFilter.removeFilter = function (matchFilter) {
var state = getStateByFilter(matchFilter);
if (!state) return;
_.pull(state.filters, matchFilter);
return saveState();
};
/**
* Removes all filters
* @returns {object} Resulting new filter list
*/
queryFilter.removeAll = function () {
var appState = getAppState();
appState.filters = [];
globalState.filters = [];
return saveState();
};
/**
* Toggles the filter between enabled/disabled.
* @param {object} filter The filter to toggle
& @param {boolean} force Disabled true/false
* @returns {object} updated filter
*/
queryFilter.toggleFilter = function (filter, force) {
// Toggle the disabled flag
var disabled = _.isUndefined(force) ? !filter.meta.disabled : !!force;
filter.meta.disabled = disabled;
// Save the filters back to the searchSource
saveState();
return filter;
};
/**
* Disables all filters
* @params {boolean} force Disable/enable all filters
* @returns {object} Resulting updated filter list
*/
queryFilter.toggleAll = function (force) {
function doToggle(filter) {
queryFilter.toggleFilter(filter, force);
}
executeOnFilters(doToggle);
return queryFilter.getFilters();
};
/**
* Inverts the nagate value on the filter
* @param {object} filter The filter to toggle
* @returns {object} updated filter
*/
queryFilter.invertFilter = function (filter) {
// Toggle the negate meta state
filter.meta.negate = !filter.meta.negate;
saveState();
return filter;
};
/**
* Inverts all filters
* @returns {object} Resulting updated filter list
*/
queryFilter.invertAll = function () {
executeOnFilters(queryFilter.invertFilter);
return queryFilter.getFilters();
};
/**
* Pins the filter to the global state
* @param {object} filter The filter to pin
* @param {boolean} force pinned state
* @returns {object} filter passed in
*/
queryFilter.pinFilter = function (filter, force) {
var appState = getAppState();
if (!appState) return filter;
// ensure that both states have a filters property
if (!_.isArray(globalState.filters)) globalState.filters = [];
if (!_.isArray(appState.filters)) appState.filters = [];
var appIndex = _.indexOf(appState.filters, filter);
var globalIndex = _.indexOf(globalState.filters, filter);
if (appIndex === -1 && globalIndex === -1) return;
if (appIndex !== -1 && force !== false) {
appState.filters.splice(appIndex, 1);
globalState.filters.push(filter);
} else if (globalIndex !== -1 && force !== true) {
globalState.filters.splice(globalIndex, 1);
appState.filters.push(filter);
}
saveState();
return filter;
};
/**
* Pins all filters
* @params {boolean} force Pin/Unpin all filters
* @returns {object} Resulting updated filter list
*/
queryFilter.pinAll = function (force) {
function pin(filter) {
queryFilter.pinFilter(filter, force);
}
executeOnFilters(pin);
return queryFilter.getFilters();
};
initWatchers();
return queryFilter;
/**
* Saves both app and global states, ensuring filters are persisted
* @returns {object} Resulting filter list, app and global combined
*/
function saveState() {
var appState = getAppState();
if (appState) appState.save();
globalState.save();
return queryFilter.getFilters();
}
function appendStoreType(type) {
return function (filter) {
filter.$state = {
store: type
};
return filter;
};
}
// get state (app or global) or the filter passed in
function getStateByFilter(filter) {
var appState = getAppState();
if (appState) {
var appIndex = _.indexOf(appState.filters, filter);
if (appIndex !== -1) return appState;
}
var globalIndex = _.indexOf(globalState.filters, filter);
if (globalIndex !== -1) return globalState;
return false;
}
// helper to run a function on all filters in all states
function executeOnFilters(fn) {
var appState = getAppState();
var appFilters;
if (appState && appState.filters) {
appFilters = appState.filters;
} else {
appFilters = [];
}
globalState.filters.concat(appFilters).forEach(fn);
}
function mergeAndMutateFilters(globalFilters, appFilters, compareOptions) {
appFilters = appFilters || [];
globalFilters = globalFilters || [];
compareOptions = _.defaults(compareOptions || {}, { disabled: true, negate: true });
// existing globalFilters should be mutated by appFilters
appFilters = _.filter(appFilters, function (filter) {
var match = _.find(globalFilters, function (globalFilter) {
return compareFilters(globalFilter, filter, compareOptions);
});
// if the filter remains, it doesn't match any filters in global state
if (!match) return true;
// filter matches a filter in globalFilters, mutate existing global filter
_.assign(match.meta, filter.meta);
return false;
});
appFilters = uniqFilters(appFilters, { disabled: true });
globalFilters = uniqFilters(globalFilters, { disabled: true });
return [globalFilters, appFilters];
}
/**
* Initializes state watchers that use the event emitter
* @returns {void}
*/
function initWatchers() {
var removeAppStateWatchers;
$rootScope.$watch(getAppState, function () {
removeAppStateWatchers && removeAppStateWatchers();
removeAppStateWatchers = initAppStateWatchers();
});
function initAppStateWatchers() {
// multi watch on the app and global states
var stateWatchers = [{
fn: $rootScope.$watch,
deep: true,
get: queryFilter.getGlobalFilters
}, {
fn: $rootScope.$watch,
deep: true,
get: queryFilter.getAppFilters
}];
// when states change, use event emitter to trigger updates and fetches
return $rootScope.$watchMulti(stateWatchers, function (next, prev) {
var doUpdate = false;
var doFetch = false;
var newFilters = [];
var oldFilters = [];
// iterate over each state type, checking for changes
stateWatchers.forEach(function (watcher, i) {
var nextVal = next[i];
var prevVal = prev[i];
newFilters = newFilters.concat(nextVal);
oldFilters = oldFilters.concat(prevVal);
// no update or fetch if there was no change
if (nextVal === prevVal) return;
if (nextVal) doUpdate = true;
// don't trigger fetch when only disabled filters
if (!onlyDisabled(nextVal, prevVal)) doFetch = true;
});
// make sure change wasn't only a state move
if (doFetch && newFilters.length === oldFilters.length) {
if (onlyStateChanged(newFilters, oldFilters)) doFetch = false;
}
// reconcile filter in global and app states
var filters = mergeAndMutateFilters(next[0], next[1]);
globalState.filters = filters[0];
var appState = getAppState();
if (appState) {
appState.filters = filters[1];
}
saveState();
if (!doUpdate) return;
return queryFilter.emit('update')
.then(function () {
if (!doFetch) return;
return queryFilter.emit('fetch');
});
});
}
}
};
});

View file

@ -1,73 +1,79 @@
// Adds a filter to a passed state
define(function (require) {
var _ = require('lodash');
var self = this;
return function (Private) {
var _ = require('lodash');
var queryFilter = Private(require('components/filter_bar/query_filter'));
var filterManager = {};
this.init = function ($state) {
self.$state = $state;
};
filterManager.add = function (field, values, operation, index) {
values = _.isArray(values) ? values : [values];
var fieldName = _.isObject(field) ? field.name : field;
var filters = _.flatten([queryFilter.getAppFilters()], true);
var newFilters = [];
this.add = function (field, values, operation, index) {
var negate = (operation === '-');
values = _.isArray(values) ? values : [values];
// Have we been passed a simple name or an actual field object?
var fieldName = _.isObject(field) ? field.name : field;
var negate = operation === '-';
var filters = _.flatten([self.$state.filters], true);
// TODO: On array fields, negating does not negate the combination, rather all terms
_.each(values, function (value) {
var existing = _.find(filters, function (filter) {
if (!filter) return;
if (fieldName === '_exists_' && filter.exists) {
return filter.exists.field === value;
}
if (filter.query) {
return filter.query.match[fieldName] && filter.query.match[fieldName].query === value;
}
});
if (existing) {
if (existing.meta.negate !== negate) {
existing.meta.negate = negate;
}
return;
}
switch (fieldName) {
case '_exists_':
filters.push({ meta: { negate: negate, index: index }, exists: { field: value } });
break;
default:
// TODO: On array fields, negating does not negate the combination, rather all terms
_.each(values, function (value) {
var filter;
if (field.scripted) {
var existing = _.find(filters, function (filter) {
if (!filter) return;
if (fieldName === '_exists_' && filter.exists) {
return filter.exists.field === value;
}
if (filter.query) {
return filter.query.match[fieldName] && filter.query.match[fieldName].query === value;
}
});
if (existing) {
existing.meta.disabled = false;
if (existing.meta.negate !== negate) {
queryFilter.invertFilter(existing);
}
return;
}
switch (fieldName) {
case '_exists_':
filter = {
meta: { negate: negate, index: index, field: fieldName },
script: {
script: '(' + field.script + ') == value',
lang: field.lang,
params: {
value: value
}
meta: {
negate: negate,
index: index
},
exists: {
field: value
}
};
} else {
filter = { meta: { negate: negate, index: index }, query: { match: {} } };
filter.query.match[fieldName] = { query: value, type: 'phrase' };
break;
default:
if (field.scripted) {
filter = {
meta: { negate: negate, index: index, field: fieldName },
script: {
script: '(' + field.script + ') == value',
lang: field.lang,
params: {
value: value
}
}
};
} else {
filter = { meta: { negate: negate, index: index }, query: { match: {} } };
filter.query.match[fieldName] = { query: value, type: 'phrase' };
}
break;
}
filters.push(filter);
break;
}
});
newFilters.push(filter);
});
self.$state.filters = filters;
queryFilter.addFilters(newFilters);
};
return filterManager;
};
return this;
});

View file

@ -58,8 +58,7 @@ define(function (require) {
State.prototype.fetch = function () {
var stash = this._readFromURL();
// nothing to read from the url?
// we should save if were are ordered to persist
// nothing to read from the url? save if ordered to persist
if (stash === null) {
if (this._persistAcrossApps) {
return this.save();

View file

@ -41,6 +41,8 @@ define(function (require) {
var vals = new Array(expressions.length);
var prev = new Array(expressions.length);
var fire = false;
var init = 0;
var neededInits = expressions.length;
// first, register all of the multi-watchers
var unwatchers = expressions.map(function (expr, i) {
@ -48,6 +50,10 @@ define(function (require) {
if (!expr) return;
return expr.fn.call($scope, expr.get, function (newVal, oldVal) {
if (newVal === oldVal) {
init += 1;
}
vals[i] = newVal;
prev[i] = oldVal;
fire = true;
@ -58,12 +64,16 @@ define(function (require) {
// the other watchers triggered this cycle
var flip = false;
unwatchers.push($scope.$watch(function () {
if (init < neededInits) return init;
if (fire) {
fire = false;
flip = !flip;
}
return flip;
}, function () {
if (init < neededInits) return false;
fn(vals.slice(0), prev.slice(0));
vals.forEach(function (v, i) {
prev[i] = v;
@ -104,4 +114,4 @@ define(function (require) {
return $delegate;
});
});
});
});

View file

@ -1,12 +1,12 @@
define(function (require) {
var moment = require('moment');
var filterManager = require('components/filter_manager/filter_manager');
var $ = require('jquery');
require('modules')
.get('app/dashboard')
.directive('dashboardPanel', function (savedVisualizations, savedSearches, Notifier, Private, $compile) {
var _ = require('lodash');
var loadPanel = Private(require('plugins/dashboard/components/panel/lib/load_panel'));
var filterManager = Private(require('components/filter_manager/filter_manager'));
var notify = new Notifier();
require('components/visualize/visualize');
@ -32,7 +32,6 @@ define(function (require) {
$scope.edit = panelConfig.edit;
$scope.$on('$destroy', panelConfig.savedObj.destroy);
filterManager.init($state);
$scope.filter = function (field, value, operator) {
var index = $scope.savedObj.searchSource.get('index').id;
filterManager.add(field, value, operator, index);

View file

@ -50,7 +50,7 @@ define(function (require) {
app.directive('dashboardApp', function (Notifier, courier, AppState, timefilter, kbnUrl) {
return {
controller: function ($scope, $route, $routeParams, $location, configFile, Private, getAppState) {
var filterBarWatchFilters = Private(require('components/filter_bar/lib/watchFilters'));
var queryFilter = Private(require('components/filter_bar/query_filter'));
var notify = new Notifier({
location: 'Dashboard'
@ -110,22 +110,24 @@ define(function (require) {
}
function updateQueryOnRootSource() {
var filters = $state.filters;
var filters = queryFilter.getFilters();
if ($state.query) {
dash.searchSource.set('filter', _.union($state.filters, [{
query: $state.query
dash.searchSource.set('filter', _.union(filters, [{
query: $state.query
}]));
} else {
dash.searchSource.set('filter', filters);
}
}
filterBarWatchFilters($scope)
.on('update', function () {
// update root source when filters update
$scope.$listen(queryFilter, 'update', function () {
updateQueryOnRootSource();
$state.save();
})
.on('fetch', $scope.refresh);
});
// update data when filters fire fetch event
$scope.$listen(queryFilter, 'fetch', $scope.refresh);
$scope.newDashboard = function () {
kbnUrl.change('/dashboard', {});

View file

@ -3,7 +3,6 @@ define(function (require) {
var angular = require('angular');
var moment = require('moment');
var ConfigTemplate = require('utils/config_template');
var filterManager = require('components/filter_manager/filter_manager');
var getSort = require('components/doc_table/lib/get_sort');
var rison = require('utils/rison');
@ -69,7 +68,8 @@ define(function (require) {
var docTitle = Private(require('components/doc_title/doc_title'));
var brushEvent = Private(require('utils/brush_event'));
var HitSortFn = Private(require('plugins/discover/_hit_sort_fn'));
var filterBarWatchFilters = Private(require('components/filter_bar/lib/watchFilters'));
var queryFilter = Private(require('components/filter_bar/query_filter'));
var filterManager = Private(require('components/filter_manager/filter_manager'));
var notify = new Notifier({
location: 'Discover'
@ -115,7 +115,7 @@ define(function (require) {
columns: savedSearch.columns || ['_source'],
index: $scope.indexPattern.id,
interval: 'auto',
filters: _.cloneDeep($scope.searchSource.get('filter'))
filters: _.cloneDeep($scope.searchSource.getOwn('filter'))
};
}
@ -127,7 +127,6 @@ define(function (require) {
});
var metaFields = config.get('metaFields');
filterManager.init($state);
$scope.opts = {
// number of records to fetch, then paginate through
@ -165,13 +164,15 @@ define(function (require) {
if (!angular.equals(sort, currentSort)) $scope.fetch();
});
filterBarWatchFilters($scope)
.on('update', function () {
// update data source when filters update
$scope.$listen(queryFilter, 'update', function () {
return $scope.updateDataSource().then(function () {
$state.save();
});
})
.on('fetch', $scope.fetch);
});
// fetch data when filters fire fetch event
$scope.$listen(queryFilter, 'fetch', $scope.fetch);
$scope.$watch('opts.timefield', function (timefield) {
timefilter.enabled = !!timefield;
@ -424,7 +425,7 @@ define(function (require) {
fields: {'*': {}},
fragment_size: 2147483647 // Limit of an integer.
})
.set('filter', $state.filters || []);
.set('filter', queryFilter.getFilters());
});
// TODO: On array fields, negating does not negate the combination, rather all terms

View file

@ -58,7 +58,7 @@ define(function (require) {
var Notifier = require('components/notify/_notifier');
var docTitle = Private(require('components/doc_title/doc_title'));
var brushEvent = Private(require('utils/brush_event'));
var filterBarWatchFilters = Private(require('components/filter_bar/lib/watchFilters'));
var queryFilter = Private(require('components/filter_bar/query_filter'));
var filterBarClickHandler = Private(require('components/filter_bar/filter_bar_click_handler'));
var notify = new Notifier({
@ -152,19 +152,16 @@ define(function (require) {
timefilter.enabled = !!timeField;
});
filterBarWatchFilters($scope)
.on('update', function () {
if ($state.filters && $state.filters.length) {
searchSource.set('filter', $state.filters);
} else {
searchSource.set('filter', []);
}
// update the searchSource when filters update
$scope.$listen(queryFilter, 'update', function () {
searchSource.set('filter', queryFilter.getFilters());
$state.save();
})
.on('fetch', function () {
$scope.fetch();
});
// fetch data when filters fire fetch event
$scope.$listen(queryFilter, 'fetch', $scope.fetch);
$scope.$listen($state, 'fetch_with_changes', function (keys) {
if (_.contains(keys, 'linked') && $state.linked === true) {
// abort and reload route
@ -187,7 +184,7 @@ define(function (require) {
}
if (_.isEqual(keys, ['filters'])) {
// updates will happen in filterBarWatchFilters() if needed
// updates will happen in filter watcher if needed
return;
}
@ -210,7 +207,7 @@ define(function (require) {
$scope.fetch = function () {
$state.save();
searchSource.set('filter', $state.filters);
searchSource.set('filter', queryFilter.getFilters());
if (!$state.linked) searchSource.set('query', $state.query);
if ($scope.vis.type.requiresSearch) {
courier.fetch();

View file

@ -5,6 +5,7 @@ define(function (require) {
{ name: 'ssl', type: 'boolean', indexed: true, analyzed: true, sortable: true, filterable: true, count: 20 },
{ name: '@timestamp', type: 'date', indexed: true, analyzed: true, sortable: true, filterable: true, count: 30 },
{ name: 'time', type: 'date', indexed: true, analyzed: true, sortable: true, filterable: true, count: 30 },
{ name: '@tags', type: 'string', indexed: true, analyzed: true, sortable: true, filterable: true },
{ name: 'utc_time', type: 'date', indexed: true, analyzed: true, sortable: true, filterable: true },
{ name: 'phpmemory', type: 'number', indexed: true, analyzed: true, sortable: true, filterable: true },
{ name: 'ip', type: 'ip', indexed: true, analyzed: true, sortable: true, filterable: true },

View file

@ -0,0 +1,18 @@
define(function (require) {
var _ = require('lodash');
var sinon = require('test_utils/auto_release_sinon');
function MockState(defaults) {
this.on = _.noop;
this.off = _.noop;
this.save = sinon.stub();
_.assign(this, defaults);
}
MockState.prototype.resetStub = function () {
this.save = sinon.stub();
return this;
};
return MockState;
});

View file

@ -147,6 +147,7 @@ define(function (require) {
var section = getSections($elem);
$scope.columns.push('bytes');
$scope.$digest();
expect(section.selected.text()).to.contain('bytes');
expect(section.popular.text()).to.not.contain('bytes');

View file

@ -0,0 +1,130 @@
define(function (require) {
return ['add filters', function () {
var _ = require('lodash');
var sinon = require('test_utils/auto_release_sinon');
var MockState = require('fixtures/mock_state');
var storeNames = {
app: 'appState',
global: 'globalState'
};
var filters;
var queryFilter;
var $rootScope, appState, globalState;
beforeEach(module('kibana'));
beforeEach(function () {
appState = new MockState({ filters: [] });
globalState = new MockState({ filters: [] });
filters = [
{
query: { match: { extension: { query: 'jpg', type: 'phrase' } } },
meta: { negate: false, disabled: false }
},
{
query: { match: { '@tags': { query: 'info', type: 'phrase' } } },
meta: { negate: false, disabled: false }
},
{
query: { match: { '_type': { query: 'nginx', type: 'phrase' } } },
meta: { negate: false, disabled: false }
}
];
});
beforeEach(function () {
module('kibana/global_state', function ($provide) {
$provide.service('getAppState', function () {
return function () {
return appState;
};
});
$provide.service('globalState', function () {
return globalState;
});
});
});
beforeEach(function () {
inject(function (_$rootScope_, Private) {
$rootScope = _$rootScope_;
queryFilter = Private(require('components/filter_bar/query_filter'));
});
});
describe('adding filters', function () {
it('should add filters to appState', function () {
queryFilter.addFilters(filters);
expect(appState.filters.length).to.be(3);
expect(globalState.filters.length).to.be(0);
});
it('should add filters to globalState', function () {
queryFilter.addFilters(filters, true);
expect(appState.filters.length).to.be(0);
expect(globalState.filters.length).to.be(3);
});
it('should accept a single filter', function () {
queryFilter.addFilters(filters[0]);
expect(appState.filters.length).to.be(1);
expect(globalState.filters.length).to.be(0);
});
it('should fire the update and fetch events', function () {
var emitSpy = sinon.spy(queryFilter, 'emit');
// set up the watchers
$rootScope.$digest();
queryFilter.addFilters(filters);
// trigger the digest loop to fire the watchers
$rootScope.$digest();
expect(emitSpy.callCount).to.be(2);
expect(emitSpy.firstCall.args[0]).to.be('update');
expect(emitSpy.secondCall.args[0]).to.be('fetch');
});
});
describe('filter reconciliation', function () {
it('should de-dupe appState filters being added', function () {
var newFilter = _.cloneDeep(filters[1]);
appState.filters = filters;
expect(appState.filters.length).to.be(3);
queryFilter.addFilters(newFilter);
$rootScope.$digest();
expect(appState.filters.length).to.be(3);
});
it('should de-dupe globalState filters being added', function () {
var newFilter = _.cloneDeep(filters[1]);
globalState.filters = filters;
expect(globalState.filters.length).to.be(3);
queryFilter.addFilters(newFilter, true);
$rootScope.$digest();
expect(globalState.filters.length).to.be(3);
});
it('should mutate global filters on appState filter changes', function () {
var idx = 1;
globalState.filters = filters;
var appFilter = _.cloneDeep(filters[idx]);
appFilter.meta.negate = true;
// use addFilters here, so custom adding logic can be applied
queryFilter.addFilters(appFilter);
var res = queryFilter.getFilters();
expect(res).to.have.length(3);
_.each(res, function (filter, i) {
expect(filter.$state.store).to.be('globalState');
// make sure global filter actually mutated
expect(filter.meta.negate).to.be(i === idx);
});
});
});
}];
});

View file

@ -0,0 +1,164 @@
define(function (require) {
return ['get filters', function () {
var _ = require('lodash');
var MockState = require('fixtures/mock_state');
var storeNames = {
app: 'appState',
global: 'globalState'
};
var queryFilter;
var $rootScope, appState, globalState;
beforeEach(module('kibana'));
beforeEach(function () {
appState = new MockState({ filters: [] });
globalState = new MockState({ filters: [] });
});
beforeEach(function () {
module('kibana/global_state', function ($provide) {
$provide.service('getAppState', function () {
return function () {
return appState;
};
});
$provide.service('globalState', function () {
return globalState;
});
});
});
beforeEach(function () {
inject(function (_$rootScope_, Private) {
$rootScope = _$rootScope_;
queryFilter = Private(require('components/filter_bar/query_filter'));
});
});
describe('getFilters method', function () {
var filters;
beforeEach(function () {
filters = [
{ query: { match: { extension: { query: 'jpg', type: 'phrase' } } } },
{ query: { match: { '@tags': { query: 'info', type: 'phrase' } } } }
];
});
it('should return app and global filters', function () {
appState.filters = [filters[0]];
globalState.filters = [filters[1]];
// global filters should be listed first
var res = queryFilter.getFilters();
expect(res[0]).to.eql(filters[1]);
expect(res[1]).to.eql(filters[0]);
// should return updated version of filters
var newFilter = { query: { match: { '_type': { query: 'nginx', type: 'phrase' } } } };
appState.filters.push(newFilter);
res = queryFilter.getFilters();
expect(res).to.contain(newFilter);
});
it('should append the state store', function () {
appState.filters = [filters[0]];
globalState.filters = [filters[1]];
var res = queryFilter.getFilters();
expect(res[0].$state.store).to.be(storeNames.global);
expect(res[1].$state.store).to.be(storeNames.app);
});
it('should return filters from specific states', function () {
var states = [
[ globalState, queryFilter.getGlobalFilters ],
[ appState, queryFilter.getAppFilters ],
];
_.each(states, function (state) {
state[0].filters = filters;
var res = state[1]();
expect(res.length).to.be(state[0].filters.length);
});
});
});
describe('filter reconciliation', function () {
var filters;
beforeEach(function () {
filters = [
{
query: { match: { extension: { query: 'jpg', type: 'phrase' } } },
meta: { negate: false, disabled: false }
},
{
query: { match: { '@tags': { query: 'info', type: 'phrase' } } },
meta: { negate: false, disabled: false }
},
{
query: { match: { '_type': { query: 'nginx', type: 'phrase' } } },
meta: { negate: false, disabled: false }
}
];
});
it('should skip appState filters that match globalState filters', function () {
globalState.filters = filters;
var appFilter = _.cloneDeep(filters[1]);
appState.filters.push(appFilter);
// global filters should be listed first
var res = queryFilter.getFilters();
expect(res).to.have.length(3);
_.each(res, function (filter) {
expect(filter.$state.store).to.be('globalState');
});
});
it('should append conflicting appState filters', function () {
globalState.filters = filters;
var appFilter = _.cloneDeep(filters[1]);
appFilter.meta.negate = true;
appState.filters.push(appFilter);
// global filters should be listed first
var res = queryFilter.getFilters();
expect(res).to.have.length(4);
expect(res.filter(function (filter) {
return filter.$state.store === storeNames.global;
}).length).to.be(3);
expect(res.filter(function (filter) {
return filter.$state.store === storeNames.app;
}).length).to.be(1);
});
it('should not affect disabled filters', function () {
// test adding to globalState
globalState.filters = _.map(filters, function (filter) {
var f = _.cloneDeep(filter);
f.meta.disabled = true;
return f;
});
_.each(filters, function (filter) { globalState.filters.push(filter); });
var res = queryFilter.getFilters();
expect(res).to.have.length(6);
// test adding to appState
globalState.filters = _.map(filters, function (filter) {
var f = _.cloneDeep(filter);
f.meta.disabled = true;
return f;
});
_.each(filters, function (filter) { appState.filters.push(filter); });
res = queryFilter.getFilters();
expect(res).to.have.length(6);
});
});
}];
});

View file

@ -0,0 +1,114 @@
define(function (require) {
return ['invert filters', function () {
var _ = require('lodash');
var sinon = require('test_utils/auto_release_sinon');
var MockState = require('fixtures/mock_state');
var storeNames = {
app: 'appState',
global: 'globalState'
};
var filters;
var queryFilter;
var $rootScope, appState, globalState;
beforeEach(module('kibana'));
beforeEach(function () {
appState = new MockState({ filters: [] });
globalState = new MockState({ filters: [] });
filters = [
{
query: { match: { extension: { query: 'jpg', type: 'phrase' } } },
meta: { negate: false, disabled: false }
},
{
query: { match: { '@tags': { query: 'info', type: 'phrase' } } },
meta: { negate: false, disabled: false }
},
{
query: { match: { '_type': { query: 'nginx', type: 'phrase' } } },
meta: { negate: false, disabled: false }
}
];
});
beforeEach(function () {
module('kibana/global_state', function ($provide) {
$provide.service('getAppState', function () {
return function () {
return appState;
};
});
$provide.service('globalState', function () {
return globalState;
});
});
});
beforeEach(function () {
inject(function (_$rootScope_, Private) {
$rootScope = _$rootScope_;
queryFilter = Private(require('components/filter_bar/query_filter'));
});
});
describe('inverting a filter', function () {
it('should swap the negate property in appState', function () {
_.each(filters, function (filter) {
expect(filter.meta.negate).to.be(false);
appState.filters.push(filter);
});
queryFilter.invertFilter(filters[1]);
expect(appState.filters[1].meta.negate).to.be(true);
});
it('should toggle the negate property in globalState', function () {
_.each(filters, function (filter) {
expect(filter.meta.negate).to.be(false);
globalState.filters.push(filter);
});
queryFilter.invertFilter(filters[1]);
expect(globalState.filters[1].meta.negate).to.be(true);
});
it('should fire the update and fetch events', function () {
var emitSpy = sinon.spy(queryFilter, 'emit');
appState.filters = filters;
// set up the watchers
$rootScope.$digest();
queryFilter.invertFilter(filters[1]);
// trigger the digest loop to fire the watchers
$rootScope.$digest();
expect(emitSpy.callCount).to.be(2);
expect(emitSpy.firstCall.args[0]).to.be('update');
expect(emitSpy.secondCall.args[0]).to.be('fetch');
});
});
describe('bulk inverting', function () {
beforeEach(function () {
appState.filters = filters;
globalState.filters = _.map(_.cloneDeep(filters), function (filter) {
filter.meta.negate = true;
return filter;
});
});
it('should swap the negate state for all filters', function () {
queryFilter.invertAll();
_.each(appState.filters, function (filter) {
expect(filter.meta.negate).to.be(true);
});
_.each(globalState.filters, function (filter) {
expect(filter.meta.negate).to.be(false);
});
});
});
}];
});

View file

@ -0,0 +1,160 @@
define(function (require) {
return ['pin filters', function () {
var _ = require('lodash');
var sinon = require('test_utils/auto_release_sinon');
var MockState = require('fixtures/mock_state');
var storeNames = {
app: 'appState',
global: 'globalState'
};
var filters;
var queryFilter;
var $rootScope, appState, globalState;
beforeEach(module('kibana'));
beforeEach(function () {
appState = new MockState({ filters: [] });
globalState = new MockState({ filters: [] });
filters = [
{
query: { match: { extension: { query: 'gif', type: 'phrase' } } },
meta: { negate: false, disabled: false }
},
{
query: { match: { extension: { query: 'jpg', type: 'phrase' } } },
meta: { negate: true, disabled: false }
},
{
query: { match: { extension: { query: 'png', type: 'phrase' } } },
meta: { negate: true, disabled: true }
},
{
query: { match: { '@tags': { query: 'info', type: 'phrase' } } },
meta: { negate: false, disabled: false }
},
{
query: { match: { '@tags': { query: 'success', type: 'phrase' } } },
meta: { negate: false, disabled: false }
},
{
query: { match: { '@tags': { query: 'security', type: 'phrase' } } },
meta: { negate: true, disabled: false }
},
{
query: { match: { '_type': { query: 'nginx', type: 'phrase' } } },
meta: { negate: false, disabled: false }
},
{
query: { match: { '_type': { query: 'apache', type: 'phrase' } } },
meta: { negate: true, disabled: true }
}
];
});
beforeEach(function () {
module('kibana/global_state', function ($provide) {
$provide.service('getAppState', function () {
return function () {
return appState;
};
});
$provide.service('globalState', function () {
return globalState;
});
});
});
beforeEach(function () {
inject(function (_$rootScope_, Private) {
$rootScope = _$rootScope_;
queryFilter = Private(require('components/filter_bar/query_filter'));
});
});
describe('pin a filter', function () {
beforeEach(function () {
globalState.filters = _.filter(filters, function (filter) {
return !!filter.query.match._type;
});
appState.filters = _.filter(filters, function (filter) {
return !filter.query.match._type;
});
expect(globalState.filters).to.have.length(2);
expect(appState.filters).to.have.length(6);
});
it('should move filter from appState to globalState', function () {
var filter = appState.filters[1];
queryFilter.pinFilter(filter);
expect(globalState.filters).to.contain(filter);
expect(globalState.filters).to.have.length(3);
expect(appState.filters).to.have.length(5);
});
it('should move filter from globalState to appState', function () {
var filter = globalState.filters[1];
queryFilter.pinFilter(filter);
expect(appState.filters).to.contain(filter);
expect(globalState.filters).to.have.length(1);
expect(appState.filters).to.have.length(7);
});
it('should only fire the update event', function () {
var filter = appState.filters[1];
var emitSpy = sinon.spy(queryFilter, 'emit');
// set up the watchers
$rootScope.$digest();
queryFilter.pinFilter(filter);
// trigger the digest loop to fire the watchers
$rootScope.$digest();
expect(emitSpy.callCount).to.be(1);
expect(emitSpy.firstCall.args[0]).to.be('update');
});
});
describe('bulk pinning', function () {
beforeEach(function () {
globalState.filters = _.filter(filters, function (filter) {
return !!filter.query.match.extension;
});
appState.filters = _.filter(filters, function (filter) {
return !filter.query.match.extension;
});
expect(globalState.filters).to.have.length(3);
expect(appState.filters).to.have.length(5);
});
it('should swap the filters in both states', function () {
var appSample = _.sample(appState.filters);
var globalSample = _.sample(globalState.filters);
queryFilter.pinAll();
expect(globalState.filters).to.have.length(5);
expect(appState.filters).to.have.length(3);
expect(globalState.filters).to.contain(appSample);
expect(appState.filters).to.contain(globalSample);
});
it('should move all filters to globalState', function () {
queryFilter.pinAll(true);
expect(globalState.filters).to.have.length(8);
expect(appState.filters).to.have.length(0);
});
it('should move all filters to appState', function () {
queryFilter.pinAll(false);
expect(globalState.filters).to.have.length(0);
expect(appState.filters).to.have.length(8);
});
});
}];
});

View file

@ -0,0 +1,116 @@
define(function (require) {
return ['remove filters', function () {
var _ = require('lodash');
var sinon = require('test_utils/auto_release_sinon');
var MockState = require('fixtures/mock_state');
var storeNames = {
app: 'appState',
global: 'globalState'
};
var filters;
var queryFilter;
var $rootScope, appState, globalState;
beforeEach(module('kibana'));
beforeEach(function () {
appState = new MockState({ filters: [] });
globalState = new MockState({ filters: [] });
filters = [
{
query: { match: { extension: { query: 'jpg', type: 'phrase' } } },
meta: { negate: false, disabled: false }
},
{
query: { match: { '@tags': { query: 'info', type: 'phrase' } } },
meta: { negate: false, disabled: false }
},
{
query: { match: { '_type': { query: 'nginx', type: 'phrase' } } },
meta: { negate: false, disabled: false }
}
];
});
beforeEach(function () {
module('kibana/global_state', function ($provide) {
$provide.service('getAppState', function () {
return function () {
return appState;
};
});
$provide.service('globalState', function () {
return globalState;
});
});
});
beforeEach(function () {
inject(function (_$rootScope_, Private) {
$rootScope = _$rootScope_;
queryFilter = Private(require('components/filter_bar/query_filter'));
});
});
describe('removing a filter', function () {
it('should remove the filter from appState', function () {
appState.filters = filters;
expect(appState.filters).to.have.length(3);
queryFilter.removeFilter(filters[0]);
expect(appState.filters).to.have.length(2);
});
it('should remove the filter from globalState', function () {
globalState.filters = filters;
expect(globalState.filters).to.have.length(3);
queryFilter.removeFilter(filters[0]);
expect(globalState.filters).to.have.length(2);
});
it('should fire the update and fetch events', function () {
var emitSpy = sinon.spy(queryFilter, 'emit');
appState.filters = filters;
// set up the watchers
$rootScope.$digest();
queryFilter.removeFilter(filters[0]);
// trigger the digest loop to fire the watchers
$rootScope.$digest();
expect(emitSpy.callCount).to.be(2);
expect(emitSpy.firstCall.args[0]).to.be('update');
expect(emitSpy.secondCall.args[0]).to.be('fetch');
});
it('should only remove matching instances', function () {
globalState.filters.push(filters[0]);
globalState.filters.push(filters[1]);
appState.filters.push(filters[2]);
queryFilter.removeFilter(_.cloneDeep(filters[0]));
expect(globalState.filters).to.have.length(2);
expect(appState.filters).to.have.length(1);
queryFilter.removeFilter(_.cloneDeep(filters[2]));
expect(globalState.filters).to.have.length(2);
expect(appState.filters).to.have.length(1);
});
});
describe('bulk removal', function () {
it('should remove all the filters from both states', function () {
globalState.filters.push(filters[0]);
globalState.filters.push(filters[1]);
appState.filters.push(filters[2]);
expect(globalState.filters).to.have.length(2);
expect(appState.filters).to.have.length(1);
queryFilter.removeAll();
expect(globalState.filters).to.have.length(0);
expect(appState.filters).to.have.length(0);
});
});
}];
});

View file

@ -0,0 +1,158 @@
define(function (require) {
return ['toggle filters', function () {
var _ = require('lodash');
var sinon = require('test_utils/auto_release_sinon');
var MockState = require('fixtures/mock_state');
var storeNames = {
app: 'appState',
global: 'globalState'
};
var filters;
var queryFilter;
var $rootScope, appState, globalState;
beforeEach(module('kibana'));
beforeEach(function () {
appState = new MockState({ filters: [] });
globalState = new MockState({ filters: [] });
filters = [
{
query: { match: { extension: { query: 'jpg', type: 'phrase' } } },
meta: { negate: false, disabled: false }
},
{
query: { match: { '@tags': { query: 'info', type: 'phrase' } } },
meta: { negate: false, disabled: false }
},
{
query: { match: { '_type': { query: 'nginx', type: 'phrase' } } },
meta: { negate: false, disabled: false }
}
];
});
beforeEach(function () {
module('kibana/global_state', function ($provide) {
$provide.service('getAppState', function () {
return function () {
return appState;
};
});
$provide.service('globalState', function () {
return globalState;
});
});
});
beforeEach(function () {
inject(function (_$rootScope_, Private) {
$rootScope = _$rootScope_;
queryFilter = Private(require('components/filter_bar/query_filter'));
});
});
describe('toggling a filter', function () {
it('should toggle the disabled property in appState', function () {
_.each(filters, function (filter) {
expect(filter.meta.disabled).to.be(false);
appState.filters.push(filter);
});
queryFilter.toggleFilter(filters[1]);
expect(appState.filters[1].meta.disabled).to.be(true);
});
it('should toggle the disabled property in globalState', function () {
_.each(filters, function (filter) {
expect(filter.meta.disabled).to.be(false);
globalState.filters.push(filter);
});
queryFilter.toggleFilter(filters[1]);
expect(globalState.filters[1].meta.disabled).to.be(true);
});
it('should fire the update and fetch events', function () {
var emitSpy = sinon.spy(queryFilter, 'emit');
appState.filters = filters;
// set up the watchers
$rootScope.$digest();
queryFilter.toggleFilter(filters[1]);
// trigger the digest loop to fire the watchers
$rootScope.$digest();
expect(emitSpy.callCount).to.be(2);
expect(emitSpy.firstCall.args[0]).to.be('update');
expect(emitSpy.secondCall.args[0]).to.be('fetch');
});
it('should always enable the filter', function () {
appState.filters = filters.map(function (filter) {
filter.meta.disabled = true;
return filter;
});
expect(appState.filters[1].meta.disabled).to.be(true);
queryFilter.toggleFilter(filters[1], false);
expect(appState.filters[1].meta.disabled).to.be(false);
queryFilter.toggleFilter(filters[1], false);
expect(appState.filters[1].meta.disabled).to.be(false);
});
it('should always disable the filter', function () {
globalState.filters = filters;
expect(globalState.filters[1].meta.disabled).to.be(false);
queryFilter.toggleFilter(filters[1], true);
expect(globalState.filters[1].meta.disabled).to.be(true);
queryFilter.toggleFilter(filters[1], true);
expect(globalState.filters[1].meta.disabled).to.be(true);
});
});
describe('bulk toggling', function () {
beforeEach(function () {
appState.filters = filters;
globalState.filters = _.map(_.cloneDeep(filters), function (filter) {
filter.meta.disabled = true;
return filter;
});
});
it('should swap the enabled state for all filters', function () {
queryFilter.toggleAll();
_.each(appState.filters, function (filter) {
expect(filter.meta.disabled).to.be(true);
});
_.each(globalState.filters, function (filter) {
expect(filter.meta.disabled).to.be(false);
});
});
it('should enable all filters', function () {
queryFilter.toggleAll(true);
_.each(appState.filters, function (filter) {
expect(filter.meta.disabled).to.be(true);
});
_.each(globalState.filters, function (filter) {
expect(filter.meta.disabled).to.be(true);
});
});
it('should disable all filters', function () {
queryFilter.toggleAll(false);
_.each(appState.filters, function (filter) {
expect(filter.meta.disabled).to.be(false);
});
_.each(globalState.filters, function (filter) {
expect(filter.meta.disabled).to.be(false);
});
});
});
}];
});

View file

@ -3,7 +3,6 @@ define(function (require) {
describe('Filter Bar Directive', function () {
describe('dedupFilters(existing, filters)', function () {
it('should return only filters which are not in the existing', function () {
var existing = [
{ range: { bytes: { from: 0, to: 1024 } } },
@ -25,13 +24,26 @@ define(function (require) {
];
var filters = [
{ range: { bytes: { from: 1024, to: 2048 } } },
{ meta: { negate: false }, query: { match: { _term: { query: 'apache', type: 'phrase' } } } }
{ query: { match: { _term: { query: 'apache', type: 'phrase' } } } }
];
var results = dedupFilters(existing, filters);
expect(results).to.contain(filters[0]);
expect(results).to.not.contain(filters[1]);
});
it('should ignore $state attribute', function () {
var existing = [
{ range: { bytes: { from: 0, to: 1024 } } },
{ $state: { store: 'appState' }, query: { match: { _term: { query: 'apache', type: 'phrase' } } } }
];
var filters = [
{ range: { bytes: { from: 1024, to: 2048 } } },
{ $state: { store: 'globalState' }, query: { match: { _term: { query: 'apache', type: 'phrase' } } } }
];
var results = dedupFilters(existing, filters);
expect(results).to.contain(filters[0]);
expect(results).to.not.contain(filters[1]);
});
});
});
});

View file

@ -16,4 +16,4 @@ define(function (require) {
});
});
});
});

View file

@ -5,16 +5,31 @@ define(function (require) {
var $ = require('jquery');
require('components/filter_bar/filter_bar');
var MockState = require('fixtures/mock_state');
describe('Filter Bar Directive', function () {
var $rootScope, $compile, $timeout, Promise;
var appState, queryFilter, mapFilter, getIndexPatternStub, indexPattern, $el;
// require('test_utils/no_digest_promises').activateForSuite();
var $rootScope, $compile, getIndexPatternStub, indexPattern;
beforeEach(function () {
appState = new MockState({ filters: [] });
beforeEach(function (done) {
module('kibana/global_state', function ($provide) {
$provide.service('getAppState', function () {
return function () {
return appState;
};
});
});
});
beforeEach(function () {
// load the application
module('kibana');
getIndexPatternStub = sinon.stub();
module('kibana/courier', function ($provide) {
$provide.service('courier', function () {
var courier = { indexPatterns: { get: getIndexPatternStub } };
@ -22,38 +37,59 @@ define(function (require) {
});
});
inject(function (Promise, Private, _$rootScope_, _$compile_) {
inject(function (Private, $injector, _$rootScope_, _$compile_, _$timeout_) {
$rootScope = _$rootScope_;
$compile = _$compile_;
$timeout = _$timeout_;
Promise = $injector.get('Promise');
mapFilter = Private(require('components/filter_bar/lib/mapFilter'));
indexPattern = Private(require('fixtures/stubbed_logstash_index_pattern'));
getIndexPatternStub.returns(Promise.resolve(indexPattern));
$rootScope.state = {
filters: [
{ meta: { index: 'logstash-*' }, query: { match: { '_type': { query: 'apache' } } } },
{ meta: { index: 'logstash-*' }, query: { match: { '_type': { query: 'nginx' } } } },
{ meta: { index: 'logstash-*' }, exists: { field: '@timestamp' } },
{ meta: { index: 'logstash-*' }, missing: { field: 'host' }, disabled: true },
]
var queryFilter = Private(require('components/filter_bar/query_filter'));
queryFilter.getFilters = function () {
return appState.filters;
};
done();
});
});
it('should render all the filters in state', function () {
var el = $compile('<filter-bar state=state></filter-bar>')($rootScope);
$rootScope.$digest();
var filters = el.find('.filter');
expect(filters).to.have.length(4);
expect($(filters[0]).find('span')[0].innerHTML).to.equal('_type:');
expect($(filters[0]).find('span')[1].innerHTML).to.equal('"apache"');
expect($(filters[1]).find('span')[0].innerHTML).to.equal('_type:');
expect($(filters[1]).find('span')[1].innerHTML).to.equal('"nginx"');
expect($(filters[2]).find('span')[0].innerHTML).to.equal('exists:');
expect($(filters[2]).find('span')[1].innerHTML).to.equal('"@timestamp"');
expect($(filters[3]).find('span')[0].innerHTML).to.equal('missing:');
expect($(filters[3]).find('span')[1].innerHTML).to.equal('"host"');
});
describe('Element rendering', function () {
beforeEach(function (done) {
var filters = [
{ meta: { index: 'logstash-*' }, query: { match: { '_type': { query: 'apache' } } } },
{ meta: { index: 'logstash-*' }, query: { match: { '_type': { query: 'nginx' } } } },
{ meta: { index: 'logstash-*' }, exists: { field: '@timestamp' } },
{ meta: { index: 'logstash-*' }, missing: { field: 'host' }, disabled: true },
];
Promise.map(filters, mapFilter).then(function (filters) {
appState.filters = filters;
$el = $compile('<filter-bar></filter-bar>')($rootScope);
});
var off = $rootScope.$on('filterbar:updated', function () {
off();
// force a nextTick so it continues *after* the $digest loop completes
setTimeout(done, 0);
});
// kick off the digest loop
$rootScope.$digest();
});
it('should render all the filters in state', function () {
var filters = $el.find('.filter');
expect(filters).to.have.length(4);
expect($(filters[0]).find('span')[0].innerHTML).to.equal('_type:');
expect($(filters[0]).find('span')[1].innerHTML).to.equal('"apache"');
expect($(filters[1]).find('span')[0].innerHTML).to.equal('_type:');
expect($(filters[1]).find('span')[1].innerHTML).to.equal('"nginx"');
expect($(filters[2]).find('span')[0].innerHTML).to.equal('exists:');
expect($(filters[2]).find('span')[1].innerHTML).to.equal('"@timestamp"');
expect($(filters[3]).find('span')[0].innerHTML).to.equal('missing:');
expect($(filters[3]).find('span')[1].innerHTML).to.equal('"host"');
});
});
});
});

View file

@ -3,23 +3,95 @@ define(function (require) {
describe('Filter Bar Directive', function () {
describe('onlyDisabled()', function () {
it('should return true if all filters remove are disabled', function () {
it('should return true if all filters are disabled', function () {
var filters = [
{ meta: { disabled: true } },
{ meta: { disabled: true } },
{ meta: { disabled: true } }
];
var newFilters = [{ meta: { disabled: true } }];
expect(onlyDisabled(newFilters, filters)).to.be(true);
});
it('should return false if all filters are not disabled', function () {
var filters = [
{ meta: { disabled: false } },
{ meta: { disabled: false } },
{ meta: { disabled: false } }
];
var newFilters = [{ meta: { disabled: false } }];
expect(onlyDisabled(newFilters, filters)).to.be(false);
});
it('should return false if only old filters are disabled', function () {
var filters = [
{ meta: { disabled: true } },
{ meta: { disabled: true } },
{ meta: { disabled: true } }
];
var newFilters = [{ meta: { disabled: false } }];
expect(onlyDisabled(newFilters, filters)).to.be(false);
});
it('should return false if new filters are not disabled', function () {
var filters = [
{ meta: { disabled: false } },
{ meta: { disabled: false } },
{ meta: { disabled: false } }
];
var newFilters = [{ meta: { disabled: true } }];
expect(onlyDisabled(newFilters, filters)).to.be(false);
});
it('should return true when all removed filters were disabled', function () {
var filters = [
{ meta: { disabled: true } },
{ meta: { disabled: true } },
{ meta: { disabled: true } }
];
var newFilters = [];
expect(onlyDisabled(newFilters, filters)).to.be(true);
});
it('should return false when all removed filters were not disabled', function () {
var filters = [
{ meta: { disabled: false } },
{ meta: { disabled: false } },
{ meta: { disabled: false } }
];
var newFilters = [];
expect(onlyDisabled(newFilters, filters)).to.be(false);
});
it('should return true if all changed filters are disabled', function () {
var filters = [
{ meta: { disabled: true, negate: false } },
{ meta: { disabled: true, negate: false } }
];
var newFilters = [
{ meta: { disabled: true, negate: true } },
{ meta: { disabled: true, negate: true } }
];
expect(onlyDisabled(newFilters, filters)).to.be(true);
});
it('should return false if all filters remove were not disabled', function () {
var filters = [
{ meta: { disabled: false } },
{ meta: { disabled: false } },
{ meta: { disabled: true } }
];
var newFilters = [{ meta: { disabled: false } }];
expect(onlyDisabled(newFilters, filters)).to.be(false);
});
it('should return false when all removed filters are not disabled', function () {
var filters = [
{ meta: { disabled: true } },
{ meta: { disabled: false } },
{ meta: { disabled: true } }
];
var newFilters = [filters[1]];
expect(onlyDisabled(newFilters, filters)).to.be(true);
});
it('should return false if all filters remove are not disabled', function () {
var filters = [
{ meta: { disabled: true } },
{ meta: { disabled: false } },
{ meta: { disabled: false } }
];
var newFilters = [filters[1]];
var newFilters = [];
expect(onlyDisabled(newFilters, filters)).to.be(false);
});

View file

@ -0,0 +1,59 @@
define(function (require) {
var _ = require('lodash');
var queryFilter;
var EventEmitter;
var $rootScope;
describe('Query Filter', function () {
describe('Module', function () {
beforeEach(module('kibana'));
beforeEach(function () {
inject(function (_$rootScope_, Private) {
$rootScope = _$rootScope_;
queryFilter = Private(require('components/filter_bar/query_filter'));
EventEmitter = Private(require('factories/events'));
});
});
describe('module instance', function () {
it('should be an event emitter', function () {
expect(queryFilter).to.be.an(EventEmitter);
});
});
describe('module methods', function () {
it('should have methods for getting filters', function () {
expect(queryFilter.getFilters).to.be.a('function');
expect(queryFilter.getAppFilters).to.be.a('function');
expect(queryFilter.getGlobalFilters).to.be.a('function');
});
it('should have methods for modifying filters', function () {
expect(queryFilter.addFilters).to.be.a('function');
expect(queryFilter.toggleFilter).to.be.a('function');
expect(queryFilter.toggleAll).to.be.a('function');
expect(queryFilter.removeFilter).to.be.a('function');
expect(queryFilter.removeAll).to.be.a('function');
expect(queryFilter.invertFilter).to.be.a('function');
expect(queryFilter.invertAll).to.be.a('function');
expect(queryFilter.pinFilter).to.be.a('function');
expect(queryFilter.pinAll).to.be.a('function');
});
});
});
describe('Actions', function () {
var childSuites = [
require('specs/components/filter_bar/_getFilters'),
require('specs/components/filter_bar/_addFilters'),
require('specs/components/filter_bar/_removeFilters'),
require('specs/components/filter_bar/_toggleFilters'),
require('specs/components/filter_bar/_invertFilters'),
require('specs/components/filter_bar/_pinFilters'),
].forEach(function (s) {
describe(s[0], s[1]);
});
});
});
});

View file

@ -1,40 +0,0 @@
define(function (require) {
var removeAll = require('components/filter_bar/lib/removeAll');
describe('Filter Bar Directive', function () {
var $rootScope, $compile;
beforeEach(function (done) {
// load the application
module('kibana');
inject(function (_$rootScope_, _$compile_) {
$rootScope = _$rootScope_;
$compile = _$compile_;
$rootScope.state = {
filters: [
{ query: { match: { '@tags': { query: 'test' } } } },
{ query: { match: { '@tags': { query: 'bar' } } } },
{ exists: { field: '@timestamp' } },
{ missing: { field: 'host' }, meta: { disabled: true } },
]
};
done();
});
});
describe('removeAll', function () {
it('should remove all the filters', function () {
var fn = removeAll($rootScope);
expect($rootScope.state.filters).to.have.length(4);
fn();
expect($rootScope.state.filters).to.have.length(0);
});
});
});
});

View file

@ -1,40 +0,0 @@
define(function (require) {
var removeFilter = require('components/filter_bar/lib/removeFilter');
describe('Filter Bar Directive', function () {
var $rootScope, $compile;
beforeEach(function (done) {
// load the application
module('kibana');
inject(function (_$rootScope_, _$compile_) {
$rootScope = _$rootScope_;
$compile = _$compile_;
$rootScope.state = {
filters: [
{ query: { match: { '@tags': { query: 'test' } } } },
{ query: { match: { '@tags': { query: 'bar' } } } },
{ exists: { field: '@timestamp' } },
{ missing: { field: 'host' }, meta: { disabled: true } },
]
};
done();
});
});
describe('removeFilter', function () {
it('should remove the filter from the state', function () {
var filter = $rootScope.state.filters[2];
var fn = removeFilter($rootScope);
fn(filter);
expect($rootScope.state.filters).to.not.contain(filter);
});
});
});
});

View file

@ -1,84 +0,0 @@
define(function (require) {
describe('Filter Bar Directive', function () {
var toggleAll = require('components/filter_bar/lib/toggleAll');
var _ = require('lodash');
var sinon = require('test_utils/auto_release_sinon');
var mapFilter, $rootScope, $compile, Promise, getIndexPatternStub, indexPattern;
beforeEach(module('kibana'));
beforeEach(function () {
getIndexPatternStub = sinon.stub();
module('kibana/courier', function ($provide) {
$provide.service('courier', function () {
var courier = { indexPatterns: { get: getIndexPatternStub } };
return courier;
});
});
});
beforeEach(inject(function (_Promise_, _$rootScope_, _$compile_, Private) {
Promise = _Promise_;
mapFilter = Private(require('components/filter_bar/lib/mapFilter'));
indexPattern = Private(require('fixtures/stubbed_logstash_index_pattern'));
getIndexPatternStub.returns(Promise.resolve(indexPattern));
$rootScope = _$rootScope_;
$compile = _$compile_;
$rootScope.state = {
filters: [
{ meta: { index: 'logstash-*' }, query: { match: { '_type': { query: 'apache' } } } },
{ meta: { index: 'logstash-*' }, query: { match: { '_type': { query: 'nginx' } } } },
{ meta: { index: 'logstash-*' }, exists: { field: '@timestamp' } },
{ meta: { index: 'logstash-*', disabled: true }, missing: { field: 'host' } },
]
};
}));
describe('toggleAll', function () {
var fn;
beforeEach(function (done) {
var _filters = _($rootScope.state.filters)
.filter(function (filter) { return filter; })
.flatten(true)
.value();
Promise.map(_filters, mapFilter).then(function (filters) {
$rootScope.filters = filters;
done();
});
$rootScope.$apply();
});
beforeEach(function () {
fn = toggleAll($rootScope);
});
var pickDisabled = function (filter) {
return filter.meta.disabled;
};
it('should toggle all the filters', function () {
expect(_.filter($rootScope.state.filters, pickDisabled)).to.have.length(1);
fn();
expect(_.filter($rootScope.state.filters, pickDisabled)).to.have.length(3);
});
it('should disable all the filters', function () {
expect(_.filter($rootScope.state.filters, pickDisabled)).to.have.length(1);
fn(true);
expect(_.filter($rootScope.state.filters, pickDisabled)).to.have.length(4);
});
it('should enable all the filters', function () {
expect(_.filter($rootScope.state.filters, pickDisabled)).to.have.length(1);
fn(false);
expect(_.filter($rootScope.state.filters, pickDisabled)).to.have.length(0);
});
});
});
});

View file

@ -1,49 +0,0 @@
define(function (require) {
describe('Filter Bar Directive', function () {
var sinon = require('test_utils/auto_release_sinon');
var toggleFilter = require('components/filter_bar/lib/toggleFilter');
var $rootScope, $compile, mapFilter, getIndexPatternStub, indexPattern;
beforeEach(module('kibana'));
beforeEach(function () {
getIndexPatternStub = sinon.stub();
module('kibana/courier', function ($provide) {
$provide.service('courier', function () {
var courier = { indexPatterns: { get: getIndexPatternStub } };
return courier;
});
});
});
beforeEach(inject(function (Promise, Private, _$rootScope_, _$compile_) {
mapFilter = Private(require('components/filter_bar/lib/mapFilter'));
$rootScope = _$rootScope_;
$compile = _$compile_;
indexPattern = Private(require('fixtures/stubbed_logstash_index_pattern'));
getIndexPatternStub.returns(Promise.resolve(indexPattern));
$rootScope.state = {
filters: [
{ meta: { index: 'logstash-*' }, query: { match: { '_type': { query: 'apache' } } } },
{ meta: { index: 'logstash-*' }, query: { match: { '_type': { query: 'nginx' } } } },
{ meta: { index: 'logstash-*' }, exists: { field: '@timestamp' } },
{ missing: { field: 'host' }, meta: { disabled: true, index: 'logstash-*' } },
]
};
}));
describe('toggleFilter', function () {
it('should toggle filters on and off', function (done) {
var filter = $rootScope.state.filters[0];
var fn = toggleFilter($rootScope);
mapFilter(filter).then(fn).then(function (result) {
expect(result.meta).to.have.property('disabled', true);
done();
});
$rootScope.$apply();
});
});
});
});

View file

@ -12,6 +12,35 @@ define(function (require) {
expect(results).to.have.length(1);
});
it('should filter out duplicates, ignoring meta attributes', function () {
var before = [
{
meta: { negate: true },
query: { _type: { match: { query: 'apache', type: 'phrase' } } }
},
{
meta: { negate: false },
query: { _type: { match: { query: 'apache', type: 'phrase' } } }
}
];
var results = uniqFilters(before);
expect(results).to.have.length(1);
});
it('should filter out duplicates, ignoring $state attributes', function () {
var before = [
{
$state: { store: 'appState' },
query: { _type: { match: { query: 'apache', type: 'phrase' } } }
},
{
$state: { store: 'globalState' },
query: { _type: { match: { query: 'apache', type: 'phrase' } } }
}
];
var results = uniqFilters(before);
expect(results).to.have.length(1);
});
});
});
});

View file

@ -1,82 +0,0 @@
define(function (require) {
describe('Filter Bar watchFilters()', function () {
var sinon = require('test_utils/auto_release_sinon');
var _ = require('lodash');
var watchFilters;
var Promise;
var EventEmitter;
var $scope;
beforeEach(module('kibana'));
beforeEach(inject(function (Private, $injector) {
Promise = $injector.get('Promise');
EventEmitter = Private(require('factories/events'));
watchFilters = Private(require('components/filter_bar/lib/watchFilters'));
$scope = {
$watch: sinon.stub()
};
}));
it('returns an event emitter', function () {
expect(watchFilters($scope)).to.be.an(EventEmitter);
});
it('listens to the filters on state', function () {
watchFilters($scope, { update: _.noop, fetch: _.noop });
expect($scope.$watch).to.have.property('callCount', 1);
var call = $scope.$watch.getCall(0);
expect(call.args[0]).to.be('state.filters');
});
describe('change handling', function () {
require('test_utils/no_digest_promises').activateForSuite();
it('calls update and fetch', function () {
var onFetch = sinon.stub();
var onUpdate = sinon.stub();
watchFilters($scope).on('fetch', onFetch).on('update', onUpdate);
var handler = $scope.$watch.args[0][1];
return handler([ {} ], [])
.then(function () {
expect(onUpdate).to.have.property('callCount', 1);
expect(onFetch).to.have.property('callCount', 1);
});
});
it('only calls update if all filters are disabled', function () {
var onFetch = sinon.stub();
var onUpdate = sinon.stub();
watchFilters($scope).on('fetch', onFetch).on('update', onUpdate);
var handler = $scope.$watch.args[0][1];
return handler([ ], [ { meta: { disabled: true } } ])
.then(function () {
expect(onUpdate).to.have.property('callCount', 1);
expect(onFetch).to.have.property('callCount', 0);
});
});
it('calls nothing if there were no changes', function () {
var onFetch = sinon.stub();
var onUpdate = sinon.stub();
watchFilters($scope).on('fetch', onFetch).on('update', onUpdate);
var handler = $scope.$watch.args[0][1];
var cur = [];
var prev = cur;
return Promise.try(handler, [cur, prev])
.then(function () {
expect(onUpdate).to.have.property('callCount', 0);
expect(onFetch).to.have.property('callCount', 0);
});
});
});
});
});

View file

@ -1,18 +1,35 @@
define(function (require) {
var filterManager = require('components/filter_manager/filter_manager');
var $state;
var _ = require('lodash');
var MockState = require('fixtures/mock_state');
var filterManager;
var appState;
describe('Filter Manager', function () {
beforeEach(module('kibana'));
beforeEach(function () {
$state = {
filters: []
};
filterManager.init($state);
module('kibana/global_state', function ($provide) {
$provide.service('getAppState', function () {
return function () {
return appState;
};
});
});
});
it('should have an init function that sets the state to be used', function () {
expect(filterManager.init).to.be.a(Function);
filterManager.init($state);
expect(filterManager.$state).to.be($state);
beforeEach(function () {
inject(function (Private) {
filterManager = Private(require('components/filter_manager/filter_manager'));
appState = new MockState();
appState.filters = [];
// mock queryFilter's invertFilter since it's used in the manager
var queryFilter = Private(require('components/filter_bar/query_filter'));
queryFilter.invertFilter = function (filter) {
filter.meta.negate = !filter.meta.negate;
};
});
});
it('should have an `add` function', function () {
@ -20,35 +37,49 @@ define(function (require) {
});
it('should add a filter', function () {
expect($state.filters.length).to.be(0);
expect(appState.filters.length).to.be(0);
filterManager.add('myField', 1, '+', 'myIndex');
expect($state.filters.length).to.be(1);
expect(appState.filters.length).to.be(1);
});
it('should add multiple filters if passed an array of values', function () {
filterManager.add('myField', [1, 2, 3], '+', 'myIndex');
expect($state.filters.length).to.be(3);
expect(appState.filters.length).to.be(3);
});
it('should add an exists filter if _exists_ is used as the field', function () {
filterManager.add('_exists_', 'myField', '+', 'myIndex');
expect($state.filters[0].exists).to.eql({field: 'myField'});
expect(appState.filters[0].exists).to.eql({field: 'myField'});
});
it('Should negate existing filter instead of added a conflicting filter', function () {
filterManager.add('myField', 1, '+', 'myIndex');
expect($state.filters.length).to.be(1);
expect(appState.filters.length).to.be(1);
filterManager.add('myField', 1, '-', 'myIndex');
expect($state.filters.length).to.be(1);
expect($state.filters[0].meta.negate).to.be(true);
expect(appState.filters.length).to.be(1);
expect(appState.filters[0].meta.negate).to.be(true);
filterManager.add('_exists_', 'myField', '+', 'myIndex');
expect($state.filters.length).to.be(2);
expect(appState.filters.length).to.be(2);
filterManager.add('_exists_', 'myField', '-', 'myIndex');
expect($state.filters.length).to.be(2);
expect($state.filters[1].meta.negate).to.be(true);
expect(appState.filters.length).to.be(2);
expect(appState.filters[1].meta.negate).to.be(true);
});
it('should enable matching filters being changed', function () {
_.each([true, false], function (negate) {
appState.filters = [{
query: { match: { myField: { query: 1 } } },
meta: { disabled: true, negate: negate }
}];
expect(appState.filters.length).to.be(1);
expect(appState.filters[0].meta.disabled).to.be(true);
filterManager.add('myField', 1, '+', 'myIndex');
expect(appState.filters.length).to.be(1);
expect(appState.filters[0].meta.disabled).to.be(false);
});
});
});
});

View file

@ -1,4 +1,5 @@
define(function (require) {
var _ = require('lodash');
describe('$scope.$watchMulti', function () {
var sinon = require('test_utils/auto_release_sinon');
@ -11,119 +12,189 @@ define(function (require) {
$scope = $rootScope.$new();
}));
it('exposes $watchMulti on all scopes', function () {
expect($rootScope.$watchMulti).to.be.a('function');
expect($scope).to.have.property('$watchMulti', $rootScope.$watchMulti);
describe('basic functionality', function () {
it('exposes $watchMulti on all scopes', function () {
expect($rootScope.$watchMulti).to.be.a('function');
expect($scope).to.have.property('$watchMulti', $rootScope.$watchMulti);
var $isoScope = $scope.$new(true);
expect($isoScope).to.have.property('$watchMulti', $rootScope.$watchMulti);
});
it('only triggers a single watch on initialization', function () {
var stub = sinon.stub();
$scope.$watchMulti([
'one',
'two',
'three'
], stub);
$rootScope.$apply();
expect(stub.callCount).to.be(1);
});
it('only triggers a single watch when multiple values change', function () {
var stub = sinon.spy(function (a, b) {});
$scope.$watchMulti([
'one',
'two',
'three'
], stub);
$rootScope.$apply();
expect(stub.callCount).to.be(1);
$scope.one = 'a';
$scope.two = 'b';
$scope.three = 'c';
$rootScope.$apply();
expect(stub.callCount).to.be(2);
});
it('passes an array of the current values as the first arg, and an array of the previous values as the second',
function () {
var stub = sinon.spy(function (a, b) {});
$scope.one = 'a';
$scope.two = 'b';
$scope.three = 'c';
$scope.$watchMulti([
'one',
'two',
'three'
], stub);
$rootScope.$apply();
expect(stub.firstCall.args).to.eql([
['a', 'b', 'c'],
['a', 'b', 'c']
]);
$scope.one = 'do';
$scope.two = 're';
$scope.three = 'mi';
$rootScope.$apply();
expect(stub.secondCall.args).to.eql([
['do', 're', 'mi'],
['a', 'b', 'c']
]);
});
it('the current value is always up to date', function () {
var count = 0;
$scope.vals = [1, 0];
$scope.$watchMulti([ 'vals[0]', 'vals[1]' ], function (cur, prev) {
expect(cur).to.eql($scope.vals);
count++;
var $isoScope = $scope.$new(true);
expect($isoScope).to.have.property('$watchMulti', $rootScope.$watchMulti);
});
var $child = $scope.$new();
$child.$watch('vals[0]', function (cur) {
$child.vals[1] = cur;
});
it('returns a working unwatch function', function () {
$scope.a = 0;
$scope.b = 0;
var triggers = 0;
var unwatch = $scope.$watchMulti(['a', 'b'], function () { triggers++; });
$rootScope.$apply();
expect(count).to.be(2);
// initial watch
$scope.$apply();
expect(triggers).to.be(1);
// prove that it triggers on chagne
$scope.a++;
$scope.$apply();
expect(triggers).to.be(2);
// remove watchers
expect($scope.$$watchers).to.not.eql([]);
unwatch();
expect($scope.$$watchers).to.eql([]);
// prove that it doesn't trigger anymore
$scope.a++;
$scope.$apply();
expect(triggers).to.be(2);
});
});
it('returns a working unwatch function', function () {
$scope.a = 0;
$scope.b = 0;
var triggers = 0;
var unwatch = $scope.$watchMulti(['a', 'b'], function () { triggers++; });
describe('simple scope watchers', function () {
it('only triggers a single watch on initialization', function () {
var stub = sinon.stub();
// initial watch
$scope.$apply();
expect(triggers).to.be(1);
$scope.$watchMulti([
'one',
'two',
'three'
], stub);
$rootScope.$apply();
// prove that it triggers on chagne
$scope.a++;
$scope.$apply();
expect(triggers).to.be(2);
expect(stub.callCount).to.be(1);
});
// remove watchers
expect($scope.$$watchers).to.not.eql([]);
unwatch();
expect($scope.$$watchers).to.eql([]);
it('only triggers a single watch when multiple values change', function () {
var stub = sinon.spy(function (a, b) {});
// prove that it doesn't trigger anymore
$scope.a++;
$scope.$apply();
expect(triggers).to.be(2);
$scope.$watchMulti([
'one',
'two',
'three'
], stub);
$rootScope.$apply();
expect(stub.callCount).to.be(1);
$scope.one = 'a';
$scope.two = 'b';
$scope.three = 'c';
$rootScope.$apply();
expect(stub.callCount).to.be(2);
});
it('passes an array of the current and previous values, in order',
function () {
var stub = sinon.spy(function (a, b) {});
$scope.one = 'a';
$scope.two = 'b';
$scope.three = 'c';
$scope.$watchMulti([
'one',
'two',
'three'
], stub);
$rootScope.$apply();
expect(stub.firstCall.args).to.eql([
['a', 'b', 'c'],
['a', 'b', 'c']
]);
$scope.one = 'do';
$scope.two = 're';
$scope.three = 'mi';
$rootScope.$apply();
expect(stub.secondCall.args).to.eql([
['do', 're', 'mi'],
['a', 'b', 'c']
]);
});
it('always has an up to date value', function () {
var count = 0;
$scope.vals = [1, 0];
$scope.$watchMulti([ 'vals[0]', 'vals[1]' ], function (cur, prev) {
expect(cur).to.eql($scope.vals);
count++;
});
var $child = $scope.$new();
$child.$watch('vals[0]', function (cur) {
$child.vals[1] = cur;
});
$rootScope.$apply();
expect(count).to.be(2);
});
});
describe('complex watch expressions', function () {
var stateWatchers;
var firstValue;
var secondValue;
beforeEach(function () {
var firstGetter = function () {
return firstValue;
};
var secondGetter = function () {
return secondValue;
};
stateWatchers = [{
fn: $rootScope.$watch,
get: firstGetter
}, {
fn: $rootScope.$watch,
get: secondGetter
}];
});
it('should trigger the watcher on initialization', function () {
var stub = sinon.stub();
firstValue = 'first';
secondValue = 'second';
$scope.$watchMulti(stateWatchers, stub);
$rootScope.$apply();
expect(stub.callCount).to.be(1);
expect(stub.firstCall.args[0]).to.eql([firstValue, secondValue]);
expect(stub.firstCall.args[1]).to.eql([firstValue, secondValue]);
});
});
describe('nested watchers', function () {
it('should trigger the handler at least once', function () {
var $scope = $rootScope.$new();
$scope.$$watchers = [{
get: _.noop,
fn: _.noop,
eq: false,
last: false
}, {
get: _.noop,
fn: registerWatchers,
eq: false,
last: false
}];
var first = sinon.stub();
var second = sinon.stub();
function registerWatchers() {
$scope.$watchMulti([first, second], function () {
expect(first.callCount).to.be.greaterThan(0);
expect(second.callCount).to.be.greaterThan(0);
});
}
$scope.$digest();
});
});
});
});

View file

@ -2,6 +2,7 @@ define(function (require) {
var sinon = require('test_utils/auto_release_sinon');
var faker = require('faker');
var _ = require('lodash');
var MockState = require('fixtures/mock_state');
// global vars, injected and mocked in init()
var kbnUrl;
@ -29,8 +30,7 @@ define(function (require) {
});
$provide.service('globalState', function () {
globalStateMock = {};
globalStateMock.on = globalStateMock.off = _.noop;
globalStateMock = new MockState();
globalStateMock.removeFromUrl = function (url) {
return url;
};