diff --git a/src/ui/public/agg_types/__tests__/metrics/get_values_at_path.js b/src/ui/public/agg_types/__tests__/metrics/get_values_at_path.js new file mode 100644 index 0000000000000..9d84ac1fb4069 --- /dev/null +++ b/src/ui/public/agg_types/__tests__/metrics/get_values_at_path.js @@ -0,0 +1,150 @@ +import getValuesAtPath from 'ui/agg_types/metrics/_get_values_at_path'; +import expect from 'expect.js'; + +describe('getValuesAtPath', function () { + it('non existing path', function () { + const values = getValuesAtPath({ aaa: 'bbb' }, [ 'not', 'in', 'there' ]); + expect(values).to.have.length(0); + }); + + it('non existing path in nested object', function () { + const json = { + aaa: { + bbb: 123 + } + }; + const values = getValuesAtPath(json, [ 'aaa', 'ccc' ]); + expect(values).to.have.length(0); + }); + + it('get value at level one', function () { + const values = getValuesAtPath({ aaa: 'bbb' }, [ 'aaa' ]); + expect(values).to.eql([ 'bbb' ]); + }); + + it('get nested value', function () { + const json = { + aaa: { + bbb: 123 + } + }; + const values = getValuesAtPath(json, [ 'aaa', 'bbb' ]); + expect(values).to.eql([ 123 ]); + }); + + it('value is an array', function () { + const json = { + aaa: [ 123, 456 ] + }; + const values = getValuesAtPath(json, [ 'aaa' ]); + expect(values).to.eql([ 123, 456 ]); + }); + + it('nested value is an array', function () { + const json = { + aaa: { + bbb: [ 123, 456 ] + } + }; + const values = getValuesAtPath(json, [ 'aaa', 'bbb' ]); + expect(values).to.eql([ 123, 456 ]); + }); + + it('multiple values are reachable via path', function () { + const json = { + aaa: [ + { + bbb: 123 + }, + { + bbb: 456 + } + ] + }; + const values = getValuesAtPath(json, [ 'aaa', 'bbb' ]); + expect(values).to.eql([ 123, 456 ]); + }); + + it('multiple values with some that are arrays are reachable via path', function () { + const json = { + aaa: [ + { + bbb: [ 123, 456 ] + }, + { + bbb: 789 + } + ] + }; + const values = getValuesAtPath(json, [ 'aaa', 'bbb' ]); + expect(values).to.eql([ 123, 456, 789 ]); + }); + + it('nested array mix', function () { + const json = { + aaa: [ + { + bbb: [ + { + ccc: 123 + }, + { + ccc: 456 + } + ] + }, + { + bbb: { + ccc: 789 + } + } + ] + }; + const values = getValuesAtPath(json, [ 'aaa', 'bbb', 'ccc' ]); + expect(values).to.eql([ 123, 456, 789 ]); + }); + + describe('nulls', function () { + it('on level 1', function () { + const json = { + aaa: null + }; + expect(getValuesAtPath(json, [ 'aaa' ])).to.have.length(0); + expect(getValuesAtPath(json, [ 'aaa', 'bbb' ])).to.have.length(0); + }); + + it('on level 2', function () { + const json = { + aaa: { + bbb: null + } + }; + expect(getValuesAtPath(json, [ 'aaa', 'bbb' ])).to.have.length(0); + expect(getValuesAtPath(json, [ 'aaa', 'bbb', 'ccc' ])).to.have.length(0); + }); + + it('in array', function () { + const json = { + aaa: [ + 123, + null + ] + }; + expect(getValuesAtPath(json, [ 'aaa' ])).to.eql([ 123 ]); + }); + + it('nested in array', function () { + const json = { + aaa: [ + { + bbb: 123 + }, + { + bbb: null + } + ] + }; + expect(getValuesAtPath(json, [ 'aaa', 'bbb' ])).to.eql([ 123 ]); + }); + }); +}); diff --git a/src/ui/public/agg_types/__tests__/param_types/_field.js b/src/ui/public/agg_types/__tests__/param_types/_field.js index cc2679fc0d4b8..813b48af48dfc 100644 --- a/src/ui/public/agg_types/__tests__/param_types/_field.js +++ b/src/ui/public/agg_types/__tests__/param_types/_field.js @@ -1,18 +1,21 @@ -import _ from 'lodash'; import expect from 'expect.js'; import ngMock from 'ng_mock'; +import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; import AggTypesParamTypesBaseProvider from 'ui/agg_types/param_types/base'; import AggTypesParamTypesFieldProvider from 'ui/agg_types/param_types/field'; + describe('Field', function () { let BaseAggParam; let FieldAggParam; + let indexPattern; beforeEach(ngMock.module('kibana')); // fetch out deps beforeEach(ngMock.inject(function (Private) { BaseAggParam = Private(AggTypesParamTypesBaseProvider); FieldAggParam = Private(AggTypesParamTypesFieldProvider); + indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); })); describe('constructor', function () { @@ -24,4 +27,38 @@ describe('Field', function () { expect(aggParam).to.be.a(BaseAggParam); }); }); + + describe('getFieldOptions', function () { + it('should return only aggregatable fields', function () { + const aggParam = new FieldAggParam({ + name: 'field' + }); + + const fields = aggParam.getFieldOptions({ + getIndexPattern: () => indexPattern + }); + for (let field of fields) { + expect(field.aggregatable).to.be(true); + } + }); + + it('should return all fields', function () { + const aggParam = new FieldAggParam({ + name: 'field' + }); + + aggParam.onlyAggregatable = false; + + const fields = aggParam.getFieldOptions({ + getIndexPattern: () => indexPattern + }); + let nAggregatable = 0; + for (let field of fields) { + if (field.aggregatable) { + nAggregatable++; + } + } + expect(fields.length - nAggregatable > 0).to.be(true); + }); + }); }); diff --git a/src/ui/public/agg_types/metrics/_get_values_at_path.js b/src/ui/public/agg_types/metrics/_get_values_at_path.js new file mode 100644 index 0000000000000..1e4a42fac640f --- /dev/null +++ b/src/ui/public/agg_types/metrics/_get_values_at_path.js @@ -0,0 +1,61 @@ +/** + * Returns the values at path, regardless if there are arrays on the way. + * Therefore, there is no need to specify the offset in an array. + * For example, for the path aaa.bbb and a JSON object like: + * + * { + * "aaa": [ + * { + * "bbb": 123 + * }, + * { + * "bbb": 456 + * } + * ] + * } + * + * the values returned are 123 and 456. + * + * + * @param json the JSON object + * @param path the path as an array + * @returns an array with all the values reachable from path + */ +export default function (json, path) { + if (!path || !path.length) { + return []; + } + + const values = []; + + const getValues = function (element, pathIndex) { + if (!element) { + return; + } + + if (pathIndex >= path.length) { + if (element) { + if (element.constructor === Array) { + for (let i = 0; i < element.length; i++) { + if (element[i]) { + values.push(element[i]); + } + } + } else { + values.push(element); + } + } + } else if (element.constructor === Object) { + if (element.hasOwnProperty(path[pathIndex])) { + getValues(element[path[pathIndex]], pathIndex + 1); + } + } else if (element.constructor === Array) { + for (let childi = 0; childi < element.length; childi++) { + getValues(element[childi], pathIndex); + } + } + }; + + getValues(json, 0); + return values; +}; diff --git a/src/ui/public/agg_types/metrics/top_hit.js b/src/ui/public/agg_types/metrics/top_hit.js index d9772d0ec7348..e6d0820e3e960 100644 --- a/src/ui/public/agg_types/metrics/top_hit.js +++ b/src/ui/public/agg_types/metrics/top_hit.js @@ -1,6 +1,7 @@ -import { get, has, noop } from 'lodash'; +import { isObject, get, has, noop } from 'lodash'; import MetricAggTypeProvider from 'ui/agg_types/metrics/metric_agg_type'; import topSortEditor from 'ui/agg_types/controls/top_sort.html'; +import getValuesAtPath from './_get_values_at_path'; export default function AggTypeMetricTopProvider(Private) { const MetricAggType = Private(MetricAggTypeProvider); @@ -15,6 +16,7 @@ export default function AggTypeMetricTopProvider(Private) { params: [ { name: 'field', + onlyAggregatable: false, filterFieldTypes: function (vis, value) { if (vis.type.name === 'table' || vis.type.name === 'metric') { return true; @@ -35,7 +37,10 @@ export default function AggTypeMetricTopProvider(Private) { } }; } else { - output.params.docvalue_fields = [ field.name ]; + if (field.doc_values) { + output.params.docvalue_fields = [ field.name ]; + } + output.params._source = field.name; } } }, @@ -71,10 +76,25 @@ export default function AggTypeMetricTopProvider(Private) { ], getValue(agg, bucket) { const hits = get(bucket, `${agg.id}.hits.hits`); - if (!hits || !hits.length || !has(hits[0], 'fields')) { - return; + if (!hits || !hits.length) { + return null; + } + const path = agg.params.field.name; + let values = getValuesAtPath(hits[0]._source, path.split('.')); + + if (!values.length && hits[0].fields) { + // no values found in the source, check the doc_values fields + values = hits[0].fields[path] || []; + } + + switch (values.length) { + case 0: + return null; + case 1: + return isObject(values[0]) ? JSON.stringify(values[0], null, ' ') : values [0]; + default: + return JSON.stringify(values, null, ' '); } - return hits[0].fields[agg.params.field.name] && hits[0].fields[agg.params.field.name][0]; } }); }; diff --git a/src/ui/public/agg_types/param_types/field.js b/src/ui/public/agg_types/param_types/field.js index 289afe6295fa1..62c4e52215897 100644 --- a/src/ui/public/agg_types/param_types/field.js +++ b/src/ui/public/agg_types/param_types/field.js @@ -18,6 +18,7 @@ export default function FieldAggParamFactory(Private, $filter) { FieldAggParam.prototype.editor = editorHtml; FieldAggParam.prototype.scriptable = true; FieldAggParam.prototype.filterFieldTypes = '*'; + FieldAggParam.prototype.onlyAggregatable = true; /** * Called to serialize values for saving an aggConfig object @@ -36,7 +37,9 @@ export default function FieldAggParamFactory(Private, $filter) { const indexPattern = aggConfig.getIndexPattern(); let fields = indexPattern.fields.raw; - fields = fields.filter(f => f.aggregatable); + if (this.onlyAggregatable) { + fields = fields.filter(f => f.aggregatable); + } if (!this.scriptable) { fields = fields.filter(field => !field.scripted);