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 @@ +
+ Accepted Date Formats + + + + + + + + + + + + +
+ + + +
+ + + + + +
+ + +
+ \ 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