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 @@ -
+
@@ -7,7 +7,7 @@
Min (optional)
@@ -15,7 +15,7 @@
Max (optional)
diff --git a/src/kibana/components/agg_types/controls/order_agg.html b/src/kibana/components/agg_types/controls/order_agg.html index 5c0acee852e04..176d39e87636a 100644 --- a/src/kibana/components/agg_types/controls/order_agg.html +++ b/src/kibana/components/agg_types/controls/order_agg.html @@ -7,10 +7,10 @@ required class="form-control">
- + +
+ + + + +
+ + +

+ You mush specify at least one percentile +

+ + +
\ 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 0714d93d721e8..82d4703aae271 100644 --- a/src/kibana/components/agg_types/index.js +++ b/src/kibana/components/agg_types/index.js @@ -3,7 +3,15 @@ define(function (require) { var IndexedArray = require('utils/indexed_array/index'); var aggs = { - metrics: Private(require('components/agg_types/metric_aggs')), + metrics: [ + Private(require('components/agg_types/metrics/count')), + Private(require('components/agg_types/metrics/avg')), + Private(require('components/agg_types/metrics/sum')), + Private(require('components/agg_types/metrics/min')), + Private(require('components/agg_types/metrics/max')), + Private(require('components/agg_types/metrics/cardinality')), + Private(require('components/agg_types/metrics/percentiles')) + ], buckets: [ Private(require('components/agg_types/buckets/date_histogram')), Private(require('components/agg_types/buckets/histogram')), diff --git a/src/kibana/components/agg_types/metric_aggs.js b/src/kibana/components/agg_types/metric_aggs.js deleted file mode 100644 index 1d1b1c0ab3031..0000000000000 --- a/src/kibana/components/agg_types/metric_aggs.js +++ /dev/null @@ -1,82 +0,0 @@ -define(function (require) { - return function MetricAggsService(Private) { - var AggType = Private(require('components/agg_types/_agg_type')); - - return [ - { - name: 'count', - title: 'Count', - hasNoDsl: true, - makeLabel: function (aggConfig) { - return 'Count of documents'; - } - }, - { - name: 'avg', - title: 'Average', - makeLabel: function (aggConfig) { - return 'Average ' + aggConfig.params.field.displayName; - }, - params: [ - { - name: 'field', - filterFieldTypes: 'number' - } - ] - }, - { - name: 'sum', - title: 'Sum', - makeLabel: function (aggConfig) { - return 'Sum of ' + aggConfig.params.field.displayName; - }, - params: [ - { - name: 'field', - filterFieldTypes: 'number' - } - ] - }, - { - name: 'min', - title: 'Min', - makeLabel: function (aggConfig) { - return 'Min ' + aggConfig.params.field.displayName; - }, - params: [ - { - name: 'field', - filterFieldTypes: 'number' - } - ] - }, - { - name: 'max', - title: 'Max', - makeLabel: function (aggConfig) { - return 'Max ' + aggConfig.params.field.displayName; - }, - params: [ - { - name: 'field', - filterFieldTypes: 'number' - } - ] - }, - { - name: 'cardinality', - title: 'Unique count', - makeLabel: function (aggConfig) { - return 'Unique count of ' + aggConfig.params.field.displayName; - }, - params: [ - { - name: 'field' - } - ] - } - ].map(function (def) { - return new AggType(def); - }); - }; -}); \ No newline at end of file diff --git a/src/kibana/components/agg_types/metrics/_get_response_agg_config.js b/src/kibana/components/agg_types/metrics/_get_response_agg_config.js new file mode 100644 index 0000000000000..10c319f50fe61 --- /dev/null +++ b/src/kibana/components/agg_types/metrics/_get_response_agg_config.js @@ -0,0 +1,51 @@ +define(function (require) { + return function ResponseAggConfigProvider() { + var _ = require('lodash'); + + /** + * Get the ResponseAggConfig class for an aggConfig, + * which might be cached on the aggConfig or created. + * + * @param {AggConfig} agg - the AggConfig the VAC should inherit from + * @param {object} props - properties that the VAC should have + * @return {Constructor} - a constructor for VAC objects that will inherit the aggConfig + */ + return function getResponseConfigClass(agg, props) { + if (agg.$$_ResponseAggConfigClass) { + return agg.$$_ResponseAggConfigClass; + } else { + return (agg.$$_ResponseAggConfigClass = create(agg, props)); + } + }; + + function create(parentAgg, props) { + + /** + * AggConfig "wrapper" for multi-value metric aggs which + * need to modify AggConfig behavior for each value produced. + * + * @param {string|number} key - the key or index that identifies + * this part of the multi-value + */ + function ResponseAggConfig(key) { + this.key = key; + this.parentId = this.id; + + var subId = String(key); + if (subId.indexOf('.') > -1) { + this.id = this.parentId + '[\'' + subId.replace(/'/g, '\\\'') + '\']'; + } else { + this.id = this.parentId + '.' + subId; + } + } + + ResponseAggConfig.prototype = Object.create(parentAgg, { + constructor: ResponseAggConfig + }); + + _.assign(ResponseAggConfig.prototype, props); + + return ResponseAggConfig; + } + }; +}); \ No newline at end of file diff --git a/src/kibana/components/agg_types/metrics/_metric_agg_type.js b/src/kibana/components/agg_types/metrics/_metric_agg_type.js new file mode 100644 index 0000000000000..c37dd85af02f4 --- /dev/null +++ b/src/kibana/components/agg_types/metrics/_metric_agg_type.js @@ -0,0 +1,26 @@ +define(function (require) { + return function MetricAggTypeProvider(Private) { + var _ = require('lodash'); + var AggType = Private(require('components/agg_types/_agg_type')); + + _(MetricAggType).inherits(AggType); + function MetricAggType(config) { + MetricAggType.Super.call(this, config); + + if (_.isFunction(config.getValue)) { + this.getValue = config.getValue; + } + } + + /** + * Read the values for this metric from the + * @param {[type]} bucket [description] + * @return {[type]} [description] + */ + MetricAggType.prototype.getValue = function (agg, bucket) { + return bucket[agg.id].value; + }; + + return MetricAggType; + }; +}); \ No newline at end of file diff --git a/src/kibana/components/agg_types/metrics/avg.js b/src/kibana/components/agg_types/metrics/avg.js new file mode 100644 index 0000000000000..d010554cf27dc --- /dev/null +++ b/src/kibana/components/agg_types/metrics/avg.js @@ -0,0 +1,19 @@ +define(function (require) { + return function AggTypeMetricAvgProvider(Private) { + var MetricAggType = Private(require('components/agg_types/metrics/_metric_agg_type')); + + return new MetricAggType({ + name: 'avg', + title: 'Average', + makeLabel: function (aggConfig) { + return 'Average ' + aggConfig.params.field.displayName; + }, + params: [ + { + name: 'field', + filterFieldTypes: 'number' + } + ] + }); + }; +}); \ No newline at end of file diff --git a/src/kibana/components/agg_types/metrics/cardinality.js b/src/kibana/components/agg_types/metrics/cardinality.js new file mode 100644 index 0000000000000..9ed33ddc38f75 --- /dev/null +++ b/src/kibana/components/agg_types/metrics/cardinality.js @@ -0,0 +1,18 @@ +define(function (require) { + return function AggTypeMetricCardinalityProvider(Private) { + var MetricAggType = Private(require('components/agg_types/metrics/_metric_agg_type')); + + return new MetricAggType({ + name: 'cardinality', + title: 'Unique count', + makeLabel: function (aggConfig) { + return 'Unique count of ' + aggConfig.params.field.displayName; + }, + params: [ + { + name: 'field' + } + ] + }); + }; +}); \ No newline at end of file diff --git a/src/kibana/components/agg_types/metrics/count.js b/src/kibana/components/agg_types/metrics/count.js new file mode 100644 index 0000000000000..a9425d6a60bad --- /dev/null +++ b/src/kibana/components/agg_types/metrics/count.js @@ -0,0 +1,17 @@ +define(function (require) { + return function AggTypeMetricCountProvider(Private) { + var MetricAggType = Private(require('components/agg_types/metrics/_metric_agg_type')); + + return new MetricAggType({ + name: 'count', + title: 'Count', + hasNoDsl: true, + makeLabel: function (aggConfig) { + return 'Count of documents'; + }, + getValue: function (agg, bucket) { + return bucket.doc_count; + } + }); + }; +}); \ No newline at end of file diff --git a/src/kibana/components/agg_types/metrics/max.js b/src/kibana/components/agg_types/metrics/max.js new file mode 100644 index 0000000000000..ad56d0081552a --- /dev/null +++ b/src/kibana/components/agg_types/metrics/max.js @@ -0,0 +1,19 @@ +define(function (require) { + return function AggTypeMetricMaxProvider(Private) { + var MetricAggType = Private(require('components/agg_types/metrics/_metric_agg_type')); + + return new MetricAggType({ + name: 'max', + title: 'Max', + makeLabel: function (aggConfig) { + return 'Max ' + aggConfig.params.field.displayName; + }, + params: [ + { + name: 'field', + filterFieldTypes: 'number' + } + ] + }); + }; +}); \ No newline at end of file diff --git a/src/kibana/components/agg_types/metrics/min.js b/src/kibana/components/agg_types/metrics/min.js new file mode 100644 index 0000000000000..740cf0e05e1fb --- /dev/null +++ b/src/kibana/components/agg_types/metrics/min.js @@ -0,0 +1,19 @@ +define(function (require) { + return function AggTypeMetricMinProvider(Private) { + var MetricAggType = Private(require('components/agg_types/metrics/_metric_agg_type')); + + return new MetricAggType({ + name: 'min', + title: 'Min', + makeLabel: function (aggConfig) { + return 'Min ' + aggConfig.params.field.displayName; + }, + params: [ + { + name: 'field', + filterFieldTypes: 'number' + } + ] + }); + }; +}); \ No newline at end of file diff --git a/src/kibana/components/agg_types/metrics/percentiles.js b/src/kibana/components/agg_types/metrics/percentiles.js new file mode 100644 index 0000000000000..f5958e8728ae3 --- /dev/null +++ b/src/kibana/components/agg_types/metrics/percentiles.js @@ -0,0 +1,62 @@ +define(function (require) { + return function AggTypeMetricPercentilesProvider(Private) { + var _ = require('lodash'); + + var MetricAggType = Private(require('components/agg_types/metrics/_metric_agg_type')); + var getResponseAggConfig = Private(require('components/agg_types/metrics/_get_response_agg_config')); + var ordinalSuffix = require('utils/ordinal_suffix'); + + require('components/agg_types/controls/_percent_list'); + var percentEditor = require('text!components/agg_types/controls/percents.html'); + + var valueProps = { + makeLabel: function () { + return ordinalSuffix(this.key) + ' percentile of ' + this.fieldDisplayName(); + } + }; + + return new MetricAggType({ + name: 'percentiles', + title: 'Percentiles', + makeLabel: function (agg) { + return 'Percentiles of ' + agg.fieldDisplayName(); + }, + params: [ + { + name: 'field', + filterFieldTypes: 'number' + }, + { + name: 'percents', + editor: percentEditor, + default: [1, 5, 25, 50, 75, 95, 99], + controller: function ($scope) { + $scope.remove = function (index) { + $scope.agg.params.percents.splice(index, 1); + }; + + $scope.add = function () { + $scope.agg.params.percents.push(_.last($scope.agg.params.percents) + 1); + }; + + $scope.$watchCollection('agg.params.percents', function (percents) { + $scope.validLength = _.size(percents) || null; + }); + } + } + ], + getResponseAggs: function (agg) { + var ValueAggConfig = getResponseAggConfig(agg, valueProps); + + return agg.params.percents.map(function (percent) { + return new ValueAggConfig(percent); + }); + }, + getValue: function (agg, bucket) { + return _.find(bucket[agg.parentId].values, function (value, key) { + return agg.key === parseFloat(key); + }); + } + }); + }; +}); \ No newline at end of file diff --git a/src/kibana/components/agg_types/metrics/sum.js b/src/kibana/components/agg_types/metrics/sum.js new file mode 100644 index 0000000000000..d7e5f0bc7d0cd --- /dev/null +++ b/src/kibana/components/agg_types/metrics/sum.js @@ -0,0 +1,19 @@ +define(function (require) { + return function AggTypeMetricSumProvider(Private) { + var MetricAggType = Private(require('components/agg_types/metrics/_metric_agg_type')); + + return new MetricAggType({ + name: 'sum', + title: 'Sum', + makeLabel: function (aggConfig) { + return 'Sum of ' + aggConfig.params.field.displayName; + }, + params: [ + { + name: 'field', + filterFieldTypes: 'number' + } + ] + }); + }; +}); \ No newline at end of file diff --git a/src/kibana/components/bind.js b/src/kibana/components/bind.js new file mode 100644 index 0000000000000..7733f3d82fc58 --- /dev/null +++ b/src/kibana/components/bind.js @@ -0,0 +1,34 @@ +define(function (require) { + var _ = require('lodash'); + + require('modules').get('kibana') + .config(function ($provide) { + + $provide.decorator('$rootScope', function ($delegate, $parse) { + /** + * Two-way bind a value from scope to another property on scope. This + * allow values on scope that work like they do in an isolate scope, but + * without requiring one. + * + * @param {expression} to - the location on scope to bind to + * @param {expression} from - the location on scope to bind from + * @return {undefined} + */ + $delegate.constructor.prototype.$bind = function (to, from) { + var $source = this.$parent; + var $target = this; + + var getter = $parse(from); + var setter = $parse(to).assign; + + setter($target, getter($source)); + this.$watch( + function () { return getter($source); }, + function (val) { setter($target, val); } + ); + }; + + return $delegate; + }); + }); +}); \ No newline at end of file diff --git a/src/kibana/components/vis/_agg_config.js b/src/kibana/components/vis/_agg_config.js index 386f5c45aa73a..2ba030e5b591c 100644 --- a/src/kibana/components/vis/_agg_config.js +++ b/src/kibana/components/vis/_agg_config.js @@ -6,7 +6,7 @@ define(function (require) { function AggConfig(vis, opts) { var self = this; - self.id = opts.id || AggConfig.nextId(vis.aggs); + self.id = String(opts.id || AggConfig.nextId(vis.aggs)); self.vis = vis; self._opts = opts = (opts || {}); @@ -42,7 +42,7 @@ define(function (require) { var nextId = AggConfig.nextId(have); haveNot.forEach(function (obj) { - obj.id = nextId++; + obj.id = String(nextId++); }); return list; @@ -55,7 +55,7 @@ define(function (require) { */ AggConfig.nextId = function (list) { return 1 + list.reduce(function (max, obj) { - return Math.max(max, obj.id || 0); + return Math.max(max, +obj.id || 0); }, 0); }; @@ -189,6 +189,15 @@ define(function (require) { ); }; + AggConfig.prototype.getResponseAggs = function () { + if (!this.type) return; + return this.type.getResponseAggs(this) || this; + }; + + AggConfig.prototype.getValue = function (bucket) { + return this.type.getValue(this, bucket); + }; + AggConfig.prototype.makeLabel = function () { if (!this.type) return ''; return this.type.makeLabel(this); diff --git a/src/kibana/components/vis/_agg_configs.js b/src/kibana/components/vis/_agg_configs.js index 54fa834c69ee5..51c4ae9e08b00 100644 --- a/src/kibana/components/vis/_agg_configs.js +++ b/src/kibana/components/vis/_agg_configs.js @@ -64,7 +64,7 @@ define(function (require) { }); } - this.getSorted() + this.getRequestAggs() .filter(function (config) { return !config.type.hasNoDsl; }) @@ -100,12 +100,47 @@ define(function (require) { return dslTopLvl; }; - AggConfigs.prototype.getSorted = function () { + AggConfigs.prototype.getRequestAggs = function () { return _.sortBy(this, function (agg) { return agg.schema.group === 'metrics' ? 1 : 0; }); }; + /** + * Gets the AggConfigs (and possibly ResponseAggConfigs) that + * represent the values that will be produced when all aggs + * are run. + * + * With multi-value metric aggs it is possible for a single agg + * request to result in multiple agg values, which is why the length + * of a vis' responseValuesAggs may be different than the vis' aggs + * + * @return {array[AggConfig]} + */ + AggConfigs.prototype.getResponseAggs = function () { + return this.getRequestAggs().reduce(function (responseValuesAggs, agg) { + var aggs = agg.getResponseAggs(); + return aggs ? responseValuesAggs.concat(aggs) : responseValuesAggs; + }, []); + }; + + + /** + * Find a response agg by it's id. This may be an agg in the aggConfigs, or one + * created specifically for a response value + * + * @param {string} id - the id of the agg to find + * @return {AggConfig} + */ + AggConfigs.prototype.getResponseAggById = function (id) { + id = String(id); + var reqAgg = _.find(this.getRequestAggs(), function (agg) { + return id.substr(0, String(agg.id).length) === agg.id; + }); + if (!reqAgg) return; + return _.find(reqAgg.getResponseAggs(), { id: id }); + }; + return AggConfigs; }; }); diff --git a/src/kibana/components/watch_multi.js b/src/kibana/components/watch_multi.js index ed1195e2c96bf..1e1498b23cc55 100644 --- a/src/kibana/components/watch_multi.js +++ b/src/kibana/components/watch_multi.js @@ -10,41 +10,82 @@ define(function (require) { * with making code simpler it also merges all of the watcher * handlers within a single tick. * - * @param {array[string|function]} expressions - the list of expressions to $watch + * # expression format + * expressions can be specified in one of the following ways: + * 1. string that evaluates to a value on scope. Creates a regular $watch + * expression. + * 'someScopeValue.prop' === $scope.$watch('someScopeValue.prop', fn); + * + * 2. #1 prefixed with '[]', which uses $watchCollection rather than $watch. + * '[]expr' === $scope.$watchCollection('expr', fn); + * + * 3. #1 prefixed with '=', which uses $watch with objectEquality turned on + * '=expr' === $scope.$watch('expr', fn, true); + * + * 4. a function that will be called, like a normal function water + * + * 5. an object with any of the properties: + * `get`: the getter called on each itteration + * `deep`: a flag to turn on objectEquality in $watch + * `fn`: the watch registration function ($scope.$watch or $scope.$watchCollection) + * + * @param {array[string|function|obj]} expressions - the list of expressions to $watch * @param {Function} fn - the callback function * @param {boolean} deep - should the watchers be created as deep watchers? * @return {undefined} */ $delegate.constructor.prototype.$watchMulti = function (expressions, fn, deep) { if (!_.isArray(expressions)) throw new TypeError('expected an array of expressions to watch'); - if (!_.isFunction(fn)) throw new TypeError('expexted a function that is triggered on each watch'); + if (!_.isFunction(fn)) throw new TypeError('expected a function that is triggered on each watch'); var $scope = this; - var initQueue = _.clone(expressions); var fired = false; - var vals = { - new: new Array(expressions.length), - old: new Array(expressions.length) - }; + var queue = []; + var vals = new Array(expressions.length); + var prev = new Array(expressions.length); + + function normalizeExpression(expr) { + if (!expr) return; + + var norm = { + fn: $scope.$watch, + deep: false + }; + + if (_.isFunction(expr)) return _.assign(norm, { get: expr }); + if (_.isObject(expr)) return _.assign(norm, expr); + if (!_.isString(expr)) return; + + if (expr.substr(0, 2) === '[]') { + return _.assign(norm, { + fn: $scope.$watchCollection, + get: expr.substr(2) + }); + } + + if (expr.charAt(0) === '=') { + return _.assign(norm, { + deep: true, + get: expr.substr(1) + }); + } + + return _.assign(norm, { get: expr }); + } expressions.forEach(function (expr, i) { - $scope.$watch(expr, function (newVal, oldVal) { - vals.new[i] = newVal; - - if (initQueue) { - vals.old[i] = oldVal; - - var qIdx = initQueue.indexOf(expr); - if (qIdx !== -1) initQueue.splice(qIdx, 1); - if (initQueue.length === 0) { - initQueue = false; - if (fn.length) { - fn(vals.new.slice(0), vals.old.slice(0)); - } else { - fn(); - } - } - return; + expr = normalizeExpression(expr); + if (!expr) return; + + queue.push(expr); + expr.fn.call($scope, expr.get, function (newVal, oldVal) { + vals[i] = newVal; + + if (queue) { + prev[i] = oldVal; + _.pull(queue, expr); + if (queue.length > 0) return; + queue = false; } if (fired) return; @@ -53,16 +94,16 @@ define(function (require) { fired = false; if (fn.length) { - fn(vals.new.slice(0), vals.old.slice(0)); + fn(vals.slice(0), prev.slice(0)); } else { fn(); } - for (var i = 0; i < vals.new.length; i++) { - vals.old[i] = vals.new[i]; + for (var i = 0; i < vals.length; i++) { + prev[i] = vals[i]; } }); - }); + }, expr.deep); }); }; diff --git a/src/kibana/plugins/kibana/index.js b/src/kibana/plugins/kibana/index.js index e532cd058250c..e20f743dfa04f 100644 --- a/src/kibana/plugins/kibana/index.js +++ b/src/kibana/plugins/kibana/index.js @@ -14,6 +14,7 @@ define(function (require) { require('components/tooltip/tooltip'); require('components/style_compile/style_compile'); require('components/watch_multi'); + require('components/bind'); require('components/listen'); require('directives/click_focus'); require('directives/info'); diff --git a/src/kibana/plugins/vis_types/controls/rows_or_columns.html b/src/kibana/plugins/vis_types/controls/rows_or_columns.html index 2c83de4b71849..a2394ae72de06 100644 --- a/src/kibana/plugins/vis_types/controls/rows_or_columns.html +++ b/src/kibana/plugins/vis_types/controls/rows_or_columns.html @@ -3,14 +3,14 @@ diff --git a/src/kibana/plugins/visualize/editor/agg.html b/src/kibana/plugins/visualize/editor/agg.html index 1fea05ee55f63..56a1c0e732d49 100644 --- a/src/kibana/plugins/visualize/editor/agg.html +++ b/src/kibana/plugins/visualize/editor/agg.html @@ -1,4 +1,4 @@ - +
@@ -30,7 +30,7 @@
\ No newline at end of file diff --git a/src/kibana/plugins/visualize/editor/agg_add.js b/src/kibana/plugins/visualize/editor/agg_add.js new file mode 100644 index 0000000000000..4540cee0314e3 --- /dev/null +++ b/src/kibana/plugins/visualize/editor/agg_add.js @@ -0,0 +1,28 @@ +define(function (require) { + require('modules') + .get('kibana') + .directive('visEditorAggAdd', function (Private) { + var AggConfig = Private(require('components/vis/_agg_config')); + + return { + restrict: 'E', + template: require('text!plugins/visualize/editor/agg_add.html'), + controllerAs: 'add', + controller: function ($scope) { + var self = this; + + self.form = false; + self.submit = function (schema) { + self.form = false; + + var aggConfig = new AggConfig($scope.vis, { + schema: schema + }); + aggConfig.brandNew = true; + + $scope.vis.aggs.push(aggConfig); + }; + } + }; + }); +}); \ No newline at end of file diff --git a/src/kibana/plugins/visualize/editor/agg_group.html b/src/kibana/plugins/visualize/editor/agg_group.html index 23b0dcb0e4568..b3dcbe9aa2bc0 100644 --- a/src/kibana/plugins/visualize/editor/agg_group.html +++ b/src/kibana/plugins/visualize/editor/agg_group.html @@ -14,42 +14,9 @@ - - +
-
- - -
- - +
\ No newline at end of file diff --git a/src/kibana/plugins/visualize/editor/agg_group.js b/src/kibana/plugins/visualize/editor/agg_group.js index c970dc4a49d61..372e75ac592e0 100644 --- a/src/kibana/plugins/visualize/editor/agg_group.js +++ b/src/kibana/plugins/visualize/editor/agg_group.js @@ -3,29 +3,21 @@ define(function (require) { .get('app/visualize') .directive('visEditorAggGroup', function (Private) { require('plugins/visualize/editor/agg'); + require('plugins/visualize/editor/agg_add'); require('plugins/visualize/editor/nesting_indicator'); - var eachGroupHtml = require('text!plugins/visualize/editor/agg_group.html'); - var AggConfig = Private(require('components/vis/_agg_config')); - return { restrict: 'E', template: require('text!plugins/visualize/editor/agg_group.html'), - replace: true, - scope: { - vis: '=', - schemas: '=', - group: '=', - groupName: '=' - }, - link: function ($scope) { - - // "sub-scope" for the add form to use - $scope.addForm = {}; + scope: true, + link: function ($scope, $el, attr) { + $scope.groupName = attr.groupName; + $scope.$bind('group', 'vis.aggs.bySchemaGroup["' + $scope.groupName + '"]'); + $scope.$bind('schemas', 'vis.type.schemas["' + $scope.groupName + '"]'); $scope.$watchMulti([ 'schemas', - 'group.length' + '[]group' ], function () { var stats = $scope.stats = { min: 0, @@ -39,9 +31,7 @@ define(function (require) { stats.min += schema.min; stats.max += schema.max; }); - }); - $scope.$watchCollection('group', function () { $scope.availableSchema = $scope.schemas.filter(function (schema) { var count = 0; @@ -55,16 +45,6 @@ define(function (require) { if (count < schema.max) return true; }); }); - - $scope.createUsingSchema = function (schema) { - $scope.addForm = {}; - - var aggConfig = new AggConfig($scope.vis, { - schema: schema - }); - aggConfig.brandNew = true; - $scope.vis.aggs.push(aggConfig); - }; } }; diff --git a/src/kibana/plugins/visualize/editor/agg_params.js b/src/kibana/plugins/visualize/editor/agg_params.js index 3d2b4bd812209..0f2ca8713090d 100644 --- a/src/kibana/plugins/visualize/editor/agg_params.js +++ b/src/kibana/plugins/visualize/editor/agg_params.js @@ -8,9 +8,8 @@ define(function (require) { var aggSelectHtml = require('text!plugins/visualize/editor/agg_select.html'); var advancedToggleHtml = require('text!plugins/visualize/editor/advanced_toggle.html'); require('angular-ui-select'); - - require('plugins/visualize/editor/agg_param'); require('filters/match_any'); + require('plugins/visualize/editor/agg_param'); var notify = new Notifier({ location: 'visAggGroup' @@ -19,11 +18,11 @@ define(function (require) { return { restrict: 'E', template: require('text!plugins/visualize/editor/agg_params.html'), - scope: { - agg: '=', - groupName: '=' - }, - link: function ($scope, $el) { + scope: true, + link: function ($scope, $el, attr) { + $scope.$bind('agg', attr.agg); + $scope.$bind('groupName', attr.groupName); + $scope.aggTypeOptions = aggTypes.byType[$scope.groupName]; $scope.advancedToggled = false; @@ -33,7 +32,7 @@ define(function (require) { if ($scope.agg.schema.editor) { $schemaEditor.append($scope.agg.schema.editor); - $compile($schemaEditor)(editorScope()); + $compile($schemaEditor)($scope.$new()); } // allow selection of an aggregation @@ -112,7 +111,6 @@ define(function (require) { } var attrs = {}; - attrs['agg-param'] = 'agg.type.params[' + idx + ']'; if (param.advanced) { attrs['ng-show'] = 'advancedToggled'; @@ -137,17 +135,6 @@ define(function (require) { return fields; } - // generic child scope creation, for both schema and agg - function editorScope() { - var $editorScope = $scope.$new(); - - setupBoundProp($editorScope, 'agg.type', 'aggType'); - setupBoundProp($editorScope, 'agg', 'aggConfig'); - setupBoundProp($editorScope, 'agg.params', 'params'); - - return $editorScope; - } - // bind a property from our scope a child scope, with one-way binding function setupBoundProp($child, get, set) { var getter = _.partial($parse(get), $scope); diff --git a/src/kibana/plugins/visualize/editor/editor.html b/src/kibana/plugins/visualize/editor/editor.html index 4d933ed742185..94621dac30b8a 100644 --- a/src/kibana/plugins/visualize/editor/editor.html +++ b/src/kibana/plugins/visualize/editor/editor.html @@ -97,12 +97,7 @@
- - +
diff --git a/src/kibana/plugins/visualize/editor/editor.js b/src/kibana/plugins/visualize/editor/editor.js index 5503e1f6cda98..18552ee2f96b3 100644 --- a/src/kibana/plugins/visualize/editor/editor.js +++ b/src/kibana/plugins/visualize/editor/editor.js @@ -138,6 +138,17 @@ define(function (require) { return editableVis.getState(); }, function (newState) { editableVis.dirty = !angular.equals(newState, vis.getState()); + + $scope.responseValueAggs = null; + try { + $scope.responseValueAggs = editableVis.aggs.getResponseAggs().filter(function (agg) { + return _.deepGet(agg, 'schema.group') === 'metrics'; + }); + } catch (e) { + // this can fail when the agg.type is changed but the + // params have not been set yet. watcher will trigger again + // when the params update + } }, true); $state.replace(); diff --git a/src/kibana/plugins/visualize/editor/nesting_indicator.js b/src/kibana/plugins/visualize/editor/nesting_indicator.js index c7c379362f3ec..c27c6e336ead0 100644 --- a/src/kibana/plugins/visualize/editor/nesting_indicator.js +++ b/src/kibana/plugins/visualize/editor/nesting_indicator.js @@ -3,24 +3,8 @@ define(function (require) { .get('kibana') .directive('nestingIndicator', function ($rootScope, $parse, Private) { var _ = require('lodash'); - var angular = require('angular'); - var ruleBase = 'border-left-'; - - var getColor = (function () { - var i = 0; - var colorPool = Private(require('components/vislib/components/color/color_palette'))(100); - var assigned = {}; - return function (item) { - var key = item.id || item.$$hashKey; - if (!key) throw new Error('expected an item that is part of an ngRepeat'); - - if (!assigned[key]) { - assigned[key] = colorPool[i++ % colorPool.length]; - } - - return assigned[key]; - }; - }()); + var $ = require('jquery'); + var getColors = Private(require('components/vislib/components/color/color_palette')); return { restrict: 'E', @@ -29,27 +13,18 @@ define(function (require) { list: '=' }, link: function ($scope, $el, attr) { - $scope.$watchCollection('list', function () { if (!$scope.list || !$scope.item) return; var item = $scope.item; - var list = $scope.list; - var bars = $scope.bars = []; - - for (var i = 0; i <= list.length; i++) { - var color = getColor(list[i]); - - bars.push( - angular - .element('') - .css('background-color', color) - ); - - if (list[i] === $scope.item) break; - } - - $el.html(bars); + var index = $scope.list.indexOf($scope.item); + var bars = $scope.list.slice(0, index + 1); + var colors = getColors(bars.length); + + $el.html(bars.map(function (item, i) { + return $(document.createElement('span')) + .css('background-color', colors[i]); + })); }); } }; diff --git a/src/kibana/plugins/visualize/editor/sidebar.html b/src/kibana/plugins/visualize/editor/sidebar.html index f30f85bf0a663..99200a1784894 100644 --- a/src/kibana/plugins/visualize/editor/sidebar.html +++ b/src/kibana/plugins/visualize/editor/sidebar.html @@ -1,55 +1,37 @@ -
- \ No newline at end of file diff --git a/src/kibana/plugins/visualize/editor/sidebar.js b/src/kibana/plugins/visualize/editor/sidebar.js index 085fb7b6caf1f..ae3145617665a 100644 --- a/src/kibana/plugins/visualize/editor/sidebar.js +++ b/src/kibana/plugins/visualize/editor/sidebar.js @@ -10,14 +10,10 @@ define(function (require) { return { restrict: 'E', template: require('text!plugins/visualize/editor/sidebar.html'), - replace: true, - scope: { - vis: '=', - savedVis: '=', - apply: '&', - reset: '&' - }, + scope: true, link: function ($scope) { + $scope.$bind('vis', 'editableVis'); + $scope.hideErrors = true; $scope.dontApply = function () { $scope.hideErrors = false; diff --git a/src/kibana/plugins/visualize/editor/styles/editor.less b/src/kibana/plugins/visualize/editor/styles/editor.less index 7c4353173fe47..2ce260eb544f8 100644 --- a/src/kibana/plugins/visualize/editor/styles/editor.less +++ b/src/kibana/plugins/visualize/editor/styles/editor.less @@ -127,6 +127,7 @@ .display(flex); .align-items(center); .flex(1, 0, auto); + margin-bottom: @vis-editor-agg-editor-spacing; &-toggle { .flex(0, 0, auto); @@ -168,8 +169,6 @@ } &-editor { - margin-top: @vis-editor-agg-editor-spacing; - &-ranges { td { padding: 0 @vis-editor-agg-editor-spacing @vis-editor-agg-editor-spacing 0; @@ -221,14 +220,17 @@ .border-radius(0); } - &-add-form { - margin: @vis-editor-agg-editor-spacing * 3; - padding: @vis-editor-agg-editor-spacing; - > button { - display: block; - text-align: left; - width: 100%; - margin: 0 0 5px 0; + &-add { + .flex-parent(); + + &-subagg { + margin-bottom: -@vis-editor-agg-editor-spacing - 1; // extra one pixel covers the aggs bottom border + margin-right: -@vis-editor-agg-editor-spacing; + margin-left: -@vis-editor-agg-editor-spacing; + } + + &-schemas { + margin: @vis-editor-agg-editor-spacing * 3; } } @@ -274,6 +276,14 @@ } } +vis-editor, +vis-editor-agg-group, +vis-editor-agg, +vis-editor-agg-params, +vis-editor-agg-param, +vis-editor-vis-options { + .flex-parent(); +} form.vis-share { div.form-control { diff --git a/src/kibana/plugins/visualize/editor/vis_options.js b/src/kibana/plugins/visualize/editor/vis_options.js index eccb6a87aaf0c..882dc088e2211 100644 --- a/src/kibana/plugins/visualize/editor/vis_options.js +++ b/src/kibana/plugins/visualize/editor/vis_options.js @@ -8,7 +8,6 @@ define(function (require) { return { restrict: 'E', template: require('text!plugins/visualize/editor/vis_options.html'), - replace: true, scope: { vis: '=', }, diff --git a/src/kibana/utils/key_map.js b/src/kibana/utils/key_map.js new file mode 100644 index 0000000000000..ee78dd1ba6c11 --- /dev/null +++ b/src/kibana/utils/key_map.js @@ -0,0 +1,104 @@ +define(function (require) { + return { + 8: 'backspace', + 9: 'tab', + 13: 'enter', + 16: 'shift', + 17: 'ctrl', + 18: 'alt', + 19: 'pause', + 20: 'capsLock', + 27: 'escape', + 32: 'space', + 33: 'pageUp', + 34: 'pageDown', + 35: 'end', + 36: 'home', + 37: 'left', + 38: 'up', + 39: 'right', + 40: 'down', + 45: 'insert', + 46: 'delete', + 48: '0', + 49: '1', + 50: '2', + 51: '3', + 52: '4', + 53: '5', + 54: '6', + 55: '7', + 56: '8', + 57: '9', + 65: 'a', + 66: 'b', + 67: 'c', + 68: 'd', + 69: 'e', + 70: 'f', + 71: 'g', + 72: 'h', + 73: 'i', + 74: 'j', + 75: 'k', + 76: 'l', + 77: 'm', + 78: 'n', + 79: 'o', + 80: 'p', + 81: 'q', + 82: 'r', + 83: 's', + 84: 't', + 85: 'u', + 86: 'v', + 87: 'w', + 88: 'x', + 89: 'y', + 90: 'z', + 91: 'leftWindowKey', + 92: 'rightWindowKey', + 93: 'selectKey', + 96: '0', + 97: '1', + 98: '2', + 99: '3', + 100: '4', + 101: '5', + 102: '6', + 103: '7', + 104: '8', + 105: '9', + 106: 'multiply', + 107: 'add', + 109: 'subtract', + 110: 'period', + 111: 'divide', + 112: 'f1', + 113: 'f2', + 114: 'f3', + 115: 'f4', + 116: 'f5', + 117: 'f6', + 118: 'f7', + 119: 'f8', + 120: 'f9', + 121: 'f10', + 122: 'f11', + 123: 'f12', + 144: 'numLock', + 145: 'scrollLock', + 186: 'semiColon', + 187: 'equalSign', + 188: 'comma', + 189: 'dash', + 190: 'period', + 191: 'forwardSlash', + 192: 'graveAccent', + 219: 'openBracket', + 220: 'backSlash', + 221: 'closeBraket', + 222: 'singleQuote', + 224: 'meta' + }; +}); \ No newline at end of file diff --git a/test/unit/specs/components/agg_types/_agg_type.js b/test/unit/specs/components/agg_types/_agg_type.js index f4e7757cfb0d9..4beb4938d40ca 100644 --- a/test/unit/specs/components/agg_types/_agg_type.js +++ b/test/unit/specs/components/agg_types/_agg_type.js @@ -94,6 +94,23 @@ define(function (require) { expect(AggParams.firstCall.args[0]).to.be(params); }); }); + + describe('getResponseAggs', function () { + it('copies the value', function () { + var football = {}; + var aggType = new AggType({ + getResponseAggs: football + }); + + expect(aggType.getResponseAggs).to.be(football); + }); + + it('defaults to _.noop', function () { + var aggType = new AggType({}); + + expect(aggType.getResponseAggs).to.be(_.noop); + }); + }); }); }); diff --git a/test/unit/specs/components/agg_types/controls/percent_list.js b/test/unit/specs/components/agg_types/controls/percent_list.js index a9cbae4bc3f47..eaf82aab078eb 100644 --- a/test/unit/specs/components/agg_types/controls/percent_list.js +++ b/test/unit/specs/components/agg_types/controls/percent_list.js @@ -2,21 +2,31 @@ define(function (require) { describe('PercentList directive', function () { var $ = require('jquery'); var _ = require('lodash'); + var simulateKeys = require('test_utils/simulate_keys'); require('components/agg_types/controls/_percent_list'); var $el; var $scope; - var $compile; - var $rootScope; + var compile; beforeEach(module('kibana')); beforeEach(inject(function ($injector) { - $rootScope = $injector.get('$rootScope'); - $compile = $injector.get('$compile'); + var $compile = $injector.get('$compile'); + var $rootScope = $injector.get('$rootScope'); - $el = $('').attr('ng-model', 'val').attr('percent-list', ''); $scope = $rootScope.$new(); + $el = $('
').append( + $('') + .attr('ng-model', 'vals[$index]') + .attr('ng-repeat', 'val in vals') + .attr('percent-list', 'vals') + ); + compile = function (vals) { + $scope.vals = vals || []; + $compile($el)($scope); + $scope.$apply(); + }; })); afterEach(function () { @@ -24,57 +34,143 @@ define(function (require) { $scope.$destroy(); }); - it('filters out empty entries', function () { - $compile($el)($scope); - - $el.val(',,1, ,, ,2, ,\n,'); - $el.change(); - expect($scope.val).to.eql([1, 2]); - }); - it('fails on invalid numbers', function () { - $compile($el)($scope); - - $el.val('foo,bar'); - $el.change(); - expect($scope.val).to.be(undefined); - expect($el.hasClass('ng-invalid')).to.be(true); + compile([1, 'foo']); + expect($scope.vals).to.eql([1, undefined]); + expect($el.find('.ng-invalid').size()).to.be(1); }); it('supports decimals', function () { - $compile($el)($scope); - - $el.val('1.2,000001.6,99.10'); - $el.change(); - expect($scope.val).to.eql([1.2, 1.6, 99.10]); + compile(['1.2', '000001.6', '99.10']); + expect($scope.vals).to.eql([1.2, 1.6, 99.1]); }); it('ensures that the values are in order', function () { - $compile($el)($scope); - - $el.val('1, 2, 3, 10, 4, 5'); - $el.change(); - expect($scope.val).to.be(undefined); - expect($el.hasClass('ng-invalid')).to.be(true); + compile([1, 2, 3, 10, 4, 5]); + expect($scope.vals).to.eql([1, 2, 3, undefined, 4, 5]); + expect($el.find('.ng-invalid').size()).to.be(1); }); - it('ensures that the values are less between 0 and 100', function () { - $compile($el)($scope); - - $el.val('-1, 0, 1'); - $el.change(); - expect($scope.val).to.be(undefined); - expect($el.hasClass('ng-invalid')).to.be(true); - - $el.val('0, 1'); - $el.change(); - expect($scope.val).to.eql([0, 1]); - expect($el.hasClass('ng-invalid')).to.be(false); + describe('ensures that the values are between 0 and 100', function () { + it(': -1', function () { + compile([-1, 1]); + expect($scope.vals).to.eql([undefined, 1]); + expect($el.find('.ng-invalid').size()).to.be(1); + }); + + it(': 0', function () { + compile([0, 1]); + expect($scope.vals).to.eql([undefined, 1]); + expect($el.find('.ng-invalid').size()).to.be(1); + }); + + it(': 0.0001', function () { + compile([0.0001, 1]); + expect($scope.vals).to.eql([0.0001, 1]); + expect($el.find('.ng-invalid').size()).to.be(0); + }); + + it(': 99.9999999', function () { + compile([1, 99.9999999]); + expect($scope.vals).to.eql([1, 99.9999999]); + expect($el.find('.ng-invalid').size()).to.be(0); + }); + + it(': 101', function () { + compile([1, 101]); + expect($scope.vals).to.eql([1, undefined]); + expect($el.find('.ng-invalid').size()).to.be(1); + }); + }); - $el.val('1, 101'); - $el.change(); - expect($scope.val).to.be(undefined); - expect($el.hasClass('ng-invalid')).to.be(true); + describe('listens for keyboard events', function () { + it('up arrow increases by 1', function () { + compile([1]); + + return simulateKeys( + function () { return $el.find('input').first(); }, + ['up', 'up', 'up'] + ) + .then(function () { + expect($scope.vals).to.eql([4]); + }); + }); + + it('shift-up increases by 0.1', function () { + compile([4.8]); + + var seq = [ + { + type: 'press', + key: 'shift', + events: [ + 'up', + 'up', + 'up' + ] + } + ]; + + return simulateKeys( + function () { return $el.find('input').first(); }, + seq + ) + .then(function () { + expect($scope.vals).to.eql([5.1]); + }); + }); + + it('down arrow decreases by 1', function () { + compile([5]); + + return simulateKeys( + function () { return $el.find('input').first(); }, + ['down', 'down', 'down'] + ) + .then(function () { + expect($scope.vals).to.eql([2]); + }); + }); + + it('shift-down decreases by 0.1', function () { + compile([5.1]); + + var seq = [ + { + type: 'press', + key: 'shift', + events: [ + 'down', + 'down', + 'down' + ] + } + ]; + + return simulateKeys( + function () { return $el.find('input').first(); }, + seq + ) + .then(function () { + expect($scope.vals).to.eql([4.8]); + }); + }); + + it('maintains valid number', function () { + compile([9, 11, 13]); + + var seq = [ + 'down', // 10 (11 - 1) + 'down' // 10 (limited by 9) + ]; + + var getEl = function () { return $el.find('input').eq(1); }; + + return simulateKeys(getEl, seq) + .then(function () { + expect($scope.vals).to.eql([9, 10, 13]); + }); + }); }); }); }); \ No newline at end of file diff --git a/test/unit/specs/components/agg_types/index.js b/test/unit/specs/components/agg_types/index.js index 965a633621e3e..c86cd2a25fbd3 100644 --- a/test/unit/specs/components/agg_types/index.js +++ b/test/unit/specs/components/agg_types/index.js @@ -6,10 +6,43 @@ define(function (require) { require('specs/components/agg_types/_bucket_count_between'), require('specs/components/agg_types/buckets/_histogram'), require('specs/components/agg_types/buckets/_date_histogram'), - require('specs/components/agg_types/buckets/_terms'), require('specs/components/agg_types/_metric_aggs') ].forEach(function (s) { describe(s[0], s[1]); }); + + describe('bucket aggs', function () { + var bucketAggs; + var BucketAggType; + + beforeEach(module('kibana')); + beforeEach(inject(function (Private) { + bucketAggs = Private(require('components/agg_types/index')).byType.buckets; + BucketAggType = Private(require('components/agg_types/buckets/_bucket_agg_type')); + })); + + it('all extend BucketAggType', function () { + bucketAggs.forEach(function (bucketAgg) { + expect(bucketAgg).to.be.a(BucketAggType); + }); + }); + }); + + describe('metric aggs', function () { + var metricAggs; + var MetricAggType; + + beforeEach(module('kibana')); + beforeEach(inject(function (Private) { + metricAggs = Private(require('components/agg_types/index')).byType.metrics; + MetricAggType = Private(require('components/agg_types/metrics/_metric_agg_type')); + })); + + it('all extend MetricAggType', function () { + metricAggs.forEach(function (metricAgg) { + expect(metricAgg).to.be.a(MetricAggType); + }); + }); + }); }); }); \ No newline at end of file diff --git a/test/unit/specs/components/vis/_agg_config.js b/test/unit/specs/components/vis/_agg_config.js index 99475ade6ead2..78e2dd3862b90 100644 --- a/test/unit/specs/components/vis/_agg_config.js +++ b/test/unit/specs/components/vis/_agg_config.js @@ -113,42 +113,42 @@ define(function (require) { {} ]; AggConfig.ensureIds(objs); - expect(objs[0]).to.have.property('id', 1); - expect(objs[1]).to.have.property('id', 2); - expect(objs[2]).to.have.property('id', 3); - expect(objs[3]).to.have.property('id', 4); + expect(objs[0]).to.have.property('id', '1'); + expect(objs[1]).to.have.property('id', '2'); + expect(objs[2]).to.have.property('id', '3'); + expect(objs[3]).to.have.property('id', '4'); }); it('assigns ids relative to the other items in the list', function () { var objs = [ - { id: 100 }, + { id: '100' }, {}, ]; AggConfig.ensureIds(objs); - expect(objs[0]).to.have.property('id', 100); - expect(objs[1]).to.have.property('id', 101); + expect(objs[0]).to.have.property('id', '100'); + expect(objs[1]).to.have.property('id', '101'); }); it('assigns ids relative to the other items in the list', function () { var objs = [ - { id: 100 }, - { id: 200 }, - { id: 500 }, - { id: 350 }, + { id: '100' }, + { id: '200' }, + { id: '500' }, + { id: '350' }, {}, ]; AggConfig.ensureIds(objs); - expect(objs[0]).to.have.property('id', 100); - expect(objs[1]).to.have.property('id', 200); - expect(objs[2]).to.have.property('id', 500); - expect(objs[3]).to.have.property('id', 350); - expect(objs[4]).to.have.property('id', 501); + expect(objs[0]).to.have.property('id', '100'); + expect(objs[1]).to.have.property('id', '200'); + expect(objs[2]).to.have.property('id', '500'); + expect(objs[3]).to.have.property('id', '350'); + expect(objs[4]).to.have.property('id', '501'); }); it('uses ::nextId to get the starting value', function () { sinon.stub(AggConfig, 'nextId').returns(534); var objs = AggConfig.ensureIds([{}]); - expect(objs[0]).to.have.property('id', 534); + expect(objs[0]).to.have.property('id', '534'); }); it('only calls ::nextId once', function () { @@ -158,7 +158,7 @@ define(function (require) { expect(AggConfig.nextId).to.have.property('callCount', 1); objs.forEach(function (obj, i) { - expect(obj).to.have.property('id', start + i); + expect(obj).to.have.property('id', String(start + i)); }); }); }); @@ -194,13 +194,13 @@ define(function (require) { }); var aggConfig = vis.aggs.byTypeName.date_histogram[0]; - expect(aggConfig.id).to.be(1); + expect(aggConfig.id).to.be('1'); expect(aggConfig.params).to.be.an('object'); expect(aggConfig.type).to.be.an(AggType).and.have.property('name', 'date_histogram'); expect(aggConfig.schema).to.be.an('object').and.have.property('name', 'segment'); var state = aggConfig.toJSON(); - expect(state).to.have.property('id', 1); + expect(state).to.have.property('id', '1'); expect(state.params).to.be.an('object'); expect(state).to.have.property('type', 'date_histogram'); expect(state).to.have.property('schema', 'segment'); diff --git a/test/unit/specs/components/vis/_agg_configs.js b/test/unit/specs/components/vis/_agg_configs.js index f99d9ab410dc4..1aea280a9da48 100644 --- a/test/unit/specs/components/vis/_agg_configs.js +++ b/test/unit/specs/components/vis/_agg_configs.js @@ -142,11 +142,10 @@ define(function (require) { expect(ac).to.have.length(3); expect(ac.bySchemaName['segment'][0].type.name).to.equal('date_histogram'); }); - }); }); - describe('#getSorted', function () { + describe('#getRequestAggs', function () { it('performs a stable sort, but moves metrics to the bottom', function () { var vis = new Vis(indexPattern, { type: 'histogram', @@ -157,27 +156,68 @@ define(function (require) { { type: 'sum', schema: 'metric' }, { type: 'date_histogram', schema: 'segment' }, { type: 'filters', schema: 'split' }, + { type: 'percentiles', schema: 'metric' } + ] + }); + + var sorted = vis.aggs.getRequestAggs(); + var aggs = _.indexBy(vis.aggs, function (agg) { + return agg.type.name; + }); + + expect(sorted.shift()).to.be(aggs.terms); + expect(sorted.shift()).to.be(aggs.histogram); + expect(sorted.shift()).to.be(aggs.date_histogram); + expect(sorted.shift()).to.be(aggs.filters); + expect(sorted.shift()).to.be(aggs.avg); + expect(sorted.shift()).to.be(aggs.sum); + expect(sorted.shift()).to.be(aggs.percentiles); + expect(sorted).to.have.length(0); + }); + }); + + describe('#getResponseAggs', function () { + it('returns all request aggs for basic aggs', function () { + var vis = new Vis(indexPattern, { + type: 'histogram', + aggs: [ + { type: 'terms', schema: 'split' }, + { type: 'date_histogram', schema: 'segment' }, { type: 'count', schema: 'metric' } ] }); - var avg = vis.aggs.byTypeName.avg[0]; - var sum = vis.aggs.byTypeName.sum[0]; - var count = vis.aggs.byTypeName.count[0]; - var terms = vis.aggs.byTypeName.terms[0]; - var histo = vis.aggs.byTypeName.histogram[0]; - var dateHisto = vis.aggs.byTypeName.date_histogram[0]; - var filters = vis.aggs.byTypeName.filters[0]; - - var sorted = vis.aggs.getSorted(); - - expect(sorted.shift()).to.be(terms); - expect(sorted.shift()).to.be(histo); - expect(sorted.shift()).to.be(dateHisto); - expect(sorted.shift()).to.be(filters); - expect(sorted.shift()).to.be(avg); - expect(sorted.shift()).to.be(sum); - expect(sorted.shift()).to.be(count); + var sorted = vis.aggs.getResponseAggs(); + var aggs = _.indexBy(vis.aggs, function (agg) { + return agg.type.name; + }); + + expect(sorted.shift()).to.be(aggs.terms); + expect(sorted.shift()).to.be(aggs.date_histogram); + expect(sorted.shift()).to.be(aggs.count); + expect(sorted).to.have.length(0); + }); + + it('expands aggs that have multiple responses', function () { + var vis = new Vis(indexPattern, { + type: 'histogram', + aggs: [ + { type: 'terms', schema: 'split' }, + { type: 'date_histogram', schema: 'segment' }, + { type: 'percentiles', schema: 'metric', params: { percents: [1, 2, 3]} } + ] + }); + + var sorted = vis.aggs.getResponseAggs(); + var aggs = _.indexBy(vis.aggs, function (agg) { + return agg.type.name; + }); + + expect(sorted.shift()).to.be(aggs.terms); + expect(sorted.shift()).to.be(aggs.date_histogram); + expect(sorted.shift().id).to.be(aggs.percentiles.id + '.' + 1); + expect(sorted.shift().id).to.be(aggs.percentiles.id + '.' + 2); + expect(sorted.shift().id).to.be(aggs.percentiles.id + '.' + 3); expect(sorted).to.have.length(0); }); }); @@ -185,9 +225,9 @@ define(function (require) { describe('#toDsl', function () { it('uses the sorted aggs', function () { var vis = new Vis(indexPattern, { type: 'histogram' }); - sinon.spy(vis.aggs, 'getSorted'); + sinon.spy(vis.aggs, 'getRequestAggs'); vis.aggs.toDsl(); - expect(vis.aggs.getSorted).to.have.property('callCount', 1); + expect(vis.aggs.getRequestAggs).to.have.property('callCount', 1); }); it('calls aggConfig#toDsl() on each aggConfig and compiles the nested output', function () { diff --git a/test/utils/simulate_keys.js b/test/utils/simulate_keys.js new file mode 100644 index 0000000000000..53744e5f2cb19 --- /dev/null +++ b/test/utils/simulate_keys.js @@ -0,0 +1,105 @@ +define(function (require) { + var $ = require('jquery'); + var _ = require('lodash'); + var Promise = require('bluebird'); + var keyMap = require('utils/key_map'); + var reverseKeyMap = _.mapValues(_.invert(keyMap), _.limit(_.parseInt, 1)); + var KeyboardEvent = window.KeyboardEvent; + + /** + * Simulate keyboard events in an element. This allows testing the way that + * elements respond to keyboard input. + * + * # sequence style + * keyboard events occur in a sequence, this array of events describe that sequence. + * + * ## event + * an object with a type property, or a string which will be turned into a single press + * + * ## event types + * ### press + * represents a key press + * - `key`: the key for the button pressed + * - `events`: optional list of events that occur before this press completes + * + * ### wait + * represents a pause in a sequence + * - `ms`: the number of milliseconds that the pause takes + * + * ### repeat + * represents a key being repeated because it is held down. Should only exist as a + * sub event of `press` events. + * - `count`: the number of times the repeat occurs + * + * @param {element} $el - jQuery element where events should occur + * @param {[type]} sequence - an array of events + * @async + */ + return function ($el, sequence) { + var modifierState = { + ctrlKey: false, + shiftKey: false, + altKey: false, + metaKey: false + }; + + return doList(_.clone(sequence)); + + function setModifier(key, state) { + var name = key + 'Key'; + if (modifierState.hasOwnProperty(name)) { + modifierState[name] = !!state; + } + } + + function doList(list) { + return Promise.try(function () { + if (!list || !list.length) return; + + var event = list[0]; + if (_.isString(event)) { + event = { type: 'press', key: event }; + } + + switch (event.type) { + case 'press': + return Promise.resolve() + .then(_.partial(fire, 'keydown', event.key)) + .then(_.partial(fire, 'keypress', event.key)) + .then(_.partial(doList, event.events)) + .then(_.partial(fire, 'keyup', event.key)); + + case 'wait': + return Promise.delay(event.ms); + + case 'repeat': + return (function again(remaining) { + if (!remaining) return Promise.resolve(); + remaining = remaining - 1; + return Promise.resolve() + .then(_.partial(fire, 'keydown', event.key, true)) + .then(_.partial(fire, 'keypress', event.key, true)) + .then(_.partial(again, remaining)); + }(event.count)); + + default: + throw new TypeError('invalid event type "' + event.type + '"'); + } + }) + .then(function () { + if (_.size(list) > 1) return doList(list.slice(1)); + }); + } + + function fire(type, key, repeat) { + var keyCode = reverseKeyMap[key]; + if (!keyCode) throw new TypeError('invalid key "' + key + '"'); + + if (type === 'keydown') setModifier(key, true); + if (type === 'keyup') setModifier(key, false); + + var $target = _.isFunction($el) ? $el() : $el; + $target.trigger($.Event(type, _.defaults({ keyCode: keyCode }, modifierState))); + } + }; +}); \ No newline at end of file