Filter Bar Directive

- Created a new direcive for the filter bar
- integrated new filter bar with discover
This commit is contained in:
Chris Cowan 2014-07-28 16:02:05 -07:00 committed by Chris Cowan
parent f73066d58c
commit 97276fc204
15 changed files with 234 additions and 18 deletions

View file

@ -77,7 +77,8 @@ define(function (require) {
query: initialQuery || '',
columns: ['_source'],
index: config.get('defaultIndex'),
interval: 'auto'
interval: 'auto',
filters: _.cloneDeep($scope.searchSource.get('filter'))
};
var metaFields = config.get('metaFields');
@ -93,7 +94,7 @@ define(function (require) {
'year'
];
var $state = $scope.state = new appStateFactory.create(stateDefaults);
var $state = $scope.state = appStateFactory.create(stateDefaults);
if (!_.contains(indexPatternList, $state.index)) {
var reason = 'The index specified in the URL is not a configured pattern. ';
@ -161,6 +162,11 @@ define(function (require) {
if (!angular.equals(sort, currentSort)) $scope.fetch();
});
$scope.$watch('state.filters', function (filters) {
$scope.searchSource.set('filter', filters);
$scope.fetch();
});
$scope.$watch('opts.timefield', function (timefield) {
timefilter.enabled(!!timefield);
});
@ -199,6 +205,8 @@ define(function (require) {
if (!init.complete) return;
$scope.updateTime();
if (_.isEmpty($state.columns)) refreshColumns();
$state.save();
$scope.updateDataSource()
.then(setupVisualization)
.then(function () {
@ -340,7 +348,6 @@ define(function (require) {
$scope.updateDataSource = function () {
var chartOptions;
$scope.searchSource
.size($scope.opts.sampleSize)
.sort(function () {
@ -354,7 +361,8 @@ define(function (require) {
}
return sort;
})
.query(!$state.query ? null : $state.query);
.query(!$state.query ? null : $state.query)
.set('filter', $state.filters || []);
// get the current indexPattern
var indexPattern = $scope.searchSource.get('index');
@ -431,20 +439,26 @@ define(function (require) {
// TODO: On array fields, negating does not negate the combination, rather all terms
$scope.filterQuery = function (field, value, operation) {
value = _.isArray(value) ? value : [value];
operation = operation || '+';
var indexPattern = $scope.searchSource.get('index');
indexPattern.popularizeField(field, 1);
_.each(value, function (clause) {
var filter = field + ':"' + addSlashes(clause) + '"';
var regex = '[\\+-]' + regexEscape(filter) + '\\s*';
// Grap the filters from the searchSource and ensure it's an array
var filters = _.flatten([$state.filters], true);
$state.query = $state.query.replace(new RegExp(regex), '') +
' ' + operation + filter;
_.each(value, function (clause) {
var previous = _.find(filters, function (item) {
return item && item.query.match[field] === clause;
});
if (!previous) {
var filter = { query: { match: {} } };
filter.negate = operation === '-';
filter.query.match[field] = clause;
filters.push(filter);
}
});
$scope.fetch();
$state.filters = filters;
};
$scope.toggleField = function (name) {

View file

@ -24,6 +24,9 @@
<config config-template="configTemplate" config-object="opts" config-close="configClose" config-submit="fetch"></config>
<div class="container-fluid">
<div class="row">
<filter-bar state="state"></filter-bar>
</div>
<div class="row">
<div class="discover-hits"><strong>{{hits || 0}}</strong> hits</div>

View file

@ -63,7 +63,7 @@ define(function (require) {
$scope.fields = _.sortBy(indexPattern.fields, 'name');
$scope.fields.byName = indexPattern.fieldsByName;
var $state = $scope.state = new appStateFactory.create(vis.getState());
var $state = $scope.state = appStateFactory.create(vis.getState());
if ($state.query) {
vis.searchSource.set('query', $state.query);

View file

@ -247,6 +247,29 @@ define(function (require) {
};
}
/**
* Create a filter that can be reversed for filters with negate set
* @param {boolean} reverse This will reverse the filter. If true then
* anything where negate is set will come
* through otherwise it will filter out
* @returns {function}
*/
var filterNegate = function (reverse) {
return function (filter) {
if (_.isUndefined(filter.negate)) return !reverse;
return filter.negate === reverse;
};
};
/**
* Clean out any invalid attributes from the filters
* @param {object} filter The filter to clean
* @returns {object}
*/
var cleanFilter = function (filter) {
return _.omit(filter, ['negate', 'disabled']);
};
// switch to filtered query if there are filters
if (flatState.filters) {
if (flatState.filters.length) {
@ -255,7 +278,8 @@ define(function (require) {
query: flatState.body.query,
filter: {
bool: {
must: flatState.filters
must: _(flatState.filters).filter(filterNegate(false)).map(cleanFilter).value(),
must_not: _(flatState.filters).filter(filterNegate(true)).map(cleanFilter).value()
}
}
}

View file

@ -157,7 +157,14 @@ define(function (require) {
switch (key) {
case 'filter':
// user a shallow flatten to detect if val is an array, and pull the values out if it is
state.filters = _.flatten([ state.filters || [], val ], true);
state.filters = _([ state.filters || [], val ])
.flatten(true)
// Yo Dawg! I heard you needed to filter out your filters
.filter(function (filter) {
if (!filter) return false;
return !(!!filter.disabled);
})
.value();
return;
case 'index':
case 'type':

View file

@ -0,0 +1,30 @@
filter-bar .bar {
padding: 6px 6px 4px 6px;
background: #dde4e6;
}
filter-bar .bar .title {
display: inline;
color: #748287;
margin: 0 6px;
}
filter-bar .bar .filter {
font-size: 12px;
border-radius: 12px;
display: inline-block;
background-color: #83949C;
padding: 4px 8px;
color: #fff;
margin-right: 4px;
margin-bottom: 4px;
}
filter-bar .bar .filter .value {
border-right: 1px solid rgba(255, 255, 255, 0.4);
padding-right: 8px;
margin-right: 4px;
}
filter-bar .bar .filter.negate {
background-color: #D18282;
}
filter-bar .bar .filter a {
color: #FFF;
}

View file

@ -0,0 +1,9 @@
<div class="bar" ng-show="filters.length">
<!-- <div class="title">Filters</div> -->
<div class="filter" ng-class="{ negate: filter.negate }" ng-repeat="filter in filters">
<span class="key">{{ filter.key }}:</span>
<span class="value">"{{ filter.value }}"</span>
<a class="fa" tooltip="Toggle" tooltip-placement="top" ng-class="{ 'fa-eye-slash': filter.disabled, 'fa-eye': !filter.disabled }" ng-click="toggleFilter(filter)"><a>
<a class="fa fa-times" tooltip="Remove" tooltip-placement="top" ng-click="removeFilter(filter)"><a>
</div>
</div>

View file

@ -0,0 +1,88 @@
define(function (require) {
'use strict';
var _ = require('lodash');
var module = require('modules').get('kibana');
var template = require('text!components/filter_bar/filter_bar.html');
module.directive('filterBar', function (courier) {
return {
restrict: 'E',
template: template,
scope: {
state: '='
},
link: function ($scope, $el, attrs) {
/**
* Map the filter into an object with the key and value exposed so it's
* easier to work with in the template
* @param {object} fitler The filter the map
* @returns {object}
*/
var mapFilter = function (filter) {
var key = _.keys(filter.query.match)[0];
return {
key: key,
value: filter.query.match[key],
disabled: !!(filter.disabled),
negate: !!(filter.negate),
filter: filter
};
};
$scope.$watch('state.filters', function (filters) {
// Get the filters from the searchSource
$scope.filters = _(filters)
.filter(function (filter) {
return filter;
})
.flatten(true)
.map(mapFilter)
.value();
});
/**
* Remap the filter from the intermediary back to it's original.
* @param {object} filter The original filter
* @returns {object}
*/
var remapFilters = function (filter) {
return filter.filter;
};
/**
* Toggles the filter between enabled/disabled.
* @param {object} filter The filter to toggle
* @returns {void}
*/
$scope.toggleFilter = function (filter) {
// Toggle the disabled flag
var disabled = !(!!filter.disabled);
filter.disabled = disabled;
filter.filter.disabled = disabled;
// Save the filters back to the searchSource
$scope.state.filters = _.map($scope.filters, remapFilters);
};
/**
* Removes the filter from the searchSource
* @param {object} filter The filter to remove
* @returns {void}
*/
$scope.removeFilter = 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.filter !== invalidFilter.filter;
})
.map(remapFilters)
.value();
};
}
};
});
});

View file

@ -0,0 +1,35 @@
filter-bar .bar {
padding: 6px 6px 4px 6px;
background: #dde4e6;
.title {
display: inline;
color: #748287;
margin: 0 6px;
}
.filter {
font-size: 12px;
border-radius: 12px;
display: inline-block;
background-color: #83949C;
padding: 4px 8px;
color: #fff;
margin-right: 4px;
margin-bottom: 4px;
.value {
border-right: 1px solid rgba(255, 255, 255, 0.4);
padding-right: 8px;
margin-right: 4px;
}
&.negate {
background-color: #D18282;
}
a {
color: #FFF;
}
}
}

View file

@ -11,6 +11,7 @@ define(function (require) {
var getAppStash = function (search) {
var appStash = search._a && rison.decode(search._a);
if (app.current) {
// Apply the defaults to appStash
appStash = _.defaults(appStash || {}, app.defaults);
}
return appStash;
@ -137,4 +138,4 @@ define(function (require) {
};
};
};
});
});

View file

@ -9,6 +9,7 @@ define(function (require) {
AppState.Super.call(this, '_a', defaults);
}
return AppState;
};

View file

@ -10,6 +10,7 @@ define(function (require) {
require('components/courier/courier');
require('components/notify/notify');
require('components/state_management/app_state_factory');
require('components/filter_bar/filter_bar');
require('directives/info');
require('directives/spinner');
require('directives/paginate');

View file

@ -347,3 +347,4 @@ input[type="checkbox"],
}
}
@import '../components/filter_bar/filter_bar.less';

View file

@ -9,7 +9,8 @@ module.exports = {
'<%= src %>/kibana/apps/settings/styles/main.less',
'<%= src %>/kibana/apps/visualize/styles/main.less',
'<%= src %>/kibana/apps/visualize/styles/visualization.less',
'<%= src %>/kibana/styles/main.less'
'<%= src %>/kibana/styles/main.less',
'<%= src %>/kibana/components/**/*.less'
],
expand: true,
ext: '.css',
@ -18,4 +19,4 @@ module.exports = {
paths: [bc + '/lesshat/build/']
}
}
};
};

View file

@ -8,7 +8,8 @@ module.exports = function (grunt) {
},
less: {
files: [
'<%= app %>/**/styles/**/*.less'
'<%= app %>/**/styles/**/*.less',
'<%= app %>/**/components/**/*.less'
],
tasks: ['less']
},