UNPKG

kibana-123

Version:

Kibana is an open source (Apache Licensed), browser based analytics and search dashboard for Elasticsearch. Kibana is a snap to setup and start using. Kibana strives to be easy to get started with, while also being flexible and powerful, just like Elastic

373 lines (323 loc) 10.2 kB
import _ from 'lodash'; import moment from 'moment'; import dateMath from '@elastic/datemath'; import parseInterval from 'ui/utils/parse_interval'; import TimeBucketsCalcAutoIntervalProvider from 'ui/time_buckets/calc_auto_interval'; import TimeBucketsCalcEsIntervalProvider from 'ui/time_buckets/calc_es_interval'; export default function IntervalHelperProvider(Private, timefilter, config) { let calcAuto = Private(TimeBucketsCalcAutoIntervalProvider); let calcEsInterval = Private(TimeBucketsCalcEsIntervalProvider); let tzOffset = moment().format('Z'); function isValidMoment(m) { return m && ('isValid' in m) && m.isValid(); } /** * Helper class for wrapping the concept of an "Interval", * which describes a timespan that will seperate moments. * * @param {state} object - one of "" * @param {[type]} display [description] */ function TimeBuckets() { return TimeBuckets.__cached__(this); } /**** * PUBLIC API ****/ /** * Set the bounds that these buckets are expected to cover. * This is required to support interval "auto" as well * as interval scaling. * * @param {object} input - an object with properties min and max, * representing the edges for the time span * we should cover * * @returns {undefined} */ TimeBuckets.prototype.setBounds = function (input) { if (!input) return this.clearBounds(); let bounds; if (_.isPlainObject(input)) { // accept the response from timefilter.getActiveBounds() bounds = [input.min, input.max]; } else { bounds = _.isArray(input) ? input : []; } let moments = _(bounds) .map(_.ary(moment, 1)) .sortBy(Number); let valid = moments.size() === 2 && moments.every(isValidMoment); if (!valid) { this.clearBounds(); throw new Error('invalid bounds set: ' + input); } this._lb = moments.shift(); this._ub = moments.pop(); if (this.getDuration().asSeconds() < 0) { throw new TypeError('Intervals must be positive'); } }; /** * Clear the stored bounds * * @return {undefined} */ TimeBuckets.prototype.clearBounds = function () { this._lb = this._ub = null; }; /** * Check to see if we have received bounds yet * * @return {Boolean} */ TimeBuckets.prototype.hasBounds = function () { return isValidMoment(this._ub) && isValidMoment(this._lb); }; /** * Return the current bounds, if we have any. * * THIS DOES NOT CLONE THE BOUNDS, so editing them * may have unexpected side-effects. Always * call bounds.min.clone() before editing * * @return {object|undefined} - If bounds are not defined, this * returns undefined, else it returns the bounds * for these buckets. This object has two props, * min and max. Each property will be a moment() * object * */ TimeBuckets.prototype.getBounds = function () { if (!this.hasBounds()) return; return { min: this._lb, max: this._ub }; }; /** * Get a moment duration object representing * the distance between the bounds, if the bounds * are set. * * @return {moment.duration|undefined} */ TimeBuckets.prototype.getDuration = function () { if (!this.hasBounds()) return; return moment.duration(this._ub - this._lb, 'ms'); }; /** * Update the interval at which buckets should be * generated. * * Input can be one of the following: * - Any object from src/ui/agg_types/buckets/_interval_options.js * - "auto" * - Pass a valid moment unit * - a moment.duration object. * * @param {object|string|moment.duration} input - see desc */ TimeBuckets.prototype.setInterval = function (input) { let interval = input; // selection object -> val if (_.isObject(input)) { interval = input.val; } if (!interval || interval === 'auto') { this._i = 'auto'; return; } if (_.isString(interval)) { input = interval; interval = parseInterval(interval); if (+interval === 0) { interval = null; } } // if the value wasn't converted to a duration, and isn't // already a duration, we have a problem if (!moment.isDuration(interval)) { throw new TypeError('"' + input + '" is not a valid interval.'); } this._i = interval; }; /** * Get the interval for the buckets. If the * number of buckets created by the interval set * is larger than config:histogram:maxBars then the * interval will be scaled up. If the number of buckets * created is less than one, the interval is scaled back. * * The interval object returned is a moment.duration * object that has been decorated with the following * properties. * * interval.description: a text description of the interval. * designed to be used list "field per {{ desc }}". * - "minute" * - "10 days" * - "3 years" * * interval.expr: the elasticsearch expression that creates this * interval. If the interval does not properly form an elasticsearch * expression it will be forced into one. * * interval.scaled: the interval was adjusted to * accomidate the maxBars setting. * * interval.scale: the numer that y-values should be * multiplied by * * interval.scaleDescription: a description that reflects * the values which will be produced by using the * interval.scale. * * * @return {[type]} [description] */ TimeBuckets.prototype.getInterval = function () { let self = this; let duration = self.getDuration(); return decorateInterval(maybeScaleInterval(readInterval())); // either pull the interval from state or calculate the auto-interval function readInterval() { let interval = self._i; if (moment.isDuration(interval)) return interval; return calcAuto.near(config.get('histogram:barTarget'), duration); } // check to see if the interval should be scaled, and scale it if so function maybeScaleInterval(interval) { if (!self.hasBounds()) return interval; let maxLength = config.get('histogram:maxBars'); let approxLen = duration / interval; let scaled; if (approxLen > maxLength) { scaled = calcAuto.lessThan(maxLength, duration); } else { return interval; } if (+scaled === +interval) return interval; decorateInterval(interval); return _.assign(scaled, { preScaled: interval, scale: interval / scaled, scaled: true }); } // append some TimeBuckets specific props to the interval function decorateInterval(interval) { let esInterval = calcEsInterval(interval); interval.esValue = esInterval.value; interval.esUnit = esInterval.unit; interval.expression = esInterval.expression; interval.overflow = duration > interval ? moment.duration(interval - duration) : false; let prettyUnits = moment.normalizeUnits(esInterval.unit); if (esInterval.value === 1) { interval.description = prettyUnits; } else { interval.description = esInterval.value + ' ' + prettyUnits + 's'; } return interval; } }; /** * Get a date format string that will represent dates that * progress at our interval. * * Since our interval can be as small as 1ms, the default * date format is usually way too much. with `dateFormat:scaled` * users can modify how dates are formatted within series * produced by TimeBuckets * * @return {string} */ TimeBuckets.prototype.getScaledDateFormat = function () { let interval = this.getInterval(); let rules = config.get('dateFormat:scaled'); for (let i = rules.length - 1; i >= 0; i--) { let rule = rules[i]; if (!rule[0] || interval >= moment.duration(rule[0])) { return rule[1]; } } return config.get('dateFormat'); }; TimeBuckets.__cached__ = function (self) { let cache = {}; let sameMoment = same(moment.isMoment); let sameDuration = same(moment.isDuration); let desc = { __cached__: { value: self }, }; let breakers = { setBounds: 'bounds', clearBounds: 'bounds', setInterval: 'interval' }; let resources = { bounds: { setup: function () { return [self._lb, self._ub]; }, changes: function (prev) { return !sameMoment(prev[0], self._lb) || !sameMoment(prev[1], self._ub); } }, interval: { setup: function () { return self._i; }, changes: function (prev) { return !sameDuration(prev, this._i); } } }; function cachedGetter(prop) { return { value: function cachedGetter() { if (cache.hasOwnProperty(prop)) { return cache[prop]; } return cache[prop] = self[prop](); } }; } function cacheBreaker(prop) { let resource = resources[breakers[prop]]; let setup = resource.setup; let changes = resource.changes; let deps = resource.deps; let fn = self[prop]; return { value: function cacheBreaker(input) { let prev = setup.call(self); let ret = fn.apply(self, arguments); if (changes.call(self, prev)) { cache = {}; } return ret; } }; } function same(checkType) { return function (a, b) { if (a === b) return true; if (checkType(a) === checkType(b)) return +a === +b; return false; }; } _.forOwn(TimeBuckets.prototype, function (fn, prop) { if (prop[0] === '_') return; if (breakers.hasOwnProperty(prop)) { desc[prop] = cacheBreaker(prop); } else { desc[prop] = cachedGetter(prop); } }); return Object.create(self, desc); }; return TimeBuckets; };