diff --git a/src/kibana/components/agg_response/hierarchical/_transform_aggregation.js b/src/kibana/components/agg_response/hierarchical/_transform_aggregation.js
index e9b1a63612c0..ab7c46c2d781 100644
--- a/src/kibana/components/agg_response/hierarchical/_transform_aggregation.js
+++ b/src/kibana/components/agg_response/hierarchical/_transform_aggregation.js
@@ -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,
diff --git a/src/kibana/components/agg_response/hierarchical/build_hierarchical_data.js b/src/kibana/components/agg_response/hierarchical/build_hierarchical_data.js
index 03a7a0bfdedb..2c56f778ac6a 100644
--- a/src/kibana/components/agg_response/hierarchical/build_hierarchical_data.js
+++ b/src/kibana/components/agg_response/hierarchical/build_hierarchical_data.js
@@ -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;
diff --git a/src/kibana/components/agg_response/tabify/_response_writer.js b/src/kibana/components/agg_response/tabify/_response_writer.js
index a6fc4e5c9917..c53dd261d833 100644
--- a/src/kibana/components/agg_response/tabify/_response_writer.js
+++ b/src/kibana/components/agg_response/tabify/_response_writer.js
@@ -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
diff --git a/src/kibana/components/agg_response/tabify/tabify.js b/src/kibana/components/agg_response/tabify/tabify.js
index 9e05c80cb412..3dd832d830c0 100644
--- a/src/kibana/components/agg_response/tabify/tabify.js
+++ b/src/kibana/components/agg_response/tabify/tabify.js
@@ -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));
});
});
}
diff --git a/src/kibana/components/agg_types/buckets/_bucket_agg_type.js b/src/kibana/components/agg_types/buckets/_bucket_agg_type.js
index fb936721954a..fd75346a1579 100644
--- a/src/kibana/components/agg_types/buckets/_bucket_agg_type.js
+++ b/src/kibana/components/agg_types/buckets/_bucket_agg_type.js
@@ -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;
};
});
\ No newline at end of file
diff --git a/src/kibana/components/agg_types/buckets/create_filter/date_range.js b/src/kibana/components/agg_types/buckets/create_filter/date_range.js
new file mode 100644
index 000000000000..db124d818fd8
--- /dev/null
+++ b/src/kibana/components/agg_types/buckets/create_filter/date_range.js
@@ -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);
+ };
+
+ };
+});
diff --git a/src/kibana/components/agg_types/buckets/date_range.js b/src/kibana/components/agg_types/buckets/date_range.js
new file mode 100644
index 000000000000..f5a8cc02559c
--- /dev/null
+++ b/src/kibana/components/agg_types/buckets/date_range.js
@@ -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')
+ }]
+ });
+ };
+});
\ No newline at end of file
diff --git a/src/kibana/components/agg_types/controls/date_ranges.html b/src/kibana/components/agg_types/controls/date_ranges.html
new file mode 100644
index 000000000000..2a9895717e3d
--- /dev/null
+++ b/src/kibana/components/agg_types/controls/date_ranges.html
@@ -0,0 +1,48 @@
+
+
\ No newline at end of file
diff --git a/src/kibana/components/agg_types/index.js b/src/kibana/components/agg_types/index.js
index 79fa63aeb2a0..4c6f7002e4a1 100644
--- a/src/kibana/components/agg_types/index.js
+++ b/src/kibana/components/agg_types/index.js
@@ -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')),
diff --git a/src/kibana/components/vis/_agg_config.js b/src/kibana/components/vis/_agg_config.js
index b72963c5f1f3..8f412283b64a 100644
--- a/src/kibana/components/vis/_agg_config.js
+++ b/src/kibana/components/vis/_agg_config.js
@@ -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);
diff --git a/src/kibana/directives/validate_date_math.js b/src/kibana/directives/validate_date_math.js
new file mode 100644
index 000000000000..2e9446866fc3
--- /dev/null
+++ b/src/kibana/directives/validate_date_math.js
@@ -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;
+ }
+ }
+ };
+ });
+});
\ No newline at end of file
diff --git a/src/kibana/utils/date_range.js b/src/kibana/utils/date_range.js
new file mode 100644
index 000000000000..93bfb7976a4f
--- /dev/null
+++ b/src/kibana/utils/date_range.js
@@ -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);
+ }
+ };
+});
\ No newline at end of file
diff --git a/test/unit/specs/components/agg_response/hierarchical/transform_aggregation.js b/test/unit/specs/components/agg_response/hierarchical/transform_aggregation.js
index ab9e6b82ec63..6709b326082a 100644
--- a/test/unit/specs/components/agg_response/hierarchical/transform_aggregation.js
+++ b/test/unit/specs/components/agg_response/hierarchical/transform_aggregation.js
@@ -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 } },
diff --git a/test/unit/specs/components/agg_types/buckets/create_filter/date_range.js b/test/unit/specs/components/agg_types/buckets/create_filter/date_range.js
new file mode 100644
index 000000000000..749858ca0abd
--- /dev/null
+++ b/test/unit/specs/components/agg_types/buckets/create_filter/date_range.js
@@ -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'));
+ });
+ });
+ });
+});
diff --git a/test/unit/specs/directives/validate_date_math.js b/test/unit/specs/directives/validate_date_math.js
new file mode 100644
index 000000000000..4ab0aaa45f1f
--- /dev/null
+++ b/test/unit/specs/directives/validate_date_math.js
@@ -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 = '';
+
+ 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();
+ });
+ });
+});
\ No newline at end of file