mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
Merge pull request #2601 from spenceralger/extendedStats
Extended stats Agg
This commit is contained in:
commit
bcb6dd9c4f
23 changed files with 536 additions and 34 deletions
|
@ -12,6 +12,10 @@ define(function (require) {
|
|||
yScale: yScale
|
||||
};
|
||||
|
||||
if (point.y === 'NaN') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (series) {
|
||||
point.series = unwrap(row[series.i]);
|
||||
}
|
||||
|
|
|
@ -15,12 +15,14 @@ define(function (require) {
|
|||
|
||||
if (!multiY) {
|
||||
var point = partGetPoint(row, aspects.y);
|
||||
addToSiri(series, point, point.series);
|
||||
if (point) addToSiri(series, point, point.series);
|
||||
return;
|
||||
}
|
||||
|
||||
aspects.y.forEach(function (y) {
|
||||
var point = partGetPoint(row, y);
|
||||
if (!point) return;
|
||||
|
||||
var prefix = point.series ? point.series + ': ' : '';
|
||||
var seriesId = prefix + y.agg.id;
|
||||
var seriesLabel = prefix + y.col.title;
|
||||
|
|
17
src/kibana/components/agg_types/controls/extended_stats.html
Normal file
17
src/kibana/components/agg_types/controls/extended_stats.html
Normal file
|
@ -0,0 +1,17 @@
|
|||
<div class="form-group" ng-controller="aggParam.controller">
|
||||
<label for="field">
|
||||
Metrics
|
||||
</label>
|
||||
|
||||
<!-- validate that there is atleast one checkbox selected -->
|
||||
<input type="hidden" ng-model="names[0]" name="first" required>
|
||||
<p class="text-danger" ng-if="aggForm.first.$invalid">
|
||||
select at least one stat
|
||||
</p>
|
||||
|
||||
<div ng-repeat="stat in statNames" class="checkbox">
|
||||
<label>
|
||||
<input ng-model="map[stat]" type="checkbox"> {{stat}}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
|
@ -9,6 +9,7 @@ define(function (require) {
|
|||
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/extended_stats')),
|
||||
Private(require('components/agg_types/metrics/cardinality')),
|
||||
Private(require('components/agg_types/metrics/percentiles'))
|
||||
],
|
||||
|
|
84
src/kibana/components/agg_types/metrics/extended_stats.js
Normal file
84
src/kibana/components/agg_types/metrics/extended_stats.js
Normal file
|
@ -0,0 +1,84 @@
|
|||
define(function (require) {
|
||||
return function AggTypeMetricExtendedStatsProvider(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 valueProps = {
|
||||
makeLabel: function () {
|
||||
return this.key + ' of ' + this.fieldDisplayName();
|
||||
}
|
||||
};
|
||||
|
||||
var statNames = [
|
||||
'count',
|
||||
'min',
|
||||
'max',
|
||||
'avg',
|
||||
'sum',
|
||||
'sum_of_squares',
|
||||
'variance',
|
||||
'std_deviation'
|
||||
];
|
||||
|
||||
var exStatsType = new MetricAggType({
|
||||
name: 'extended_stats',
|
||||
title: 'Extended Stats',
|
||||
makeLabel: function (agg) {
|
||||
return 'Extended Stats on ' + agg.fieldDisplayName();
|
||||
},
|
||||
params: [
|
||||
{
|
||||
name: 'field',
|
||||
filterFieldTypes: 'number'
|
||||
},
|
||||
{
|
||||
name: 'names',
|
||||
editor: require('text!components/agg_types/controls/extended_stats.html'),
|
||||
default: statNames.slice(),
|
||||
write: _.noop,
|
||||
controller: function ($scope) {
|
||||
$scope.map = mapList();
|
||||
$scope.names = listMap();
|
||||
$scope.statNames = statNames;
|
||||
|
||||
$scope.$watchCollection('agg.params.names', function (names) {
|
||||
if (names === $scope.names) return;
|
||||
|
||||
$scope.names = _.intersection(statNames, names || []);
|
||||
$scope.map = mapList();
|
||||
});
|
||||
|
||||
$scope.$watchCollection('map', function () {
|
||||
$scope.names = $scope.agg.params.names = listMap();
|
||||
});
|
||||
|
||||
function mapList() {
|
||||
return _.transform($scope.names, function (map, key) {
|
||||
map[key] = true;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function listMap() {
|
||||
return _.transform(statNames, function (list, stat) {
|
||||
if ($scope.map[stat]) list.push(stat);
|
||||
}, []);
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
getResponseAggs: function (agg) {
|
||||
var ValueAggConfig = getResponseAggConfig(agg, valueProps);
|
||||
return _.map(agg.params.names, function (name) {
|
||||
return new ValueAggConfig(name);
|
||||
});
|
||||
},
|
||||
getValue: function (agg, bucket) {
|
||||
return bucket[agg.parentId][agg.key];
|
||||
}
|
||||
});
|
||||
|
||||
exStatsType.statNames = statNames;
|
||||
return exStatsType;
|
||||
};
|
||||
});
|
|
@ -53,6 +53,8 @@ define(function (require) {
|
|||
});
|
||||
},
|
||||
getValue: function (agg, bucket) {
|
||||
// percentiles for 1, 5, and 10 will come back as 1.0, 5.0, and 10.0 so we
|
||||
// parse the keys and respond with the value that matches
|
||||
return _.find(bucket[agg.parentId].values, function (value, key) {
|
||||
return agg.key === parseFloat(key);
|
||||
});
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
function(value){this.$viewValue=value;//changetodirtyif(this.$pristine){this.$dirty=true;this.$pristine=false;$animate.removeClass($element,PRISTINE_CLASS);$animate.addClass($element,DIRTY_CLASS);parentForm.$setDirty();}forEach(this.$parsers,function(fn){value=fn(value);});if(this.$modelValue!==value){this.$modelValue=value;ngModelSet($scope,value);forEach(this.$viewChangeListeners,function(listener){try{listener();}catch(e){$exceptionHandler(e);}});}}
|
45
src/kibana/components/fancy_forms/fancy_forms.js
Normal file
45
src/kibana/components/fancy_forms/fancy_forms.js
Normal file
|
@ -0,0 +1,45 @@
|
|||
define(function (require) {
|
||||
var _ = require('lodash');
|
||||
var $ = require('jquery');
|
||||
|
||||
var KbnFormController = require('components/fancy_forms/kbn_form');
|
||||
var KbnModelController = require('components/fancy_forms/kbn_model');
|
||||
|
||||
require('modules')
|
||||
.get('kibana')
|
||||
.config(function ($provide) {
|
||||
function decorateDirectiveController(DecorativeController) {
|
||||
return function ($delegate, $injector) {
|
||||
// directive providers are arrays
|
||||
$delegate.forEach(function (directive) {
|
||||
// get metadata about all init fns
|
||||
var chain = [directive.controller, DecorativeController].map(function (fn) {
|
||||
var deps = $injector.annotate(fn);
|
||||
return { deps: deps, fn: _.isArray(fn) ? _.last(fn) : fn };
|
||||
});
|
||||
|
||||
// replace the controller with one that will setup the actual controller
|
||||
directive.controller = function stub() {
|
||||
var allDeps = _.toArray(arguments);
|
||||
return chain.reduce(function (controller, link, i) {
|
||||
var deps = allDeps.splice(0, link.deps.length);
|
||||
return link.fn.apply(controller, deps) || controller;
|
||||
}, this);
|
||||
};
|
||||
|
||||
// set the deps of our new controller to be the merged deps of every fn
|
||||
directive.controller.$inject = chain.reduce(function (deps, link) {
|
||||
return deps.concat(link.deps);
|
||||
}, []);
|
||||
});
|
||||
|
||||
return $delegate;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
$provide.decorator('formDirective', decorateDirectiveController(KbnFormController));
|
||||
$provide.decorator('ngFormDirective', decorateDirectiveController(KbnFormController));
|
||||
$provide.decorator('ngModelDirective', decorateDirectiveController(KbnModelController));
|
||||
});
|
||||
});
|
26
src/kibana/components/fancy_forms/kbn_form.js
Normal file
26
src/kibana/components/fancy_forms/kbn_form.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
define(function (require) {
|
||||
var _ = require('lodash');
|
||||
|
||||
/**
|
||||
* Extension of Angular's FormController class
|
||||
* that provides helpers for error handling/validation.
|
||||
*
|
||||
* @param {$scope} $scope
|
||||
*/
|
||||
function KbnFormController($scope, $element) {
|
||||
var self = this;
|
||||
|
||||
self.errorCount = function () {
|
||||
return _.reduce(self.$error, function (count, controls, errorType) {
|
||||
return count + _.size(controls);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
self.describeErrors = function () {
|
||||
var count = self.errorCount();
|
||||
return count + ' Error' + (count === 1 ? '' : 's');
|
||||
};
|
||||
}
|
||||
|
||||
return KbnFormController;
|
||||
});
|
102
src/kibana/components/fancy_forms/kbn_model.js
Normal file
102
src/kibana/components/fancy_forms/kbn_model.js
Normal file
|
@ -0,0 +1,102 @@
|
|||
define(function (require) {
|
||||
var _ = require('lodash');
|
||||
var SVV_CHECKSUM = require('text!components/fancy_forms/_set_view_value.checksum');
|
||||
var PRISTINE_CLASS = 'ng-pristine';
|
||||
var DIRTY_CLASS = 'ng-dirty';
|
||||
|
||||
// http://goo.gl/eJofve
|
||||
var nullFormCtrl = {
|
||||
$addControl: _.noop,
|
||||
$removeControl: _.noop,
|
||||
$setValidity: _.noop,
|
||||
$setDirty: _.noop,
|
||||
$setPristine: _.noop
|
||||
};
|
||||
|
||||
/**
|
||||
* Extension of Angular's NgModelController class
|
||||
* that ensures models are marked "dirty" after
|
||||
* they move from an invalid state to valid.
|
||||
*
|
||||
* @param {$scope} $scope
|
||||
*/
|
||||
function KbnModelController($scope, $element, $animate) {
|
||||
var ngModel = this;
|
||||
|
||||
// verify that angular works the way we are assuming it does
|
||||
if (String(ngModel.$setViewValue).replace(/\s+/g, '') !== SVV_CHECKSUM) {
|
||||
throw new Error('ngModelController.$setViewValue has updated but KbnModelController has not!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the form a model belongs to
|
||||
*
|
||||
* @return {NgFormController} - the parent controller of a noop controller
|
||||
*/
|
||||
ngModel.$getForm = function () {
|
||||
return $element.inheritedData('$formController') || nullFormCtrl;
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the ngModel to be "dirty" if it is pristine.
|
||||
*
|
||||
* @return {undefined}
|
||||
*/
|
||||
ngModel.$setDirty = function () {
|
||||
if (ngModel.$dirty) return;
|
||||
ngModel.$dirty = true;
|
||||
ngModel.$pristine = false;
|
||||
$animate.removeClass($element, PRISTINE_CLASS);
|
||||
$animate.addClass($element, DIRTY_CLASS);
|
||||
ngModel.$getForm().$setDirty();
|
||||
};
|
||||
|
||||
/**
|
||||
* While the model is pristine, ensure that the model
|
||||
* gets set to dirty if it becomes invalid. If the model
|
||||
* becomes dirty of other reasons stop watching and
|
||||
* waitForPristine()
|
||||
*
|
||||
* @return {undefined}
|
||||
*/
|
||||
function watchForDirtyOrInvalid() {
|
||||
var unwatch = $scope.$watch(get, react);
|
||||
|
||||
function get() {
|
||||
return ngModel.$dirty || ngModel.$invalid;
|
||||
}
|
||||
|
||||
function react(is, was) {
|
||||
if (is === was) return;
|
||||
unwatch();
|
||||
waitForPristine();
|
||||
ngModel.$setDirty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Once a model becomes dirty, there is no longer a need
|
||||
* for a watcher. Instead, we will react to the $setPristine
|
||||
* method being called. This is the only way for a model to go
|
||||
* from dirty -> pristine.
|
||||
*
|
||||
* @return {[type]} [description]
|
||||
*/
|
||||
function waitForPristine() {
|
||||
var fn = ngModel.$setPristine;
|
||||
ngModel.$setPristine = function () {
|
||||
var ret = fn.apply(this, arguments);
|
||||
if (ngModel.$pristine) {
|
||||
ngModel.$setPristine = fn;
|
||||
watchForDirtyOrInvalid();
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
}
|
||||
|
||||
if (ngModel.$dirty) waitForPristine();
|
||||
else watchForDirtyOrInvalid();
|
||||
}
|
||||
|
||||
return KbnModelController;
|
||||
});
|
|
@ -16,6 +16,7 @@ define(function (require) {
|
|||
require('components/watch_multi');
|
||||
require('components/bind');
|
||||
require('components/listen');
|
||||
require('components/fancy_forms/fancy_forms');
|
||||
require('directives/click_focus');
|
||||
require('directives/info');
|
||||
require('directives/spinner');
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
|
||||
<!-- error -->
|
||||
<span ng-if="!editorOpen && aggForm.$invalid" class="vis-editor-agg-header-description danger">
|
||||
{{ describeError() }}
|
||||
{{ aggForm.describeErrors() }}
|
||||
</span>
|
||||
|
||||
<!-- controls !!!actually disabling buttons will break tooltips¡¡¡ -->
|
||||
|
|
|
@ -38,18 +38,6 @@ define(function (require) {
|
|||
return label ? label : '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Describe the errors in this agg
|
||||
* @return {[type]} [description]
|
||||
*/
|
||||
$scope.describeError = function () {
|
||||
var count = _.reduce($scope.aggForm.$error, function (count, controls, errorType) {
|
||||
return count + _.size(controls);
|
||||
}, 0);
|
||||
|
||||
return count + ' Error' + (count > 1 ? 's' : '');
|
||||
};
|
||||
|
||||
function move(below, agg) {
|
||||
_.move($scope.vis.aggs, agg, below, function (otherAgg) {
|
||||
return otherAgg.schema.group === agg.schema.group;
|
||||
|
|
|
@ -18,6 +18,11 @@
|
|||
|
||||
<!-- apply/discard -->
|
||||
<li class="vis-editor-sidebar-buttons sidebar-item">
|
||||
<p
|
||||
ng-if="visualizeEditor.$invalid"
|
||||
class="text-center text-danger sidebar-item-text">
|
||||
<i class="fa fa-warning"></i> {{visualizeEditor.describeErrors()}}
|
||||
</p>
|
||||
<button
|
||||
ng-disabled="!vis.dirty || visualizeEditor.$invalid"
|
||||
type="submit"
|
||||
|
@ -28,7 +33,7 @@
|
|||
ng-disabled="!vis.dirty"
|
||||
type="button"
|
||||
ng-click="resetEditableVis()"
|
||||
class="sidebar-item-button warn">
|
||||
class="sidebar-item-button default">
|
||||
Discard
|
||||
</button>
|
||||
</li>
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.sidebar-list-header {
|
||||
&-header {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
color: @sidebar-header-color;
|
||||
|
@ -35,8 +35,19 @@
|
|||
}
|
||||
}
|
||||
|
||||
.sidebar-item-title {
|
||||
&-title,
|
||||
&-text,
|
||||
&-button {
|
||||
margin: 0;
|
||||
padding: 5px 10px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
&-title {
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
.ellipsis();
|
||||
|
||||
|
@ -51,11 +62,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
.sidebar-item-button {
|
||||
padding: 5px 10px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
border-radius: 0;
|
||||
&-text {
|
||||
background: white;
|
||||
}
|
||||
|
||||
&-button {
|
||||
font-size: inherit;
|
||||
|
||||
&[disabled] {
|
||||
|
@ -87,6 +98,10 @@
|
|||
background-color: @btn-danger-bg;
|
||||
color: @btn-danger-color
|
||||
}
|
||||
|
||||
&.default {
|
||||
.btn-default();
|
||||
}
|
||||
}
|
||||
|
||||
.active {
|
||||
|
|
|
@ -114,12 +114,20 @@ ul.navbar-inline li {
|
|||
}
|
||||
|
||||
.top-fixed {
|
||||
position:fixed;
|
||||
bottom:0px;
|
||||
position: fixed;
|
||||
bottom: 0px;
|
||||
}
|
||||
|
||||
.checkbox input[type="checkbox"] {
|
||||
float: none;
|
||||
.checkbox label {
|
||||
.display(flex);
|
||||
.align-items(center);
|
||||
padding-left: 0;
|
||||
|
||||
input[type="checkbox"] {
|
||||
float: none;
|
||||
margin: 0 4px;
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
|
||||
notifications {
|
||||
|
|
|
@ -8,7 +8,7 @@ define(function (require) {
|
|||
getPoint = Private(require('components/agg_response/point_series/_get_point'));
|
||||
}));
|
||||
|
||||
it('properly unwraps and scales values without the a series', function () {
|
||||
it('properly unwraps and scales values without a series', function () {
|
||||
var row = [ { value: 1 }, { value: 2 }];
|
||||
var point = getPoint({ i: 0 }, null, 5, row, { i: 1 });
|
||||
|
||||
|
@ -29,5 +29,11 @@ define(function (require) {
|
|||
.and.have.property('y', 3)
|
||||
.and.have.property('aggConfigResult', row[2]);
|
||||
});
|
||||
|
||||
it('ignores points with a y value of NaN', function () {
|
||||
var row = [ { value: 1 }, { value: 'NaN' }];
|
||||
var point = getPoint({ i: 0 }, null, 5, row, { i: 1 });
|
||||
expect(point).to.be(void 0);
|
||||
});
|
||||
}];
|
||||
});
|
|
@ -98,7 +98,9 @@ define(function (require) {
|
|||
var rows = [
|
||||
['0', 3],
|
||||
['1', 3],
|
||||
['1', 'NaN'],
|
||||
['0', 3],
|
||||
['0', 'NaN'],
|
||||
['1', 3],
|
||||
['0', 3],
|
||||
['1', 3]
|
||||
|
|
|
@ -4,7 +4,7 @@ define(function (require) {
|
|||
run(require('specs/components/agg_response/point_series/_add_to_siri'));
|
||||
run(require('specs/components/agg_response/point_series/_fake_x_aspect'));
|
||||
run(require('specs/components/agg_response/point_series/_get_aspects'));
|
||||
run(require('specs/components/agg_response/point_series/_get_points'));
|
||||
run(require('specs/components/agg_response/point_series/_get_point'));
|
||||
run(require('specs/components/agg_response/point_series/_get_series'));
|
||||
run(require('specs/components/agg_response/point_series/_init_x_axis'));
|
||||
run(require('specs/components/agg_response/point_series/_init_y_axis'));
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
define(function (require) {
|
||||
return ['AggParams', function () {
|
||||
|
||||
}];
|
||||
});
|
|
@ -6,7 +6,7 @@ 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/_metric_aggs')
|
||||
require('specs/components/agg_types/metrics/_extended_stats')
|
||||
].forEach(function (s) {
|
||||
describe(s[0], s[1]);
|
||||
});
|
||||
|
|
104
test/unit/specs/components/agg_types/metrics/_extended_stats.js
Normal file
104
test/unit/specs/components/agg_types/metrics/_extended_stats.js
Normal file
|
@ -0,0 +1,104 @@
|
|||
define(function (require) {
|
||||
return ['¡Extended Stats!', function () {
|
||||
var _ = require('lodash');
|
||||
var $ = require('jquery');
|
||||
|
||||
var vis;
|
||||
var agg;
|
||||
|
||||
beforeEach(module('kibana'));
|
||||
beforeEach(inject(function (Private) {
|
||||
var Vis = Private(require('components/vis/vis'));
|
||||
var indexPattern = Private(require('fixtures/stubbed_logstash_index_pattern'));
|
||||
|
||||
// the vis which wraps the agg
|
||||
vis = new Vis(indexPattern, {
|
||||
type: 'histogram',
|
||||
aggs: [
|
||||
{ type: 'extended_stats', params: { field: 'bytes' } }
|
||||
]
|
||||
});
|
||||
|
||||
// the extended_stats agg
|
||||
agg = vis.aggs[0];
|
||||
|
||||
}));
|
||||
|
||||
describe('#makeLabel', function () {
|
||||
it('makes pretty labels', function () {
|
||||
expect(agg.makeLabel()).to.be('Extended Stats on bytes');
|
||||
});
|
||||
});
|
||||
|
||||
describe('names param', function () {
|
||||
it('defaults to the full metric list', function () {
|
||||
expect(_.size(agg.params.names)).to.be.greaterThan(0);
|
||||
expect(agg.params.names).to.eql(agg.type.statNames);
|
||||
});
|
||||
|
||||
describe('editor controller', function () {
|
||||
var $el;
|
||||
var $scope;
|
||||
var $rootScope;
|
||||
|
||||
beforeEach(inject(function ($injector, $compile) {
|
||||
$rootScope = $injector.get('$rootScope');
|
||||
|
||||
$scope = $rootScope.$new();
|
||||
$scope.agg = agg;
|
||||
$scope.aggParam = agg.type.params.byName.names;
|
||||
|
||||
$el = $($scope.aggParam.editor);
|
||||
$compile($el)($scope);
|
||||
}));
|
||||
|
||||
afterEach(function () {
|
||||
$el.remove();
|
||||
$scope.$destroy();
|
||||
});
|
||||
|
||||
it('reflects the selected names as selected checkboxes', function () {
|
||||
agg.params.names = _.sample(agg.type.statNames, 3);
|
||||
$rootScope.$apply();
|
||||
|
||||
var $checks = $el.find('input[type=checkbox]');
|
||||
expect($checks).to.have.length(agg.type.statNames.length);
|
||||
|
||||
$checks.each(function () {
|
||||
var $check = $(this);
|
||||
var name = $check.parent().text().trim();
|
||||
var index = agg.params.names.indexOf(name);
|
||||
|
||||
if (!$check.is(':checked')) expect(index).to.be(-1);
|
||||
else expect(index).to.be.greaterThan(-1);
|
||||
});
|
||||
});
|
||||
|
||||
it('syncs the checked boxes with the name list', function () {
|
||||
agg.params.names = [];
|
||||
$rootScope.$apply();
|
||||
|
||||
var $checks = $el.find('input[type=checkbox]');
|
||||
expect($checks).to.have.length(agg.type.statNames.length);
|
||||
|
||||
$checks.each(function (i) {
|
||||
var $check = $(this).click();
|
||||
$rootScope.$apply();
|
||||
|
||||
var name = $check.parent().text().trim();
|
||||
var index = agg.params.names.indexOf(name);
|
||||
expect(index).to.be(i);
|
||||
expect(agg.params.names).to.have.length(i + 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getResponseAggs', function () {
|
||||
it('creates a response agg for each name', function () {
|
||||
var aggs = agg.type.getResponseAggs(agg);
|
||||
expect(agg.params.names).to.eql(_.pluck(aggs, 'key'));
|
||||
});
|
||||
});
|
||||
}];
|
||||
});
|
94
test/unit/specs/components/fancy_forms/fancy_forms.js
Normal file
94
test/unit/specs/components/fancy_forms/fancy_forms.js
Normal file
|
@ -0,0 +1,94 @@
|
|||
define(function (require) {
|
||||
var $ = require('jquery');
|
||||
|
||||
describe('fancy forms', function () {
|
||||
var $baseEl = $('<form>').append(
|
||||
$('<input ng-model="val" required>')
|
||||
);
|
||||
|
||||
var $el;
|
||||
var $scope;
|
||||
var $compile;
|
||||
var $rootScope;
|
||||
var ngForm;
|
||||
var ngModel;
|
||||
|
||||
beforeEach(module('kibana'));
|
||||
beforeEach(inject(function ($injector) {
|
||||
$rootScope = $injector.get('$rootScope');
|
||||
$compile = $injector.get('$compile');
|
||||
|
||||
$scope = $rootScope.$new();
|
||||
$el = $baseEl.clone();
|
||||
|
||||
$compile($el)($scope);
|
||||
$scope.$apply();
|
||||
|
||||
ngForm = $el.controller('form');
|
||||
ngModel = $el.find('input').controller('ngModel');
|
||||
}));
|
||||
|
||||
describe('ngFormController', function () {
|
||||
it('counts errors', function () {
|
||||
expect(ngForm.errorCount()).to.be(1);
|
||||
});
|
||||
|
||||
it('clears errors', function () {
|
||||
$scope.val = 'someting';
|
||||
$scope.$apply();
|
||||
expect(ngForm.errorCount()).to.be(0);
|
||||
});
|
||||
|
||||
it('describes 0 errors', function () {
|
||||
$scope.val = 'someting';
|
||||
$scope.$apply();
|
||||
expect(ngForm.describeErrors()).to.be('0 Errors');
|
||||
});
|
||||
|
||||
it('describes 1 error', function () {
|
||||
$scope.$apply();
|
||||
expect(ngForm.describeErrors()).to.be('1 Error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ngModelController', function () {
|
||||
it('gives access to the ngFormController', function () {
|
||||
expect(ngModel.$getForm()).to.be(ngForm);
|
||||
});
|
||||
|
||||
it('allows setting the model dirty', function () {
|
||||
expect($el.find('input.ng-dirty')).to.have.length(0);
|
||||
ngModel.$setDirty();
|
||||
expect($el.find('input.ng-dirty')).to.have.length(1);
|
||||
});
|
||||
|
||||
it('sets the model dirty when it moves from valid to invalid', function () {
|
||||
// clear out the old scope/el
|
||||
$scope.$destroy();
|
||||
$el = $baseEl.clone();
|
||||
$scope = $rootScope.$new();
|
||||
|
||||
// start with a valid value
|
||||
$scope.val = 'something';
|
||||
$compile($el)($scope);
|
||||
$rootScope.$apply();
|
||||
|
||||
// ensure that the field is valid and pristinve
|
||||
var $valid = $el.find('input.ng-valid');
|
||||
expect($valid).to.have.length(1);
|
||||
expect($valid.hasClass('ng-pristine')).to.be(true);
|
||||
expect($valid.hasClass('ng-dirty')).to.be(false);
|
||||
|
||||
// remove the value without actually setting the view model
|
||||
$scope.val = null;
|
||||
$rootScope.$apply();
|
||||
|
||||
// ensure that the field is now invalid and dirty
|
||||
var $invalid = $el.find('input.ng-invalid');
|
||||
expect($invalid).to.have.length(1);
|
||||
expect($valid.hasClass('ng-pristine')).to.be(false);
|
||||
expect($valid.hasClass('ng-dirty')).to.be(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue