Merge branch 'pr/5109'

This commit is contained in:
spalger 2015-10-30 16:30:17 -05:00
commit 41bf13cd80
12 changed files with 329 additions and 12 deletions

View file

@ -0,0 +1,57 @@
var angular = require('angular');
var expect = require('expect.js');
var ngMock = require('ngMock');
require('ui/directives/json_input');
describe('JSON input validation', function () {
var $compile;
var $rootScope;
var html = '<input ng-model="value" json-input require-keys=true />';
var element;
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function (_$compile_, _$rootScope_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
}));
beforeEach(function () {
element = $compile(html)($rootScope);
});
it('should be able to require keys', function () {
element.val('{}');
element.trigger('input');
expect(element.hasClass('ng-invalid')).to.be.ok();
});
it('should be able to not require keys', function () {
var html = '<input ng-model="value" json-input require-keys=false />';
var element = $compile(html)($rootScope);
element.val('{}');
element.trigger('input');
expect(element.hasClass('ng-valid')).to.be.ok();
});
it('should be able to read parse an input', function () {
element.val('{}');
element.trigger('input');
expect($rootScope.value).to.eql({});
});
it('should not allow invalid json', function () {
element.val('{foo}');
element.trigger('input');
expect(element.hasClass('ng-invalid')).to.be.ok();
});
it('should allow valid json', function () {
element.val('{"foo": "bar"}');
element.trigger('input');
expect($rootScope.value).to.eql({foo: 'bar'});
expect(element.hasClass('ng-valid')).to.be.ok();
});
});

View file

@ -0,0 +1,33 @@
define(function (require) {
var _ = require('lodash');
var angular = require('angular');
require('ui/modules')
.get('kibana')
.directive('jsonInput', function () {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, $el, attrs, ngModelCntrl) {
ngModelCntrl.$formatters.push(toJSON);
ngModelCntrl.$parsers.push(fromJSON);
function fromJSON(value) {
try {
value = JSON.parse(value);
var validity = !scope.$eval(attrs.requireKeys) ? true : _.keys(value).length > 0;
ngModelCntrl.$setValidity('json', validity);
} catch (e) {
ngModelCntrl.$setValidity('json', false);
}
return value;
}
function toJSON(value) {
return angular.toJson(value, 2);
}
}
};
});
});

View file

@ -0,0 +1,85 @@
describe('update filters', function () {
var _ = require('lodash');
var sinon = require('auto-release-sinon');
var expect = require('expect.js');
var ngMock = require('ngMock');
var MockState = require('fixtures/mock_state');
var storeNames = {
app: 'appState',
global: 'globalState'
};
var queryFilter;
var appState;
var globalState;
var $rootScope;
beforeEach(ngMock.module(
'kibana',
'kibana/courier',
'kibana/global_state',
function ($provide) {
$provide.service('courier', require('fixtures/mock_courier'));
appState = new MockState({ filters: [] });
$provide.service('getAppState', function () {
return function () { return appState; };
});
globalState = new MockState({ filters: [] });
$provide.service('globalState', function () {
return globalState;
});
}
));
beforeEach(ngMock.inject(function (Private, _$rootScope_) {
$rootScope = _$rootScope_;
queryFilter = Private(require('ui/filter_bar/query_filter'));
}));
describe('updating', function () {
var currentFilter;
beforeEach(function () {
currentFilter = {query: { match: { extension: { query: 'jpg', type: 'phrase' } } } };
});
it('should be able to update a filter', function () {
var newFilter = _.cloneDeep(currentFilter);
newFilter.query.match.extension.query = 'png';
expect(currentFilter.query.match.extension.query).to.be('jpg');
queryFilter.updateFilter({
source: currentFilter,
model: newFilter
});
$rootScope.$digest();
expect(currentFilter.query.match.extension.query).to.be('png');
});
it('should replace the filter type if it is changed', function () {
var newFilter = {
'range': {
'bytes': {
'gte': 0,
'lt': 1000
}
}
};
expect(currentFilter.query).not.to.be(undefined);
queryFilter.updateFilter({
source: currentFilter,
model: newFilter,
type: 'query'
});
$rootScope.$digest();
expect(currentFilter.query).to.be(undefined);
expect(currentFilter.range).not.to.be(undefined);
expect(_.eq(currentFilter.range, newFilter.range)).to.be(true);
});
});
});

View file

