Merge pull request #3262 from lukasolson/issues/3172

Add date range aggregation
This commit is contained in:
Joe Fleming 2015-03-31 10:25:57 -07:00
commit 3cef495a57
15 changed files with 294 additions and 10 deletions

View file

@ -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,

View file

@ -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;

View file

@ -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

View file

@ -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));
});
});
}

View file

@ -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;
};
});

View file

@ -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);
};
};
});

View 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')
}]
});
};
});

View 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>

View file

@ -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')),

View file

@ -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);

View 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;
}
}
};
});
});

View 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);
}
};
});

View file

@ -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 } },

View file

@ -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'));
});
});
});
});

View 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();
});
});
});