From 74e18e47d8eec752f74f39033082b86920a59dca Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Thu, 30 Aug 2018 12:41:41 +0200 Subject: [PATCH] Add option to drop partial buckets from date_histogram visuals (#19979) (#22528) * Add Drop Partials option to date histogram agg settings UI * Add timeRange to aggOpts and parse in _response_writer * Implement dropPartials method in TabifyBuckets * Fixed a couple issues * Fixed issue with undefined timeRange * Use braces for conditionals --- .../agg_response/tabify/__tests__/_buckets.js | 75 +++++++++++++++++++ .../tabify/__tests__/_response_writer.js | 29 +++++++ src/ui/public/agg_response/tabify/_buckets.js | 38 +++++++++- .../agg_response/tabify/_response_writer.js | 9 +++ src/ui/public/agg_response/tabify/tabify.js | 2 +- .../agg_types/buckets/date_histogram.js | 8 ++ .../agg_types/controls/drop_partials.html | 11 +++ src/ui/public/vis/response_handlers/basic.js | 6 +- src/ui/public/vis/response_handlers/tabify.js | 5 +- 9 files changed, 177 insertions(+), 6 deletions(-) create mode 100644 src/ui/public/agg_types/controls/drop_partials.html diff --git a/src/ui/public/agg_response/tabify/__tests__/_buckets.js b/src/ui/public/agg_response/tabify/__tests__/_buckets.js index 785a62ff82657..8be13c1ad6c12 100644 --- a/src/ui/public/agg_response/tabify/__tests__/_buckets.js +++ b/src/ui/public/agg_response/tabify/__tests__/_buckets.js @@ -80,4 +80,79 @@ describe('Buckets wrapper', function () { expect(buckets).to.have.length(1); }); }); + + describe('drop_partial option', function () { + const aggResp = { + buckets: [ + { key: 0, value: {} }, + { key: 100, value: {} }, + { key: 200, value: {} }, + { key: 300, value: {} } + ] + }; + + it('drops partial buckets when enabled', function () { + const aggParams = { + drop_partials: true, + field: { + name: 'date' + } + }; + const timeRange = { + gte: 150, + lte: 350, + name: 'date' + }; + const buckets = new TabifyBuckets(aggResp, aggParams, timeRange); + expect(buckets).to.have.length(1); + }); + + it('keeps partial buckets when disabled', function () { + const aggParams = { + drop_partials: false, + field: { + name: 'date' + } + }; + const timeRange = { + gte: 150, + lte: 350, + name: 'date' + }; + const buckets = new TabifyBuckets(aggResp, aggParams, timeRange); + expect(buckets).to.have.length(4); + }); + + it('keeps aligned buckets when enabled', function () { + const aggParams = { + drop_partials: true, + field: { + name: 'date' + } + }; + const timeRange = { + gte: 100, + lte: 400, + name: 'date' + }; + const buckets = new TabifyBuckets(aggResp, aggParams, timeRange); + expect(buckets).to.have.length(3); + }); + + it('does not drop buckets for non-timerange fields', function () { + const aggParams = { + drop_partials: true, + field: { + name: 'other_time' + } + }; + const timeRange = { + gte: 150, + lte: 350, + name: 'date' + }; + const buckets = new TabifyBuckets(aggResp, aggParams, timeRange); + expect(buckets).to.have.length(4); + }); + }); }); diff --git a/src/ui/public/agg_response/tabify/__tests__/_response_writer.js b/src/ui/public/agg_response/tabify/__tests__/_response_writer.js index 3835e4ceb3398..d9e04af7253db 100644 --- a/src/ui/public/agg_response/tabify/__tests__/_response_writer.js +++ b/src/ui/public/agg_response/tabify/__tests__/_response_writer.js @@ -97,6 +97,35 @@ describe('TabbedAggResponseWriter class', function () { expect(writer.splitStack).to.have.length(1); expect(writer.splitStack[0]).to.be(writer.root); }); + + describe('sets timeRange', function () { + it('to the first nested object\'s range', function () { + const vis = new Vis(indexPattern, { type: 'histogram', aggs: [] }); + const range = { + gte: 0, + lte: 100 + }; + + const writer = new TabbedAggResponseWriter(vis.getAggConfig(), { + timeRange: { + '@timestamp': range + } + }); + + expect(writer.timeRange.gte).to.be(range.gte); + expect(writer.timeRange.lte).to.be(range.lte); + expect(writer.timeRange.name).to.be('@timestamp'); + }); + + it('to undefined if no nested object', function () { + const vis = new Vis(indexPattern, { type: 'histogram', aggs: [] }); + + const writer = new TabbedAggResponseWriter(vis.getAggConfig(), { + timeRange: {} + }); + expect(writer).to.have.property('timeRange', undefined); + }); + }); }); describe('', function () { diff --git a/src/ui/public/agg_response/tabify/_buckets.js b/src/ui/public/agg_response/tabify/_buckets.js index 6beadefe5e5b9..846536b9831f9 100644 --- a/src/ui/public/agg_response/tabify/_buckets.js +++ b/src/ui/public/agg_response/tabify/_buckets.js @@ -19,7 +19,7 @@ import _ from 'lodash'; -function TabifyBuckets(aggResp, aggParams) { +function TabifyBuckets(aggResp, aggParams, timeRange) { if (_.has(aggResp, 'buckets')) { this.buckets = aggResp.buckets; } else if (aggResp) { @@ -38,7 +38,12 @@ function TabifyBuckets(aggResp, aggParams) { this.length = this.buckets.length; } - if (this.length && aggParams) this._orderBucketsAccordingToParams(aggParams); + if (this.length && aggParams) { + this._orderBucketsAccordingToParams(aggParams); + if (aggParams.drop_partials) { + this._dropPartials(aggParams, timeRange); + } + } } TabifyBuckets.prototype.forEach = function (fn) { @@ -75,10 +80,37 @@ TabifyBuckets.prototype._orderBucketsAccordingToParams = function (params) { ranges = params.ipRangeType === 'mask' ? ranges.mask : ranges.fromTo; } this.buckets = ranges.map(range => { - if (range.mask) return this.buckets.find(el => el.key === range.mask); + if (range.mask) { + return this.buckets.find(el => el.key === range.mask); + } return this.buckets.find(el => this._isRangeEqual(el, range)); }); } }; +// dropPartials should only be called if the aggParam setting is enabled, +// and the agg field is the same as the Time Range. +TabifyBuckets.prototype._dropPartials = function (params, timeRange) { + if (!timeRange || + this.buckets.length <= 1 || + this.objectMode || + params.field.name !== timeRange.name) { + return; + } + + const interval = this.buckets[1].key - this.buckets[0].key; + + this.buckets = this.buckets.filter(bucket => { + if (bucket.key < timeRange.gte) { + return false; + } + if (bucket.key + interval > timeRange.lte) { + return false; + } + return true; + }); + + this.length = this.buckets.length; +}; + export { TabifyBuckets }; diff --git a/src/ui/public/agg_response/tabify/_response_writer.js b/src/ui/public/agg_response/tabify/_response_writer.js index 1d0bd1c23863c..0b6d4cb947466 100644 --- a/src/ui/public/agg_response/tabify/_response_writer.js +++ b/src/ui/public/agg_response/tabify/_response_writer.js @@ -70,6 +70,15 @@ function TabbedAggResponseWriter(aggs, opts) { this.root = new TabifyTableGroup(); this.acrStack = []; this.splitStack = [this.root]; + + // Extract the time range object if provided + if (this.opts.timeRange) { + const timeRangeKey = Object.keys(this.opts.timeRange)[0]; + this.timeRange = this.opts.timeRange[timeRangeKey]; + if (this.timeRange) { + this.timeRange.name = timeRangeKey; + } + } } /** diff --git a/src/ui/public/agg_response/tabify/tabify.js b/src/ui/public/agg_response/tabify/tabify.js index 05ec560d5214a..7e91670d83fb3 100644 --- a/src/ui/public/agg_response/tabify/tabify.js +++ b/src/ui/public/agg_response/tabify/tabify.js @@ -49,7 +49,7 @@ function collectBucket(write, bucket, key, aggScale) { switch (agg.type.type) { case 'buckets': - const buckets = new TabifyBuckets(bucket[agg.id], agg.params); + const buckets = new TabifyBuckets(bucket[agg.id], agg.params, write.timeRange); if (buckets.length) { const splitting = write.canSplit && agg.schema.name === 'split'; if (splitting) { diff --git a/src/ui/public/agg_types/buckets/date_histogram.js b/src/ui/public/agg_types/buckets/date_histogram.js index fe93fe41f28e9..c712a34a95eaf 100644 --- a/src/ui/public/agg_types/buckets/date_histogram.js +++ b/src/ui/public/agg_types/buckets/date_histogram.js @@ -28,6 +28,7 @@ import { TimeBuckets } from '../../time_buckets'; import { createFilterDateHistogram } from './create_filter/date_histogram'; import { intervalOptions } from './_interval_options'; import intervalTemplate from '../controls/time_interval.html'; +import dropPartialTemplate from '../controls/drop_partials.html'; const config = chrome.getUiSettingsClient(); const detectedTimezone = tzDetect.determine().name(); @@ -147,6 +148,13 @@ export const dateHistogramBucketAgg = new BucketAggType({ return isDefaultTimezone ? detectedTimezone || tzOffset : config.get('dateFormat:tz'); }, }, + { + name: 'drop_partials', + default: false, + write: _.noop, + editor: dropPartialTemplate, + }, + { name: 'customInterval', default: '2h', diff --git a/src/ui/public/agg_types/controls/drop_partials.html b/src/ui/public/agg_types/controls/drop_partials.html new file mode 100644 index 0000000000000..3b3713db22f87 --- /dev/null +++ b/src/ui/public/agg_types/controls/drop_partials.html @@ -0,0 +1,11 @@ +
+ +
diff --git a/src/ui/public/vis/response_handlers/basic.js b/src/ui/public/vis/response_handlers/basic.js index d8ce9942a8898..42625fade7b59 100644 --- a/src/ui/public/vis/response_handlers/basic.js +++ b/src/ui/public/vis/response_handlers/basic.js @@ -19,6 +19,7 @@ import { AggResponseIndexProvider } from '../../agg_response'; import { TabifyTable } from '../../agg_response/tabify/_table'; +import { getTime } from 'ui/timefilter/get_time'; import { VisResponseHandlersRegistryProvider } from '../../registry/vis_response_handlers'; @@ -71,10 +72,13 @@ const BasicResponseHandlerProvider = function (Private) { resolve(aggResponse.hierarchical(vis, response)); } + const time = getTime(vis.indexPattern, vis.filters.timeRange); + const tableGroup = aggResponse.tabify(vis.getAggConfig(), response, { canSplit: true, asAggConfigResults: true, - isHierarchical: vis.isHierarchical() + isHierarchical: vis.isHierarchical(), + timeRange: time ? time.range : undefined }); let converted = convertTableGroup(vis, tableGroup); diff --git a/src/ui/public/vis/response_handlers/tabify.js b/src/ui/public/vis/response_handlers/tabify.js index 1b3cc3a1c3346..6a74fd2dbd728 100644 --- a/src/ui/public/vis/response_handlers/tabify.js +++ b/src/ui/public/vis/response_handlers/tabify.js @@ -20,6 +20,7 @@ import _ from 'lodash'; import { AggResponseIndexProvider } from '../../agg_response'; import { VisResponseHandlersRegistryProvider } from '../../registry/vis_response_handlers'; +import { getTime } from 'ui/timefilter/get_time'; const TabifyResponseHandlerProvider = function (Private) { const aggResponse = Private(AggResponseIndexProvider); @@ -28,11 +29,13 @@ const TabifyResponseHandlerProvider = function (Private) { name: 'tabify', handler: function (vis, response) { return new Promise((resolve) => { + const time = getTime(vis.indexPattern, vis.filters.timeRange); const tableGroup = aggResponse.tabify(vis.getAggConfig(), response, { canSplit: true, asAggConfigResults: _.get(vis, 'type.responseHandlerConfig.asAggConfigResults', false), - isHierarchical: vis.isHierarchical() + isHierarchical: vis.isHierarchical(), + timeRange: time ? time.range : undefined }); resolve(tableGroup);