@ -1,9 +1,9 @@
var angular = require('angular');
var _ = require('lodash');
var $ = require('jquery');
var ngMock = require('ngMock');
var expect = require('expect.js');
var sinon = require('sinon');
require('ui/filter_bar');
var MockState = require('fixtures/mock_state');
@ -17,6 +17,7 @@ describe('Filter Bar Directive', function () {
var queryFilter;
var mapFilter;
var $el;
var $scope;
// require('testUtils/noDigestPromises').activateForSuite();
beforeEach(ngMock.module('kibana/global_state', function ($provide) {
@ -59,6 +60,7 @@ describe('Filter Bar Directive', function () {
Promise.map(filters, mapFilter).then(function (filters) {
appState.filters = filters;
$el = $compile('<filter-bar></filter-bar>')($rootScope);
$scope = $el.isolateScope();
});
var off = $rootScope.$on('filterbar:updated', function () {
@ -83,5 +85,29 @@ describe('Filter Bar Directive', function () {
expect($(filters[3]).find('span')[0].innerHTML).to.equal('missing:');
expect($(filters[3]).find('span')[1].innerHTML).to.equal('"host"');
});
describe('editing filters', function () {
beforeEach(function () {
$scope.startEditingFilter(appState.filters[3]);
$scope.$digest();
});
it('should be able to edit a filter', function () {
expect($el.find('.filter-edit-container').length).to.be(1);
});
it('should be able to stop editing a filter', function () {
$scope.stopEditingFilter();
$scope.$digest();
expect($el.find('.filter-edit-container').length).to.be(0);
});
it('should merge changes after clicking done', function () {
sinon.spy($scope, 'updateFilter');
$scope.editDone();
expect($scope.updateFilter.called).to.be(true);
});
});
});
});

View file

@ -33,6 +33,7 @@ describe('Query Filter', function () {
expect(queryFilter.toggleAll).to.be.a('function');
expect(queryFilter.removeFilter).to.be.a('function');
expect(queryFilter.removeAll).to.be.a('function');
expect(queryFilter.updateFilter).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');
@ -45,6 +46,7 @@ describe('Query Filter', function () {
require('./_getFilters');
require('./_addFilters');
require('./_removeFilters');
require('./_updateFilters');
require('./_toggleFilters');
require('./_invertFilters');
require('./_pinFilters');

View file

@ -32,8 +32,32 @@
<a class="action filter-remove" ng-click="removeFilter(filter)">
<i class="fa fa-fw fa-trash"></i>
</a>
<a class="action filter-edit" ng-click="startEditingFilter(filter)">
<i class="fa fa-fw fa-edit"></i>
</a>
</div>
</div>
<div class="filter-edit-container" ng-if="editingFilter">
<form role="form" name="editFilterForm" ng-submit="editDone()">
<div
json-input
require-keys="true"
ui-ace="{
mode: 'json',
onLoad: aceLoaded
}"
ng-model="editingFilter.model"></div>
<div class="form-group">
<button class="btn btn-primary" ng-click="stopEditingFilter()">Cancel</button>
<button type="submit" class="btn btn-success"
ng-disabled="editFilterForm.$invalid"
>Done</button>
<small ng-show="editFilterForm.$invalid">Could not parse JSON input</small>
</div>
</form>
</div>
<div class="filter-link">
<div class="filter-description small">
<a ng-click="showFilterActions = !showFilterActions">

View file

@ -3,6 +3,9 @@ define(function (require) {
var module = require('ui/modules').get('kibana');
var template = require('ui/filter_bar/filter_bar.html');
var moment = require('moment');
var angular = require('angular');
require('ui/directives/json_input');
module.directive('filterBar', function (Private, Promise, getAppState) {
var mapAndFlattenFilters = Private(require('ui/filter_bar/lib/mapAndFlattenFilters'));
@ -28,13 +31,21 @@ define(function (require) {
'invertFilter',
'invertAll',
'removeFilter',
'removeAll'
'removeAll',
'updateFilter'
].forEach(function (method) {
$scope[method] = queryFilter[method];
});
$scope.state = getAppState();
$scope.aceLoaded = function (editor) {
editor.$blockScrolling = Infinity;
var session = editor.getSession();
session.setTabSize(2);
session.setUseSoftTabs(true);
};
$scope.applyFilters = function (filters) {
// add new filters
$scope.addFilters(filterAppliedAndUnwrap(filters));
@ -46,6 +57,36 @@ define(function (require) {
}
};
var privateFieldRegexp = /(^\$|meta)/;
$scope.startEditingFilter = function (source) {
var model = _.cloneDeep(source);
var filterType;
//Hide private properties and figure out what type of filter this is
_.each(model, function (value, key) {
if (key.match(privateFieldRegexp)) {
delete model[key];
} else {
filterType = key;
}
});
$scope.editingFilter = {
source: source,
type: filterType,
model: model
};
};
$scope.stopEditingFilter = function () {
$scope.editingFilter = null;
};
$scope.editDone = function () {
$scope.updateFilter($scope.editingFilter);
$scope.stopEditingFilter();
};
$scope.clearFilterBar = function () {
$scope.newFilters = [];
$scope.changeTimeFilter = null;
@ -53,6 +94,7 @@ define(function (require) {
// update the scope filter list on filter changes
$scope.$listen(queryFilter, 'update', function () {
$scope.stopEditingFilter();
updateFilters();
});

View file

@ -28,7 +28,7 @@ filter-bar .confirm {
position: relative;
display: inline-block;
text-align: center;
min-width: 110px;
min-width: 140px;
font-size: @font-size-small;
background-color: @filter-bar-confirm-filter-bg;
@ -55,6 +55,11 @@ filter-bar .bar {
background: @filter-bar-bar-condensed-bg;
}
.ace_editor {
height: 175px;
margin: 15px 0;
}
.filter-link {
position: relative;
display: inline-block;
@ -72,7 +77,7 @@ filter-bar .bar {
position: relative;
display: inline-block;
text-align: center;
min-width: 110px;
min-width: 140px;
font-size: @font-size-small;
background-color: @filter-bar-bar-filter-bg;

View file

@ -21,8 +21,26 @@ describe('Filter Bar Directive', function () {
$rootScope.$apply();
});
it('should return undefined for none matching', function (done) {
var filter = { range: { gt: 0, lt: 1024 } };
it('should work with undefined filter types', function (done) {
var filter = {
'bool': {
'must': {
'term': {
'geo.src': 'US'
}
}
}
};
mapDefault(filter).then(function (result) {
expect(result).to.have.property('key', 'bool');
expect(result).to.have.property('value', JSON.stringify(filter.bool));
done();
});
$rootScope.$apply();
});
it('should return undefined if there is no valid key', function (done) {
var filter = { meta: {} };
mapDefault(filter).catch(function (result) {
expect(result).to.be(filter);
done();

View file

@ -74,7 +74,7 @@ describe('Filter Bar Directive', function () {
});
it('should finish with a catch', function (done) {
var before = { meta: { index: 'logstash-*' }, foo: '' };
var before = { meta: { index: 'logstash-*' }};
mapFilter(before).catch(function (error) {
expect(error).to.be.an(Error);
expect(error.message).to.be('No mappings have been found for filter.');

View file

@ -1,13 +1,17 @@
define(function (require) {
return function mapDefaultProvider(Promise) {
var angular = require('angular');
var _ = require('lodash');
var metaProperty = /(^\$|meta)/;
return function (filter) {
var key;
var value;
if (filter.query) {
key = 'query';
value = angular.toJson(filter.query);
var key = _.find(_.keys(filter), function (key) {
return !key.match(metaProperty);
});
if (key) {
var value = angular.toJson(filter[key]);
return Promise.resolve({ key: key, value: value });
}
return Promise.reject(filter);

View file

@ -8,6 +8,7 @@ define(function (require) {
var uniqFilters = require('ui/filter_bar/lib/uniqFilters');
var compareFilters = require('ui/filter_bar/lib/compareFilters');
var mapAndFlattenFilters = Private(require('ui/filter_bar/lib/mapAndFlattenFilters'));
var angular = require('angular');
var queryFilter = new EventEmitter();
@ -82,6 +83,26 @@ define(function (require) {
state.filters.splice(index, 1);
};
/**
* Updates an existing filter
* @param {object} filter Contains a reference to a filter and its new model
* @param {object} filter.source The filter reference
* @param {string} filter.model The edited filter
* @returns {object} Promise that resolves to the new filter on a successful merge
*/
queryFilter.updateFilter = function (filter) {
var mergedFilter = _.assign({}, filter.source, filter.model);
//If the filter type is changed we want to discard the old type
//when merging changes back in
var filterTypeReplaced = filter.model[filter.type] !== mergedFilter[filter.type];
if (filterTypeReplaced) {
delete mergedFilter[filter.type];
}
return angular.copy(mergedFilter, filter.source);
};
/**
* Removes all filters
*/