Merge pull request #278 from spenceralger/master

WIP changes for incomming PR
This commit is contained in:
Spencer 2014-09-03 16:03:04 -07:00
commit 3db112657c
19 changed files with 764 additions and 24 deletions

View file

@ -10,8 +10,6 @@
- a legit way to update the index pattern
- **[src/kibana/apps/settings/sections/indices/_create.js](https://github.com/elasticsearch/kibana4/blob/master/src/kibana/apps/settings/sections/indices/_create.js)**
- we should probably display a message of some kind
- **[src/kibana/components/agg_types/buckets/terms.js](https://github.com/elasticsearch/kibana4/blob/master/src/kibana/components/agg_types/buckets/terms.js)**
- We need more than just _count here.
- **[src/kibana/components/index_patterns/_mapper.js](https://github.com/elasticsearch/kibana4/blob/master/src/kibana/components/index_patterns/_mapper.js)**
- Change index to be the resolved in some way, last three months, last hour, last year, whatever
- **[src/kibana/components/visualize/visualize.js](https://github.com/elasticsearch/kibana4/blob/master/src/kibana/components/visualize/visualize.js)**

View file

@ -10,9 +10,9 @@ Collection of `AggType` definition objects. See the [Vis component](../vis) for
### Included
- [`AggType`](_agg_type.js) class
- `AggParam` classes
- [`AggType`](_agg_type.js)
- `AggParam`
- [`BaseAggParam`](param_types/base.js)
- [`FieldAggParam`](param_types/field.js)
- [`OptionedAggParam`](param_types/optioned.js)
- [`AggParams`](_agg_params.js) class
- [`AggParams`](_agg_params.js)

View file

@ -7,11 +7,31 @@ define(function (require) {
var FieldAggParam = Private(require('components/agg_types/param_types/field'));
var OptionedAggParam = Private(require('components/agg_types/param_types/optioned'));
/**
* Wraps a list of {{#crossLink "AggParam"}}{{/crossLink}} objects; owned by an {{#crossLink "AggType"}}{{/crossLink}}
*
* used to create:
* - `OptionedAggParam` When the config has an array of `options: []`
* - `FieldAggParam` When the config has `name: "field"`
* - `BaseAggParam` All other params
*
* @class AggParams
* @constructor
* @extends Registry
* @param {object[]} params - array of params that get new-ed up as AggParam objects as descibed above
*/
_(AggParams).inherits(Registry);
function AggParams(params) {
if (_.isPlainObject(params)) {
// convert the names: details format into details[].name
params = _.map(params, function (param, name) {
param.name = name;
return param;
});
}
AggParams.Super.call(this, {
index: ['name'],
group: ['required'],
initialSet: params.map(function (param) {
if (param.name === 'field') {
return new FieldAggParam(param);
@ -26,6 +46,20 @@ define(function (require) {
});
}
/**
* Reads an aggConfigs
*
* @method write
* @param {AggConfig} aggConfig
* the AggConfig object who's type owns these aggParams and contains the param values for our param defs
* @param {object} [locals]
* an array of locals that will be available to the write function (can be used to enhance
* the quality of things like date_histogram's "auto" interval)
* @return {object} output
* output of the write calls, reduced into a single object. A `params: {}` property is exposed on the
* output object which is used to create the agg DSL for the search request. All other properties
* are dependent on the AggParam#write methods which should be studied for each AggType.
*/
AggParams.prototype.write = function (aggConfig, locals) {
var output = { params: {} };
locals = locals || {};

View file

@ -3,23 +3,67 @@ define(function (require) {
var _ = require('lodash');
var AggParams = Private(require('components/agg_types/_agg_params'));
/**
* Generic AggType Constructor
*
* Used to create the values exposed by the agg_types module.
*
* @class AggType
* @private
* @param {object} config - used to set the properties of the AggType
*/
function AggType(config) {
/**
* the unique, unchanging, name that elasticsearch has assigned this aggType
*
* @property name
* @type {string}
*/
this.name = config.name;
/**
* the user friendly name that will be shown in the ui for this aggType
*
* @property title
* @type {string}
*/
this.title = config.title;
/**
* a function that will be called when this aggType is assigned to
* an aggConfig, and that aggConfig is being rendered (in a form, chart, etc.).
*
* @method makeLabel
* @param {AggConfig} aggConfig - an agg config of this type
* @returns {string} - label that can be used in the ui to descripe the aggConfig
*/
this.makeLabel = config.makeLabel || _.constant(this.name);
/**
* Describes if this aggType creates data that is ordered, and if that ordered data
* is some sort of time series.
*
* If the aggType does not create ordered data, set this to something "falsey".
*
* If this does create orderedData, then the value should be an object.
*
* If the orderdata is some sort of time series, `this.ordered` should be an object
* with the property `date: true`
*
* @property ordered
* @type {object|undefined}
*/
this.ordered = config.ordered;
/**
* An instance of {{#crossLink "AggParams"}}{{/crossLink}}.
*
* @property params
* @type {AggParams}
*/
var params = this.params = config.params || [];
if (!(params instanceof AggParams)) {
if (_.isPlainObject(params)) {
// convert the names: details format into details[].name
params = _.map(params, function (param, name) {
param.name = name;
return param;
});
}
params = this.params = new AggParams(params);
}
}

View file

@ -28,10 +28,11 @@ define(function (require) {
editor: require('text!components/agg_types/controls/order_and_size.html'),
default: 'desc',
write: function (aggConfig, output) {
// TODO: We need more than just _count here.
output.params.order = {
_count: aggConfig.params.order.val
};
var metricAgg = _.first(aggConfig.vis.aggs.bySchemaGroup.metrics);
output.params.order = {};
output.params.order[metricAgg.id] = aggConfig.params.order.val;
}
}
]

View file

@ -20,8 +20,26 @@ define(function (require) {
});
});
/**
* Registry of Aggregation Types.
*
* These types form two groups, metric and buckets.
*
* @module agg_types
* @type {Registry}
*/
return new Registry({
/**
* @type {Array}
*/
index: ['name'],
/**
* [group description]
* @type {Array}
*/
group: ['type'],
initialSet: aggs.metrics.concat(aggs.buckets)
});

View file

@ -25,15 +25,19 @@ define(function (require) {
// one cache per instance of the Private service
var cache = {};
function Private(fn) {
function identify(fn) {
if (typeof fn !== 'function') {
throw new TypeError('Expected private module "' + fn + '" to be a function');
}
var id = fn.$$id;
if (id && cache[id]) return cache[id];
if (fn.$$id) return fn.$$id;
else return (fn.$$id = nextId());
}
if (!id) id = fn.$$id = nextId();
function Private(fn) {
var id = identify(fn);
if (cache[id]) return cache[id];
else if (~privPath.indexOf(id)) {
throw new Error(
'Circluar refrence to "' + name(fn) + '"' +
@ -54,6 +58,11 @@ define(function (require) {
return instance;
}
Private.stub = function (fn, val) {
cache[identify(fn)] = val;
return val;
};
return Private;
});
});

View file

@ -14,6 +14,9 @@ define(function (require) {
* Generic extension of Array class, which will index (and reindex) the
* objects it contains based on their properties.
*
* @class Registry
* @module utils
* @constructor
* @param {object} [config] - describes the properties of this registry object
* @param {string[]} [config.index] - a list of props/paths that should be used to index the docs.
* @param {string[]} [config.group] - a list of keys/paths to group docs by.

View file

@ -0,0 +1,17 @@
define(function (require) {
return function stubbedLogstashIndexPatternService(Private) {
var StubIndexPattern = Private(require('test_utils/stub_index_pattern'));
return new StubIndexPattern('logstash-*', 'time', [
{ type: 'number', name: 'bytes' },
{ type: 'boolean', name: 'ssl' },
{ type: 'date', name: '@timestamp' },
{ type: 'ip', name: 'ip' },
{ type: 'attachment', name: 'request_body' },
{ type: 'string', name: 'extension' },
{ type: 'geo_point', name: 'point' },
{ type: 'geo_shape', name: 'area' },
{ type: 'string', name: 'extension' },
{ type: 'conflict', name: 'custom_user_field' }
]);
};
});

View file

@ -87,7 +87,8 @@
'specs/factories/events',
'specs/index_patterns/_flatten_search_response',
'specs/utils/registry/index',
'specs/directives/filter_bar'
'specs/directives/filter_bar',
'specs/components/agg_types/index'
], function (kibana, sinon) {
kibana.load(function () {
var xhr = sinon.useFakeXMLHttpRequest();

View file

@ -0,0 +1,99 @@
define(function (require) {
return ['AggParams class', function () {
var _ = require('lodash');
var AggParams;
var BaseAggParam;
var FieldAggParam;
var OptionedAggParam;
beforeEach(module('kibana'));
// stub out the param classes before we get the AggParams
beforeEach(inject(require('specs/components/agg_types/utils/stub_agg_params')));
// fetch out deps
beforeEach(inject(function (Private) {
AggParams = Private(require('components/agg_types/_agg_params'));
BaseAggParam = Private(require('components/agg_types/param_types/base'));
FieldAggParam = Private(require('components/agg_types/param_types/field'));
OptionedAggParam = Private(require('components/agg_types/param_types/optioned'));
}));
describe('constructor args', function () {
it('accepts an object of params defs', function () {
var aggParams = new AggParams({
one: {},
two: {}
});
expect(aggParams).to.have.length(2);
expect(aggParams).to.be.an(Array);
expect(aggParams.byName).to.have.keys(['one', 'two']);
});
it('accepts an array of param defs', function () {
var aggParams = new AggParams([
{ name: 'one' },
{ name: 'two' }
]);
expect(aggParams).to.have.length(2);
expect(aggParams).to.be.an(Array);
expect(aggParams.byName).to.have.keys(['one', 'two']);
});
});
describe('AggParam creation', function () {
it('Uses the FieldAggParam class for params with the name "field"', function () {
var aggParams = new AggParams([
{ name: 'field' }
]);
expect(aggParams).to.have.length(1);
expect(aggParams[0]).to.be.a(FieldAggParam);
expect(aggParams[0]).to.be.a(BaseAggParam);
});
it('Uses the OptionedAggParam class for params with defined options', function () {
var aggParams = new AggParams([
{
name: 'interval',
options: [
{ display: 'Automatic', val: 'auto' },
{ display: '2 Hours', val: '2h' }
]
}
]);
expect(aggParams).to.have.length(1);
expect(aggParams[0]).to.be.a(OptionedAggParam);
expect(aggParams[0]).to.be.a(BaseAggParam);
});
it('Always converts the params to a BaseAggParam', function () {
var aggParams = new AggParams([
{
name: 'height',
editor: '<blink>high</blink>'
},
{
name: 'weight',
editor: '<blink>big</blink>'
},
{
name: 'waist',
editor: '<blink>small</blink>'
}
]);
expect(BaseAggParam).to.have.property('callCount', 3);
expect(FieldAggParam).to.have.property('callCount', 0);
expect(OptionedAggParam).to.have.property('callCount', 0);
expect(aggParams).to.have.length(3);
aggParams.forEach(function (aggParam) {
expect(aggParam).to.be.a(BaseAggParam);
});
});
});
}];
});

View file

@ -0,0 +1,100 @@
define(function (require) {
return ['AggType Class', function () {
var _ = require('lodash');
var sinon = require('test_utils/auto_release_sinon');
var AggType;
var AggParams;
require('services/private');
beforeEach(module('kibana'));
beforeEach(inject(function (Private) {
var AggParamsPM = require('components/agg_types/_agg_params');
AggParams = sinon.spy(Private(AggParamsPM));
Private.stub(AggParamsPM, AggParams);
AggType = Private(require('components/agg_types/_agg_type'));
}));
describe('constructor', function () {
it('requires a config object as it\'s first param', function () {
expect(function () {
new AggType(null);
}).to.throwError();
});
describe('application of config properties', function () {
var copiedConfigProps = [
'name',
'title',
'makeLabel',
'ordered'
];
describe('"' + copiedConfigProps.join('", "') + '"', function () {
it('assigns the config value to itself', function () {
var config = _.transform(copiedConfigProps, function (config, prop) {
config[prop] = {};
}, {});
var aggType = new AggType(config);
copiedConfigProps.forEach(function (prop) {
expect(aggType[prop]).to.be(config[prop]);
});
});
});
describe('makeLabel', function () {
it('makes a function when the makeLabel config is not specified', function () {
var someGetter = function () {};
var aggType = new AggType({
makeLabel: someGetter
});
expect(aggType.makeLabel).to.be(someGetter);
aggType = new AggType({
name: 'pizza'
});
expect(aggType.makeLabel).to.be.a('function');
expect(aggType.makeLabel()).to.be('pizza');
});
});
describe('params', function () {
it('defaults to an empty AggParams object', function () {
var aggType = new AggType({
name: 'smart agg'
});
expect(aggType.params).to.be.an(AggParams);
expect(aggType.params.length).to.be(0);
});
it('passes the params arg directly to the AggParams constructor', function () {
var params = [
{name: 'one'},
{name: 'two'}
];
var aggType = new AggType({
name: 'bucketeer',
params: params
});
expect(aggType.params).to.be.an(AggParams);
expect(aggType.params.length).to.be(2);
expect(AggParams.callCount).to.be(1);
expect(AggParams.firstCall.args[0]).to.be(params);
});
});
});
});
}];
});

View file

@ -0,0 +1,5 @@
define(function (require) {
return ['AggParams', function () {
}];
});

View file

@ -0,0 +1,113 @@
define(function (require) {
return ['Date Histogram Agg', function () {
var _ = require('lodash');
describe('ordered', function () {
var histogram;
beforeEach(module('kibana'));
beforeEach(inject(function (Private) {
histogram = Private(require('components/agg_types/index')).byName.histogram;
}));
it('is ordered', function () {
expect(histogram.ordered).to.be.ok();
});
it('is not ordered by date', function () {
expect(histogram.ordered).to.not.have.property('date');
});
});
describe('params', function () {
var paramWriter;
beforeEach(module('kibana'));
beforeEach(inject(function (Private) {
var AggParamWriter = Private(require('test_utils/agg_param_writer'));
paramWriter = new AggParamWriter({ aggType: 'histogram' });
}));
describe('interval', function () {
// reads aggConfig.params.interval, writes to DSL.interval
it('accepts a number', function () {
var output = paramWriter.write({ interval: 100 });
expect(output.params).to.have.property('interval', 100);
});
it('accepts a string', function () {
var output = paramWriter.write({ interval: '10' });
expect(output.params).to.have.property('interval', 10);
});
it('fails on non-numeric values', function () {
// template validation prevents this from users, not devs
var output = paramWriter.write({ interval: [] });
expect(isNaN(output.params.interval)).to.be.ok();
});
});
describe('min_doc_count', function () {
it('casts true values to 0', function () {
var output = paramWriter.write({ min_doc_count: true });
expect(output.params).to.have.property('min_doc_count', 0);
output = paramWriter.write({ min_doc_count: 'yes' });
expect(output.params).to.have.property('min_doc_count', 0);
output = paramWriter.write({ min_doc_count: 1 });
expect(output.params).to.have.property('min_doc_count', 0);
output = paramWriter.write({ min_doc_count: {} });
expect(output.params).to.have.property('min_doc_count', 0);
});
it('writes nothing for false values', function () {
var output = paramWriter.write({ min_doc_count: '' });
expect(output.params).to.not.have.property('min_doc_count');
output = paramWriter.write({ min_doc_count: null });
expect(output.params).to.not.have.property('min_doc_count');
output = paramWriter.write({ min_doc_count: undefined });
expect(output.params).to.not.have.property('min_doc_count');
});
});
describe('extended_bounds', function () {
it('writes when only eb.min is set', function () {
var output = paramWriter.write({
extended_bounds: { min: 0 }
});
expect(output.params.extended_bounds).to.have.property('min', 0);
expect(output.params.extended_bounds).to.have.property('max', undefined);
});
it('writes when only eb.max is set', function () {
var output = paramWriter.write({
extended_bounds: { max: 0 }
});
expect(output.params.extended_bounds).to.have.property('min', undefined);
expect(output.params.extended_bounds).to.have.property('max', 0);
});
it('writes when both eb.min and eb.max are set', function () {
var output = paramWriter.write({
extended_bounds: { min: 99, max: 100 }
});
expect(output.params.extended_bounds).to.have.property('min', 99);
expect(output.params.extended_bounds).to.have.property('max', 100);
});
it('does not write when nothing is set', function () {
var output = paramWriter.write({
extended_bounds: {}
});
expect(output.params).to.not.have.property('extended_bounds');
});
});
});
}];
});

View file

@ -0,0 +1,113 @@
define(function (require) {
return ['Histogram Agg', function () {
var _ = require('lodash');
describe('ordered', function () {
var histogram;
beforeEach(module('kibana'));
beforeEach(inject(function (Private) {
histogram = Private(require('components/agg_types/index')).byName.histogram;
}));
it('is ordered', function () {
expect(histogram.ordered).to.be.ok();
});
it('is not ordered by date', function () {
expect(histogram.ordered).to.not.have.property('date');
});
});
describe('params', function () {
var paramWriter;
beforeEach(module('kibana'));
beforeEach(inject(function (Private) {
var AggParamWriter = Private(require('test_utils/agg_param_writer'));
paramWriter = new AggParamWriter({ aggType: 'histogram' });
}));
describe('interval', function () {
// reads aggConfig.params.interval, writes to DSL.interval
it('accepts a number', function () {
var output = paramWriter.write({ interval: 100 });
expect(output.params).to.have.property('interval', 100);
});
it('accepts a string', function () {
var output = paramWriter.write({ interval: '10' });
expect(output.params).to.have.property('interval', 10);
});
it('fails on non-numeric values', function () {
// template validation prevents this from users, not devs
var output = paramWriter.write({ interval: [] });
expect(isNaN(output.params.interval)).to.be.ok();
});
});
describe('min_doc_count', function () {
it('casts true values to 0', function () {
var output = paramWriter.write({ min_doc_count: true });
expect(output.params).to.have.property('min_doc_count', 0);
output = paramWriter.write({ min_doc_count: 'yes' });
expect(output.params).to.have.property('min_doc_count', 0);
output = paramWriter.write({ min_doc_count: 1 });
expect(output.params).to.have.property('min_doc_count', 0);
output = paramWriter.write({ min_doc_count: {} });
expect(output.params).to.have.property('min_doc_count', 0);
});
it('writes nothing for false values', function () {
var output = paramWriter.write({ min_doc_count: '' });
expect(output.params).to.not.have.property('min_doc_count');
output = paramWriter.write({ min_doc_count: null });
expect(output.params).to.not.have.property('min_doc_count');
output = paramWriter.write({ min_doc_count: undefined });
expect(output.params).to.not.have.property('min_doc_count');
});
});
describe('extended_bounds', function () {
it('writes when only eb.min is set', function () {
var output = paramWriter.write({
extended_bounds: { min: 0 }
});
expect(output.params.extended_bounds).to.have.property('min', 0);
expect(output.params.extended_bounds).to.have.property('max', undefined);
});
it('writes when only eb.max is set', function () {
var output = paramWriter.write({
extended_bounds: { max: 0 }
});
expect(output.params.extended_bounds).to.have.property('min', undefined);
expect(output.params.extended_bounds).to.have.property('max', 0);
});
it('writes when both eb.min and eb.max are set', function () {
var output = paramWriter.write({
extended_bounds: { min: 99, max: 100 }
});
expect(output.params.extended_bounds).to.have.property('min', 99);
expect(output.params.extended_bounds).to.have.property('max', 100);
});
it('does not write when nothing is set', function () {
var output = paramWriter.write({
extended_bounds: {}
});
expect(output.params).to.not.have.property('extended_bounds');
});
});
});
}];
});

View file

@ -0,0 +1,13 @@
define(function (require) {
describe('AggTypesComponent', function () {
var childSuites = [
require('specs/components/agg_types/_agg_type'),
require('specs/components/agg_types/_agg_params'),
require('specs/components/agg_types/bucket_aggs/histogram'),
require('specs/components/agg_types/bucket_aggs/date_histogram'),
require('specs/components/agg_types/_metric_aggs')
].forEach(function (s) {
describe(s[0], s[1]);
});
});
});

View file

@ -0,0 +1,45 @@
define(function (require) {
var _ = require('lodash');
var sinon = require('test_utils/auto_release_sinon');
function ParamClassStub(parent, body) {
var stub = sinon.spy(body || function () {
stub.Super && stub.Super.call(this);
});
if (parent) _.inherits(stub, parent);
return stub;
}
/**
* stub all of the param classes, but ensure that they still inherit properly.
* This method should be passed directly to inject();
*
* ```js
* var stubParamClasses = require('specs/components/agg_types/utils/stub_agg_params');
* describe('something', function () {
* beforeEach(inject(stubParamClasses));
* })
* ```
*
* @param {PrivateLoader} Private - The private module loader, inject by passing this function to inject()
* @return {undefined}
*/
return function stubParamClasses(Private) {
var BaseAggParam = Private.stub(
require('components/agg_types/param_types/base'),
ParamClassStub(null, function (config) {
_.assign(this, config);
})
);
Private.stub(
require('components/agg_types/param_types/field'),
ParamClassStub(BaseAggParam)
);
Private.stub(
require('components/agg_types/param_types/optioned'),
ParamClassStub(BaseAggParam)
);
};
});

View file

@ -0,0 +1,100 @@
define(function (require) {
return function AggParamWriterHelper(Private) {
var _ = require('lodash');
var Vis = Private(require('components/vis/vis'));
var aggTypes = Private(require('components/agg_types/index'));
var visTypes = Private(require('components/vis_types/index'));
var stubbedLogstashIndexPattern = Private(require('fixtures/stubbed_logstash_index_pattern'));
/**
* Helper object for writing aggParams. Specify an aggType and it will find a vis & schema, and
* wire up the supporting objects required to feed in parameters, and get #write() output.
*
* Use cases:
* - Verify that the interval parameter of the histogram visualization casts it's input to a number
* ```js
* it('casts to a number', function () {
* var writer = new AggParamWriter({ aggType: 'histogram' });
* var output = writer.write({ interval : '100/10' });
* expect(output.params.interval).to.be.a('number');
* expect(output.params.interval).to.be(100);
* });
* ```
*
* @class AggParamWriter
* @param {object} opts - describe the properties of this paramWriter
* @param {string} opts.aggType - the name of the aggType we want to test. ('histogram', 'filter', etc.)
*/
function AggParamWriter(opts) {
var self = this;
self.aggType = opts.aggType;
if (_.isString(self.aggType)) {
self.aggType = aggTypes.byName[self.aggType];
}
// not configurable right now, but totally required
self.indexPattern = stubbedLogstashIndexPattern;
// the vis type we will use to write the aggParams
self.visType = null;
// the schema that the aggType satisfies
self.visAggSchema = null;
// find a suitable vis type and schema
_.find(visTypes, function (visType) {
var schema = _.find(visType.schemas.all, function (schema) {
// type, type, type, type, type... :(
return schema.group === self.aggType.type;
});
if (schema) {
self.visType = visType;
self.visAggSchema = schema;
return true;
}
});
if (!self.aggType || !self.visType || !self.visAggSchema) {
throw new Error('unable to find a usable visType and schema for the ' + opts.aggType + ' agg type');
}
self.vis = new Vis(self.indexPattern, {
type: self.visType
});
}
AggParamWriter.prototype.write = function (paramValues) {
var self = this;
paramValues = _.clone(paramValues);
if (self.aggType.params.byName.field && !paramValues.field) {
// pick a field rather than force a field to be specified everywhere
if (self.aggType.type === 'metrics') {
paramValues.field = _.sample(self.indexPattern.fields.byType.number);
} else {
paramValues.field = _.sample(self.indexPattern.fields.byType.string);
}
}
self.vis.setState({
type: self.vis.type.name,
aggs: [{
type: self.aggType,
schema: self.visAggSchema,
params: paramValues
}]
});
var aggConfig = _.find(self.vis.aggs, function (aggConfig) {
return aggConfig.type === self.aggType;
});
return aggConfig.type.params.write(aggConfig);
};
return AggParamWriter;
};
});

View file

@ -0,0 +1,27 @@
define(function (require) {
return function (Private) {
var Registry = require('utils/registry/registry');
var fieldFormats = Private(require('components/index_patterns/_field_formats'));
function StubIndexPattern(pattern, timeField, fields) {
this.fields = new Registry({
index: ['name'],
group: ['type'],
initialSet: fields.map(function (field) {
field.count = field.count || 0;
// non-enumerable type so that it does not get included in the JSON
Object.defineProperty(field, 'format', {
enumerable: false,
get: function () {
fieldFormats.defaultByType[field.type];
}
});
return field;
})
});
}
return StubIndexPattern;
};
});