diff --git a/src/kibana/components/agg_response/tabify/_get_columns.js b/src/kibana/components/agg_response/tabify/_get_columns.js index 3e9c63746b19e..cfa9fdfaeabbb 100644 --- a/src/kibana/components/agg_response/tabify/_get_columns.js +++ b/src/kibana/components/agg_response/tabify/_get_columns.js @@ -4,7 +4,8 @@ define(function (require) { var AggConfig = Private(require('components/vis/_agg_config')); return function getColumns(vis, minimal) { - var aggs = vis.aggs.getSorted(); + var aggs = vis.aggs.getResponseAggs(); + if (minimal == null) minimal = !vis.isHierarchical(); if (!vis.aggs.bySchemaGroup.metrics) { diff --git a/src/kibana/components/agg_response/tabify/tabify.js b/src/kibana/components/agg_response/tabify/tabify.js index 3e30402ab75de..bb9164560dccc 100644 --- a/src/kibana/components/agg_response/tabify/tabify.js +++ b/src/kibana/components/agg_response/tabify/tabify.js @@ -30,11 +30,10 @@ define(function (require) { */ function collectBucket(write, bucket, key) { var agg = write.aggStack.shift(); - var aggResp = bucket[agg.id]; switch (agg.schema.group) { case 'buckets': - var buckets = new Buckets(aggResp); + var buckets = new Buckets(bucket[agg.id]); if (buckets.length) { var splitting = write.canSplit && agg.schema.name === 'split'; if (splitting) { @@ -62,7 +61,7 @@ define(function (require) { } break; case 'metrics': - var value = (agg.type.name === 'count') ? bucketCount(bucket) : metricValue(aggResp); + var value = agg.getValue(bucket); write.cell(agg, value, function () { if (!write.aggStack.length) { // row complete diff --git a/src/kibana/components/agg_types/_agg_params.js b/src/kibana/components/agg_types/_agg_params.js index 2a8c21d1a3109..2fb59b8ff8b2c 100644 --- a/src/kibana/components/agg_types/_agg_params.js +++ b/src/kibana/components/agg_types/_agg_params.js @@ -33,14 +33,7 @@ define(function (require) { initialSet: params.map(function (config) { var type = config.name === 'field' ? config.name : config.type; var Class = paramTypeMap[type] || paramTypeMap._default; - var param = new Class(config); - - // recursively init sub params - if (param.params && !(params.params instanceof AggParams)) { - param.params = new AggParams(param.params); - } - - return param; + return new Class(config); }) }); } diff --git a/src/kibana/components/agg_types/_agg_type.js b/src/kibana/components/agg_types/_agg_type.js index f1cba5c42bc05..858f9fa13dc90 100644 --- a/src/kibana/components/agg_types/_agg_type.js +++ b/src/kibana/components/agg_types/_agg_type.js @@ -90,6 +90,18 @@ define(function (require) { this.params = new AggParams(this.params); } + + /** + * Designed for multi-value metric aggs, this method can return a + * set of AggConfigs that should replace this aggConfig in result sets + * that walk the AggConfig set. + * + * @method getResponseAggs + * @returns {array[AggConfig]|undefined} - an array of aggConfig objects + * that should replace this one, + * or undefined + */ + this.getResponseAggs = config.getResponseAggs || _.noop; } return AggType; diff --git a/src/kibana/components/agg_types/buckets/_bucket_agg_type.js b/src/kibana/components/agg_types/buckets/_bucket_agg_type.js new file mode 100644 index 0000000000000..fb936721954a3 --- /dev/null +++ b/src/kibana/components/agg_types/buckets/_bucket_agg_type.js @@ -0,0 +1,13 @@ +define(function (require) { + return function BucketAggTypeProvider(Private) { + var _ = require('lodash'); + var AggType = Private(require('components/agg_types/_agg_type')); + + _(BucketAggType).inherits(AggType); + function BucketAggType(config) { + BucketAggType.Super.call(this, config); + } + + return BucketAggType; + }; +}); \ No newline at end of file diff --git a/src/kibana/components/agg_types/buckets/_bucket_count_between.js b/src/kibana/components/agg_types/buckets/_bucket_count_between.js index 379dc09300e27..59b5ebab12585 100644 --- a/src/kibana/components/agg_types/buckets/_bucket_count_between.js +++ b/src/kibana/components/agg_types/buckets/_bucket_count_between.js @@ -14,7 +14,7 @@ define(function (require) { * @return {null|number} */ function bucketCountBetween(aggConfigA, aggConfigB) { - var aggs = aggConfigA.vis.aggs.getSorted(); + var aggs = aggConfigA.vis.aggs.getRequestAggs(); var aIndex = aggs.indexOf(aggConfigA); var bIndex = aggs.indexOf(aggConfigB); diff --git a/src/kibana/components/agg_types/buckets/date_histogram.js b/src/kibana/components/agg_types/buckets/date_histogram.js index a8f6dce46ddaf..a67d665fa0eed 100644 --- a/src/kibana/components/agg_types/buckets/date_histogram.js +++ b/src/kibana/components/agg_types/buckets/date_histogram.js @@ -3,7 +3,7 @@ define(function (require) { var _ = require('lodash'); var moment = require('moment'); var interval = require('utils/interval'); - var AggType = Private(require('components/agg_types/_agg_type')); + var BucketAggType = Private(require('components/agg_types/buckets/_bucket_agg_type')); var calculateInterval = Private(require('components/agg_types/param_types/_calculate_interval')); var createFilter = Private(require('components/agg_types/buckets/create_filter/date_histogram')); @@ -15,7 +15,7 @@ define(function (require) { return interval.calculate(bounds.min, bounds.max, targetBuckets); }; - return new AggType({ + return new BucketAggType({ name: 'date_histogram', title: 'Date Histogram', ordered: { diff --git a/src/kibana/components/agg_types/buckets/filters.js b/src/kibana/components/agg_types/buckets/filters.js index 352aa1fa7e10f..c7067ecbd49f0 100644 --- a/src/kibana/components/agg_types/buckets/filters.js +++ b/src/kibana/components/agg_types/buckets/filters.js @@ -1,12 +1,12 @@ define(function (require) { return function FiltersAggDefinition(Private, Notifier) { var _ = require('lodash'); - var AggType = Private(require('components/agg_types/_agg_type')); + var BucketAggType = Private(require('components/agg_types/buckets/_bucket_agg_type')); var createFilter = Private(require('components/agg_types/buckets/create_filter/filters')); var decorateQuery = Private(require('components/courier/data_source/_decorate_query')); var notif = new Notifier({ location: 'Filters Agg' }); - return new AggType({ + return new BucketAggType({ name: 'filters', title: 'Filters', createFilter: createFilter, diff --git a/src/kibana/components/agg_types/buckets/geo_hash.js b/src/kibana/components/agg_types/buckets/geo_hash.js index ba7de3557da70..638c014e0bdbd 100644 --- a/src/kibana/components/agg_types/buckets/geo_hash.js +++ b/src/kibana/components/agg_types/buckets/geo_hash.js @@ -2,7 +2,7 @@ define(function (require) { return function GeoHashAggDefinition(Private, config) { var _ = require('lodash'); var moment = require('moment'); - var AggType = Private(require('components/agg_types/_agg_type')); + var BucketAggType = Private(require('components/agg_types/buckets/_bucket_agg_type')); var defaultPrecision = 3; function getPrecision(precision) { @@ -21,7 +21,7 @@ define(function (require) { return precision; } - return new AggType({ + return new BucketAggType({ name: 'geohash_grid', title: 'Geohash', ordered: {}, diff --git a/src/kibana/components/agg_types/buckets/histogram.js b/src/kibana/components/agg_types/buckets/histogram.js index 702337186ae7e..aba79efd33384 100644 --- a/src/kibana/components/agg_types/buckets/histogram.js +++ b/src/kibana/components/agg_types/buckets/histogram.js @@ -2,10 +2,10 @@ define(function (require) { return function HistogramAggDefinition(Private) { var _ = require('lodash'); var moment = require('moment'); - var AggType = Private(require('components/agg_types/_agg_type')); + var BucketAggType = Private(require('components/agg_types/buckets/_bucket_agg_type')); var createFilter = Private(require('components/agg_types/buckets/create_filter/histogram')); - return new AggType({ + return new BucketAggType({ name: 'histogram', title: 'Histogram', ordered: {}, diff --git a/src/kibana/components/agg_types/buckets/range.js b/src/kibana/components/agg_types/buckets/range.js index 47775bfa7404f..a6433716602e4 100644 --- a/src/kibana/components/agg_types/buckets/range.js +++ b/src/kibana/components/agg_types/buckets/range.js @@ -3,10 +3,10 @@ define(function (require) { var _ = require('lodash'); var moment = require('moment'); var angular = require('angular'); - var AggType = Private(require('components/agg_types/_agg_type')); + var BucketAggType = Private(require('components/agg_types/buckets/_bucket_agg_type')); var createFilter = Private(require('components/agg_types/buckets/create_filter/range')); - return new AggType({ + return new BucketAggType({ name: 'range', title: 'Range', createFilter: createFilter, diff --git a/src/kibana/components/agg_types/buckets/significant_terms.js b/src/kibana/components/agg_types/buckets/significant_terms.js index 379961ef35d0f..57233333ebfd3 100644 --- a/src/kibana/components/agg_types/buckets/significant_terms.js +++ b/src/kibana/components/agg_types/buckets/significant_terms.js @@ -1,10 +1,10 @@ define(function (require) { return function SignificantTermsAggDefinition(Private) { var _ = require('lodash'); - var AggType = Private(require('components/agg_types/_agg_type')); + var BucketAggType = Private(require('components/agg_types/buckets/_bucket_agg_type')); var createFilter = Private(require('components/agg_types/buckets/create_filter/terms')); - return new AggType({ + return new BucketAggType({ name: 'significant_terms', title: 'Significant Terms', makeLabel: function (aggConfig) { diff --git a/src/kibana/components/agg_types/buckets/terms.js b/src/kibana/components/agg_types/buckets/terms.js index c9f4088de2457..22133ad900e44 100644 --- a/src/kibana/components/agg_types/buckets/terms.js +++ b/src/kibana/components/agg_types/buckets/terms.js @@ -1,11 +1,22 @@ define(function (require) { return function TermsAggDefinition(Private) { var _ = require('lodash'); - var AggType = Private(require('components/agg_types/_agg_type')); + var BucketAggType = Private(require('components/agg_types/buckets/_bucket_agg_type')); + var bucketCountBetween = Private(require('components/agg_types/buckets/_bucket_count_between')); var AggConfig = Private(require('components/vis/_agg_config')); + var Schemas = Private(require('plugins/vis_types/_schemas')); var createFilter = Private(require('components/agg_types/buckets/create_filter/terms')); - return new AggType({ + var orderAggSchema = (new Schemas([ + { + group: 'none', + name: 'orderAgg', + title: 'Order Agg', + aggFilter: '!percentiles' + } + ])).all[0]; + + return new BucketAggType({ name: 'terms', title: 'Terms', makeLabel: function (agg) { @@ -56,8 +67,15 @@ define(function (require) { serialize: function (orderAgg) { return orderAgg.toJSON(); }, - deserialize: function (stateJSON, aggConfig) { - return new AggConfig(aggConfig.vis, stateJSON); + deserialize: function (state, agg) { + return this.makeOrderAgg(agg, state); + }, + makeOrderAgg: function (termsAgg, state) { + state = state || {}; + state.schema = orderAggSchema; + var orderAgg = new AggConfig(termsAgg.vis, state); + orderAgg.id = termsAgg.id + '-orderAgg'; + return orderAgg; }, controller: function ($scope) { $scope.safeMakeLabel = function (agg) { @@ -68,43 +86,62 @@ define(function (require) { } }; - $scope.$watch('agg.params.orderBy', function (orderBy, prevOrderBy) { + var INIT = {}; // flag to know when prevOrderBy has changed + var prevOrderBy = INIT; + + $scope.$watch('responseValueAggs', updateOrderAgg); + $scope.$watch('agg.params.orderBy', updateOrderAgg); + + function updateOrderAgg() { var agg = $scope.agg; var aggs = agg.vis.aggs; var params = agg.params; + var orderBy = params.orderBy; + var paramDef = agg.type.params.byName.orderAgg; - if (orderBy === prevOrderBy && !orderBy) { - params.orderBy = (_.first(aggs.bySchemaGroup.metrics) || { id: 'custom' }).id; + // setup the initial value of orderBy + if (!orderBy && prevOrderBy === INIT) { + // abort until we get the responseValueAggs + if (!$scope.responseValueAggs) return; + params.orderBy = (_.first($scope.responseValueAggs) || { id: 'custom' }).id; return; } - if (!orderBy) return; - if (orderBy !== 'custom') { + // track the previous value + prevOrderBy = orderBy; + + // we aren't creating a custom aggConfig + if (!orderBy || orderBy !== 'custom') { params.orderAgg = null; + // ensure that orderBy is set to a valid agg + if (!_.find($scope.responseValueAggs, { id: orderBy })) { + params.orderBy = null; + } return; } - if (params.orderAgg) return; - params.orderAgg = new AggConfig(agg.vis, { - schema: _.first(agg.vis.type.schemas.metrics) - }); - }); + params.orderAgg = params.orderAgg || paramDef.makeOrderAgg(agg); + } }, write: function (agg, output) { + var vis = agg.vis; var dir = agg.params.order.val; var order = output.params.order = {}; - var orderAgg = agg.params.orderAgg; - if (!orderAgg) { - orderAgg = agg.vis.aggs.byId[agg.params.orderBy]; - } + var orderAgg = agg.params.orderAgg || vis.aggs.getResponseAggById(agg.params.orderBy); - if (orderAgg.type.name === 'count') { + if (!orderAgg || orderAgg.type.name === 'count') { order._count = dir; - } else { - output.subAggs = (output.subAggs || []).concat(orderAgg); - order[orderAgg.id] = dir; + return; } + + var orderAggId = orderAgg.id; + if (orderAgg.parentId) { + orderAgg = vis.aggs.byId[orderAgg.parentId]; + } + + output.subAggs = (output.subAggs || []).concat(orderAgg); + order[orderAggId] = dir; } } ] diff --git a/src/kibana/components/agg_types/controls/_percent_list.js b/src/kibana/components/agg_types/controls/_percent_list.js index 8f2e0cd442e77..4bce09e7d9953 100644 --- a/src/kibana/components/agg_types/controls/_percent_list.js +++ b/src/kibana/components/agg_types/controls/_percent_list.js @@ -1,54 +1,163 @@ define(function (require) { + var $ = require('jquery'); var _ = require('lodash'); + var keyMap = require('utils/key_map'); var INVALID = {}; // invalid flag var FLOATABLE = /^[\d\.e\-\+]+$/i; require('modules') .get('kibana') - .directive('percentList', function () { + .directive('percentList', function ($parse) { return { restrict: 'A', require: 'ngModel', - link: function ($scope, $el, attrs, ngModelCntr) { - function parse(viewValue) { - if (!_.isString(viewValue)) return INVALID; + link: function ($scope, $el, attrs, ngModelController) { + var $setModel = $parse(attrs.ngModel).assign; + var $repeater = $el.closest('[ng-repeat]'); + var $listGetter = $parse(attrs.percentList); + + var handlers = { + up: change(add, 1), + 'shift-up': change(addTenth, 1), + + down: change(add, -1), + 'shift-down': change(addTenth, -1), - var nums = _(viewValue.split(',')) - .invoke('trim') - .filter(Boolean) - .map(function (num) { - // prevent '100 boats' from passing - return FLOATABLE.test(num) ? parseFloat(num) : NaN; - }); + tab: go('next'), + 'shift-tab': go('prev'), - var ration = nums.none(_.isNaN); - var ord = ration && nums.isOrdinal(); - var range = ord && nums.min() >= 0 && nums.max() <= 100; + backspace: removeIfEmpty, + delete: removeIfEmpty + }; - return range ? nums.value() : INVALID; + function removeIfEmpty(event) { + if ($el.val() === '') { + $get('prev').focus(); + $scope.remove($scope.$index); + event.preventDefault(); + } + + return false; } - function makeString(list) { - if (!_.isArray(list)) return INVALID; - return list.join(', '); + function $get(dir) { + return $repeater[dir]().find('[percent-list]'); } - function converter(/* fns... */) { - var fns = _.toArray(arguments); - return function (input) { - var value = input; - var valid = fns.every(function (fn) { - return (value = fn(value)) !== INVALID; - }); + function go(dir) { + return function () { + var $to = $get(dir); + if ($to.size()) $to.focus(); + else return false; + }; + } + + function idKey(event) { + var id = []; + if (event.ctrlKey) id.push('ctrl'); + if (event.shiftKey) id.push('shift'); + if (event.metaKey) id.push('meta'); + if (event.altKey) id.push('alt'); + id.push(keyMap[event.keyCode] || event.keyCode); + return id.join('-'); + } + + function add(n, val) { + return parse(val + n); + } + + function addTenth(n, val, str) { + var int = Math.floor(val); + var dec = parseInt(str.split('.')[1] || 0, 10); + dec = dec + parseInt(n, 10); + + if (dec < 0 || dec > 9) { + int += Math.floor(dec / 10); + if (dec < 0) { + dec = 10 + (dec % 10); + } else { + dec = dec % 10; + } + } + + return parse(int + '.' + dec); + } + + function change(using, mod) { + return function () { + var str = String(ngModelController.$viewValue); + var val = parse(str); + if (val === INVALID) return; + + var next = using(mod, val, str); + if (next === INVALID) return; - ngModelCntr.$setValidity('listInput', valid); - return valid ? value : void 0; + $el.val(next); + ngModelController.$setViewValue(next); + }; + } + + function onKeydown(event) { + var handler = handlers[idKey(event)]; + if (!handler) return; + + if (handler(event) !== false) { + event.preventDefault(); + } + + $scope.$apply(); + } + + $el.on('keydown', onKeydown); + $scope.$on('$destroy', function () { + $el.off('keydown', onKeydown); + }); + + function parse(viewValue) { + viewValue = String(viewValue || 0); + var num = viewValue.trim(); + if (!FLOATABLE.test(num)) return INVALID; + num = parseFloat(num); + if (isNaN(num)) return INVALID; + + var list = $listGetter($scope); + var min = list[$scope.$index - 1] || 0; + var max = list[$scope.$index + 1] || 100; + + if (num <= min || num >= max) return INVALID; + + return num; + } + + $scope.$watchMulti([ + '$index', + { + fn: $scope.$watchCollection, + get: function () { + return $listGetter($scope); + } + } + ], function () { + var valid = parse(ngModelController.$viewValue) !== INVALID; + ngModelController.$setValidity('percentList', valid); + }); + + function validate(then) { + return function (input) { + var value = parse(input); + var valid = value !== INVALID; + value = valid ? value : void 0; + ngModelController.$setValidity('percentList', valid); + then && then(input, value); + return value; }; } - ngModelCntr.$parsers.push(converter(parse)); - ngModelCntr.$formatters.push(converter(makeString)); + ngModelController.$parsers.push(validate()); + ngModelController.$formatters.push(validate(function (input, value) { + if (input !== value) $setModel($scope, value); + })); } }; }); diff --git a/src/kibana/components/agg_types/controls/extended_bounds.html b/src/kibana/components/agg_types/controls/extended_bounds.html index 7502cb47033ee..b85a6c6f8f522 100644 --- a/src/kibana/components/agg_types/controls/extended_bounds.html +++ b/src/kibana/components/agg_types/controls/extended_bounds.html @@ -1,4 +1,4 @@ -
+ You mush specify at least one percentile +
+ + +