Merge branch 'pr/5109'
This commit is contained in:
commit
41bf13cd80
12 changed files with 329 additions and 12 deletions
57
src/ui/public/directives/__tests__/json_input.js
Normal file
57
src/ui/public/directives/__tests__/json_input.js
Normal 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();
|
||||
});
|
||||
});
|
33
src/ui/public/directives/json_input.js
Normal file
33
src/ui/public/directives/json_input.js
Normal 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);
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
});
|
85
src/ui/public/filter_bar/__tests__/_updateFilters.js
Normal file
85
src/ui/public/filter_bar/__tests__/_updateFilters.js
Normal 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);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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.');
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
Loading…
Reference in a new issue