Merge pull request #3262 from lukasolson/issues/3172
Add date range aggregation
This commit is contained in:
commit
3cef495a57
|
@ -14,7 +14,7 @@ define(function (require) {
|
|||
|
||||
// Create the new branch record
|
||||
var $parent = parent && parent.aggConfigResult;
|
||||
var aggConfigResult = new AggConfigResult(agg, $parent, value, bucket.key);
|
||||
var aggConfigResult = new AggConfigResult(agg, $parent, value, agg.getKey(bucket));
|
||||
var branch = {
|
||||
name: bucket.key,
|
||||
size: value,
|
||||
|
|
|
@ -75,7 +75,7 @@ define(function (require) {
|
|||
if (!_.isEmpty(displayName)) split.label += ': ' + displayName;
|
||||
|
||||
split.tooltipFormatter = tooltipFormatter(raw.columns);
|
||||
var aggConfigResult = new AggConfigResult(firstAgg, null, null, bucket.key);
|
||||
var aggConfigResult = new AggConfigResult(firstAgg, null, null, firstAgg.getKey(bucket));
|
||||
split.split = { aggConfig: firstAgg, aggConfigResult: aggConfigResult, key: bucket.key };
|
||||
_.each(split.slices.children, function (child) {
|
||||
child.aggConfigResult.$parent = aggConfigResult;
|
||||
|
|
|
@ -187,7 +187,7 @@ define(function (require) {
|
|||
newList.unshift(injected);
|
||||
}
|
||||
|
||||
var newAcr = new AggConfigResult(acr.aggConfig, newList[0], acr.value, acr.key);
|
||||
var newAcr = new AggConfigResult(acr.aggConfig, newList[0], acr.value, acr.aggConfig.getKey(acr));
|
||||
newList.unshift(newAcr);
|
||||
|
||||
// and replace the acr in the row buffer if its there
|
||||
|
|
|
@ -38,12 +38,12 @@ define(function (require) {
|
|||
var splitting = write.canSplit && agg.schema.name === 'split';
|
||||
if (splitting) {
|
||||
write.split(agg, buckets, function forEachBucket(subBucket, key) {
|
||||
collectBucket(write, subBucket, key);
|
||||
collectBucket(write, subBucket, agg.getKey(subBucket), key);
|
||||
});
|
||||
} else {
|
||||
buckets.forEach(function (subBucket, key) {
|
||||
write.cell(agg, key, function () {
|
||||
collectBucket(write, subBucket, key);
|
||||
write.cell(agg, agg.getKey(subBucket, key), function () {
|
||||
collectBucket(write, subBucket, agg.getKey(subBucket, key));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -6,8 +6,16 @@ define(function (require) {
|
|||
_(BucketAggType).inherits(AggType);
|
||||
function BucketAggType(config) {
|
||||
BucketAggType.Super.call(this, config);
|
||||
|
||||
if (_.isFunction(config.getKey)) {
|
||||
this.getKey = config.getKey;
|
||||
}
|
||||
}
|
||||
|
||||
BucketAggType.prototype.getKey = function (bucket, key) {
|
||||
return key || bucket.key;
|
||||
};
|
||||
|
||||
return BucketAggType;
|
||||
};
|
||||
});
|
|
@ -0,0 +1,18 @@
|
|||
define(function (require) {
|
||||
var dateRange = require('utils/date_range');
|
||||
|
||||
return function createDateRangeFilterProvider(config) {
|
||||
var buildRangeFilter = require('components/filter_manager/lib/range');
|
||||
|
||||
return function (agg, key) {
|
||||
var range = dateRange.parse(key, config.get('dateFormat'));
|
||||
|
||||
var filter = {};
|
||||
if (range.from) filter.gte = +range.from;
|
||||
if (range.to) filter.lt = +range.to;
|
||||
|
||||
return buildRangeFilter(agg.params.field, filter, agg.vis.indexPattern);
|
||||
};
|
||||
|
||||
};
|
||||
});
|
36
src/kibana/components/agg_types/buckets/date_range.js
Normal file
36
src/kibana/components/agg_types/buckets/date_range.js
Normal file
|
@ -0,0 +1,36 @@
|
|||
define(function (require) {
|
||||
var moment = require('moment');
|
||||
var dateRange = require('utils/date_range');
|
||||
require('directives/validate_date_math');
|
||||
|
||||
return function DateRangeAggDefinition(Private, config) {
|
||||
var BucketAggType = Private(require('components/agg_types/buckets/_bucket_agg_type'));
|
||||
var createFilter = Private(require('components/agg_types/buckets/create_filter/date_range'));
|
||||
|
||||
return new BucketAggType({
|
||||
name: 'date_range',
|
||||
title: 'Date Range',
|
||||
createFilter: createFilter,
|
||||
getKey: function (bucket) {
|
||||
return dateRange.toString(bucket, config.get('dateFormat'));
|
||||
},
|
||||
makeLabel: function (aggConfig) {
|
||||
return aggConfig.params.field.displayName + ' date ranges';
|
||||
},
|
||||
params: [{
|
||||
name: 'field',
|
||||
filterFieldTypes: 'date',
|
||||
default: function (agg) {
|
||||
return agg.vis.indexPattern.timeFieldName;
|
||||
}
|
||||
}, {
|
||||
name: 'ranges',
|
||||
default: [{
|
||||
from: 'now-1w/w',
|
||||
to: 'now'
|
||||
}],
|
||||
editor: require('text!components/agg_types/controls/date_ranges.html')
|
||||
}]
|
||||
});
|
||||
};
|
||||
});
|
48
src/kibana/components/agg_types/controls/date_ranges.html
Normal file
48
src/kibana/components/agg_types/controls/date_ranges.html
Normal file
|
@ -0,0 +1,48 @@
|
|||
<div>
|
||||
<small><a target="_window" href="http://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-date-format.html#date-math">Accepted Date Formats <i class="fa-link fa"></i></a></small>
|
||||
|
||||
<table class="vis-editor-agg-editor-ranges form-group">
|
||||
<tr>
|
||||
<th>
|
||||
<label>From</label>
|
||||
</th>
|
||||
<th colspan="2">
|
||||
<label>To</label>
|
||||
</th>
|
||||
</tr>
|
||||
|
||||
<tr
|
||||
ng-repeat="range in agg.params.ranges track by $index">
|
||||
<td class="kbn-timepicker">
|
||||
<input
|
||||
ng-model="range.from"
|
||||
validate-date-math
|
||||
type="text"
|
||||
class="form-control"
|
||||
name="range.from" />
|
||||
</td>
|
||||
<td class="kbn-timepicker">
|
||||
<input
|
||||
ng-model="range.to"
|
||||
validate-date-math
|
||||
class="form-control"
|
||||
name="range.to" />
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
type="button"
|
||||
ng-click="agg.params.ranges.splice($index, 1)"
|
||||
class="btn btn-danger btn-xs">
|
||||
<i class="fa fa-ban" ></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div
|
||||
ng-click="agg.params.ranges.push({})"
|
||||
class="sidebar-item-button primary">
|
||||
Add Range
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -17,6 +17,7 @@ define(function (require) {
|
|||
Private(require('components/agg_types/buckets/date_histogram')),
|
||||
Private(require('components/agg_types/buckets/histogram')),
|
||||
Private(require('components/agg_types/buckets/range')),
|
||||
Private(require('components/agg_types/buckets/date_range')),
|
||||
Private(require('components/agg_types/buckets/ip_range')),
|
||||
Private(require('components/agg_types/buckets/terms')),
|
||||
Private(require('components/agg_types/buckets/filters')),
|
||||
|
|
|
@ -254,6 +254,10 @@ define(function (require) {
|
|||
return this.type.getValue(this, bucket);
|
||||
};
|
||||
|
||||
AggConfig.prototype.getKey = function (bucket, key) {
|
||||
return this.type.getKey(bucket, key);
|
||||
};
|
||||
|
||||
AggConfig.prototype.makeLabel = function () {
|
||||
if (!this.type) return '';
|
||||
return this.type.makeLabel(this);
|
||||
|
|
29
src/kibana/directives/validate_date_math.js
Normal file
29
src/kibana/directives/validate_date_math.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
define(function (require) {
|
||||
var _ = require('lodash');
|
||||
var DateMath = require('utils/datemath');
|
||||
|
||||
require('modules').get('kibana').directive('validateDateMath', function () {
|
||||
return {
|
||||
restrict: 'A',
|
||||
require: 'ngModel',
|
||||
scope: {
|
||||
'ngModel': '='
|
||||
},
|
||||
link: function ($scope, elem, attr, ngModel) {
|
||||
ngModel.$parsers.unshift(validateDateMath);
|
||||
ngModel.$formatters.unshift(validateDateMath);
|
||||
|
||||
function validateDateMath(input) {
|
||||
if (input == null || input === '') {
|
||||
ngModel.$setValidity('validDateMath', true);
|
||||
return null;
|
||||
}
|
||||
|
||||
var moment = DateMath.parse(input);
|
||||
ngModel.$setValidity('validDateMath', moment != null && moment.isValid());
|
||||
return input;
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
27
src/kibana/utils/date_range.js
Normal file
27
src/kibana/utils/date_range.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
define(function (require) {
|
||||
var moment = require('moment');
|
||||
|
||||
return {
|
||||
toString: function (range, format) {
|
||||
if (!range.from) {
|
||||
return 'Before ' + moment(range.to).format(format);
|
||||
} else if (!range.to) {
|
||||
return 'After ' + moment(range.from).format(format);
|
||||
} else {
|
||||
return moment(range.from).format(format) + ' to ' + moment(range.to).format(format);
|
||||
}
|
||||
},
|
||||
parse: function (rangeString, format) {
|
||||
var chunks = rangeString.split(' to ');
|
||||
if (chunks.length === 2) return {from: moment(chunks[0], format), to: moment(chunks[1], format)};
|
||||
|
||||
chunks = rangeString.split('Before ');
|
||||
if (chunks.length === 2) return {to: moment(chunks[1], format)};
|
||||
|
||||
chunks = rangeString.split('After ');
|
||||
if (chunks.length === 2) return {from: moment(chunks[1], format)};
|
||||
|
||||
throw new Error('Error attempting to parse date range: ' + rangeString);
|
||||
}
|
||||
};
|
||||
});
|
|
@ -9,8 +9,8 @@ define(function (require) {
|
|||
}));
|
||||
|
||||
var fixture = {};
|
||||
fixture.agg = { id: 'agg_2', name: 'test', schema: { group: 'buckets' },
|
||||
_next: { id: 'agg_3', name: 'example', schema: { group: 'buckets' } } };
|
||||
fixture.agg = { id: 'agg_2', name: 'test', schema: { group: 'buckets' }, getKey: function () {},
|
||||
_next: { id: 'agg_3', name: 'example', schema: { group: 'buckets' }, getKey: function () {} } };
|
||||
fixture.metric = { id: 'agg_1' };
|
||||
fixture.aggData = {
|
||||
buckets: [
|
||||
|
@ -20,7 +20,7 @@ define(function (require) {
|
|||
};
|
||||
|
||||
it('should return an array of objects with the doc_count as the size if the metric does not exist', function () {
|
||||
var agg = { id: 'agg_2', name: 'test', schema: { group: 'buckets' }};
|
||||
var agg = { id: 'agg_2', name: 'test', schema: { group: 'buckets' }, getKey: function () {}};
|
||||
var aggData = {
|
||||
buckets: [
|
||||
{ key: 'foo', doc_count: 1 },
|
||||
|
@ -36,7 +36,7 @@ define(function (require) {
|
|||
});
|
||||
|
||||
it('should return an array of objects with the metric agg value as the size', function () {
|
||||
var agg = { id: 'agg_2', name: 'test', schema: { group: 'buckets' } };
|
||||
var agg = { id: 'agg_2', name: 'test', schema: { group: 'buckets' }, getKey: function () {} };
|
||||
var aggData = {
|
||||
buckets: [
|
||||
{ key: 'foo', doc_count: 1, agg_1: { value: 0 } },
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
define(function (require) {
|
||||
var moment = require('moment');
|
||||
describe('AggConfig Filters', function () {
|
||||
describe('Date range', function () {
|
||||
var AggConfig;
|
||||
var indexPattern;
|
||||
var Vis;
|
||||
var createFilter;
|
||||
|
||||
beforeEach(module('kibana'));
|
||||
beforeEach(inject(function (Private) {
|
||||
Vis = Private(require('components/vis/vis'));
|
||||
AggConfig = Private(require('components/vis/_agg_config'));
|
||||
indexPattern = Private(require('fixtures/stubbed_logstash_index_pattern'));
|
||||
createFilter = Private(require('components/agg_types/buckets/create_filter/date_range'));
|
||||
}));
|
||||
|
||||
it('should return a range filter for date_range agg', function () {
|
||||
var vis = new Vis(indexPattern, {
|
||||
type: 'histogram',
|
||||
aggs: [
|
||||
{
|
||||
type: 'date_range',
|
||||
params: {
|
||||
field: '@timestamp',
|
||||
ranges: [
|
||||
{ from: '2014-01-01', to: '2014-12-31' }
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
var aggConfig = vis.aggs.byTypeName.date_range[0];
|
||||
var filter = createFilter(aggConfig, 'February 1st, 2015 to February 7th, 2015');
|
||||
expect(filter).to.have.property('range');
|
||||
expect(filter).to.have.property('meta');
|
||||
expect(filter.meta).to.have.property('index', indexPattern.id);
|
||||
expect(filter.range).to.have.property('@timestamp');
|
||||
expect(filter.range['@timestamp']).to.have.property('gte', +new Date('1 Feb 2015'));
|
||||
expect(filter.range['@timestamp']).to.have.property('lt', +new Date('7 Feb 2015'));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
68
test/unit/specs/directives/validate_date_math.js
Normal file
68
test/unit/specs/directives/validate_date_math.js
Normal file
|
@ -0,0 +1,68 @@
|
|||
define(function (require) {
|
||||
var angular = require('angular');
|
||||
require('directives/validate_date_math');
|
||||
|
||||
describe('Validate date math directive', function () {
|
||||
var $compile, $rootScope;
|
||||
var html = '<input type="text" ng-model="value" validate-date-math />';
|
||||
|
||||
beforeEach(module('kibana'));
|
||||
|
||||
beforeEach(inject(function (_$compile_, _$rootScope_) {
|
||||
$compile = _$compile_;
|
||||
$rootScope = _$rootScope_;
|
||||
}));
|
||||
|
||||
it('should allow valid date math', function () {
|
||||
var element = $compile(html)($rootScope);
|
||||
|
||||
$rootScope.value = 'now';
|
||||
$rootScope.$digest();
|
||||
expect(element.hasClass('ng-valid')).to.be.ok();
|
||||
|
||||
$rootScope.value = '2012-02-28';
|
||||
$rootScope.$digest();
|
||||
expect(element.hasClass('ng-valid')).to.be.ok();
|
||||
|
||||
$rootScope.value = 'now-3d';
|
||||
$rootScope.$digest();
|
||||
expect(element.hasClass('ng-valid')).to.be.ok();
|
||||
|
||||
$rootScope.value = 'now-3M/M';
|
||||
$rootScope.$digest();
|
||||
expect(element.hasClass('ng-valid')).to.be.ok();
|
||||
|
||||
$rootScope.value = '2012-05-31||-3M/M';
|
||||
$rootScope.$digest();
|
||||
expect(element.hasClass('ng-valid')).to.be.ok();
|
||||
});
|
||||
|
||||
it('should disallow invalid date math', function () {
|
||||
var element = $compile(html)($rootScope);
|
||||
|
||||
$rootScope.value = 'hello, world';
|
||||
$rootScope.$digest();
|
||||
expect(element.hasClass('ng-invalid')).to.be.ok();
|
||||
|
||||
$rootScope.value = 'now+-5w';
|
||||
$rootScope.$digest();
|
||||
expect(element.hasClass('ng-invalid')).to.be.ok();
|
||||
|
||||
$rootScope.value = '2012-02-31';
|
||||
$rootScope.$digest();
|
||||
expect(element.hasClass('ng-invalid')).to.be.ok();
|
||||
|
||||
$rootScope.value = '5/5/2005+3d';
|
||||
$rootScope.$digest();
|
||||
expect(element.hasClass('ng-invalid')).to.be.ok();
|
||||
});
|
||||
|
||||
it('should allow empty values', function () {
|
||||
var element = $compile(html)($rootScope);
|
||||
|
||||
$rootScope.value = '';
|
||||
$rootScope.$digest();
|
||||
expect(element.hasClass('ng-valid')).to.be.ok();
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue