mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
add top_hit metric (#9809)
Backports PR #7302 **Commit 1:** add top_hits aggregation * Original sha:22bab6246e
* Authored by Stéphane Campinas <stephane.campinas@gmail.com> on 2016-05-26T22:02:08Z **Commit 2:** support nested fields * Original sha:8aef2b8f42
* Authored by Stéphane Campinas <stephane.campinas@gmail.com> on 2016-06-11T14:57:35Z **Commit 3:** added choice of the order and select by default the index pattern associated time field * Original sha:9791ac50e4
* Authored by Stéphane Campinas <stephane.campinas@gmail.com> on 2016-06-14T21:08:00Z **Commit 4:** improved parameters name and changed aggregation from latest to top to better reflect what this aggregation may accomplish * Original sha:692f41cc6f
* Authored by Stéphane Campinas <stephane.campinas@gmail.com> on 2016-06-22T20:32:12Z **Commit 5:** made variable a constant * Original sha:39cddab05c
* Authored by Stéphane Campinas <stephane.campinas@gmail.com> on 2016-07-01T22:42:13Z **Commit 6:** use shorter name for variable * Original sha:c6494ccc9c
* Authored by Stéphane Campinas <stephane.campinas@gmail.com> on 2016-07-27T08:50:44Z **Commit 7:** prevent to sort terms on top_hits * Original sha:b365ddc797
* Authored by Stéphane Campinas <stephane.campinas@gmail.com> on 2016-08-28T09:47:47Z **Commit 8:** Added the top_hits agg to line/pie/tile vis. Added support for function in prop_filter to allow more flexibility when filtering the list of values. The _term order is now the default ordering instead of _custom. * Original sha:be44e905af
* Authored by Stéphane Campinas <stephane.campinas@gmail.com> on 2016-09-19T14:53:48Z **Commit 9:** simplified tests * Original sha:43767f0929
* Authored by Stéphane Campinas <stephane.campinas@gmail.com> on 2016-10-11T09:29:11Z **Commit 10:** changed as per code review * Original sha:53469d51a6
* Authored by Stéphane Campinas <stephane.campinas@gmail.com> on 2016-10-11T21:49:36Z **Commit 11:** Top Hit should support all field types on the metric vis * Original sha:20be95006f
* Authored by Stéphane Campinas <stephane.campinas@gmail.com> on 2016-10-17T21:18:31Z **Commit 12:** renamed metric to top hit * Original sha:20cbb12e67
* Authored by Stéphane Campinas <stephane.campinas@gmail.com> on 2016-10-17T21:28:30Z **Commit 13:** check if there is any result from buckets * Original sha:b99d6c76dd
* Authored by Stéphane Campinas <stephane.campinas@gmail.com> on 2016-10-18T09:13:19Z **Commit 14:** support scripted fields * Original sha:f79cdc1531
* Authored by Stéphane Campinas <stephane.campinas@gmail.com> on 2016-10-23T20:58:55Z **Commit 15:** Merge branch 'master' of github.com:elastic/kibana into latest-value * Original sha:82060ce91f
* Authored by Stéphane Campinas <stephane.campinas@gmail.com> on 2016-11-01T09:47:19Z **Commit 16:** Merge branch 'master' of github.com:elastic/kibana into latest-value * Original sha:257729868c
* Authored by Stéphane Campinas <stephane.campinas@gmail.com> on 2016-11-12T11:30:42Z **Commit 17:** - added logic for trying to get the field value from the source or from a doc_values field. - added onlyAggregatable option for a field agg param to decide whether or not to retain only aggregatable fields. * Original sha:6a2bc0188c
* Authored by Stéphane Campinas <stephane.campinas@gmail.com> on 2016-11-13T12:34:10Z **Commit 18:** improved loops in getValuesAtPath method * Original sha:991864cdd3
* Authored by Stéphane Campinas <stephane.campinas@gmail.com> on 2016-11-16T10:10:28Z **Commit 19:** Merge branch 'master' of github.com:elastic/kibana into latest-value * Original sha:bc1c90ac06
* Authored by Stéphane Campinas <stephane.campinas@gmail.com> on 2016-11-17T23:51:13Z **Commit 20:** do not try to get the doc_values field of an IP field * Original sha:d697f11ca1
* Authored by Stéphane Campinas <stephane.campinas@gmail.com> on 2016-11-18T00:49:34Z **Commit 21:** improved field tests on the onlyAggregatable option * Original sha:c2249e525f
* Authored by Stéphane Campinas <stephane.campinas@gmail.com> on 2016-11-18T01:08:34Z **Commit 22:** use indexPattern.formatField to get all the values at a given path * Original sha:f4200e1bc0
* Authored by Stéphane Campinas <stephane.campinas@gmail.com> on 2016-11-19T23:02:18Z **Commit 23:** do not show the analyzed warning * Original sha:2dda53dfb7
* Authored by Stéphane Campinas <stephane.campinas@gmail.com> on 2016-11-19T23:30:59Z **Commit 24:** support to sort on scripted field * Original sha:eda9610fa2
* Authored by Stéphane Campinas <stephane.campinas@gmail.com> on 2016-11-19T23:52:15Z **Commit 25:** corrected tests and rely on the field's formatter instead of trying to return a nice string * Original sha:b465438176
* Authored by Stéphane Campinas <stephane.campinas@gmail.com> on 2016-11-20T14:40:43Z **Commit 26:** do not rely on init_default_field_props to set the doc_values property * Original sha:19e2104745
* Authored by Stéphane Campinas <stephane.campinas@gmail.com> on 2016-11-21T23:11:29Z **Commit 27:** added test for the doc_values property * Original sha:fd323f3c2a
* Authored by Stéphane Campinas <stephane.campinas@gmail.com> on 2016-11-23T10:04:16Z **Commit 28:** Merge branch 'master' of github.com:elastic/kibana into latest-value * Original sha:cfed72ec95
* Authored by Stéphane Campinas <stephane.campinas@gmail.com> on 2016-12-05T15:47:45Z **Commit 29:** - corrected source formatter - add option to show/hide analyzed warning of a field * Original sha:30901820de
* Authored by Stéphane Campinas <stephane.campinas@gmail.com> on 2016-12-05T22:40:14Z **Commit 30:** set the default value of the showAnalyzedWarning in fieldparam * Original sha:0d924fc73d
* Authored by Stéphane Campinas <stephane.campinas@gmail.com> on 2016-12-08T16:29:46Z **Commit 31:** added description of the showAnalyzedWarning option * Original sha:7f85803f73
* Authored by Stéphane Campinas <stephane.campinas@gmail.com> on 2016-12-08T16:39:07Z **Commit 32:** use aggParam object to access the options of the field parameter * Original sha:ef95cebf3f
* Authored by Stéphane Campinas <stephane.campinas@gmail.com> on 2016-12-09T10:54:51Z **Commit 33:** flattenHit: support object in array * Original sha:b64f92a90e
* Authored by Stéphane Campinas <stephane.campinas@gmail.com> on 2016-12-10T23:36:45Z **Commit 34:** added aggregation option to support multivalued fields and top_hits aggregation with a size different than 1 * Original sha:e18a30d292
* Authored by Stéphane Campinas <stephane.campinas@gmail.com> on 2016-12-17T13:49:17Z **Commit 35:** disable concatenate aggregation type for visualizations other than table and metric * Original sha:2a2fa67d2a
* Authored by Stéphane Campinas <stephane.campinas@gmail.com> on 2016-12-17T15:00:08Z **Commit 36:** corrected display of arrays in the metric vis * Original sha:eb70e84a56
* Authored by Stéphane Campinas <stephane.campinas@gmail.com> on 2016-12-17T15:27:36Z **Commit 37:** removed condition on the IP field type when retrieving doc values * Original sha:d19ba5698c
* Authored by Stéphane Campinas <stephane.campinas@gmail.com> on 2016-12-17T21:30:47Z **Commit 38:** - do not set a default value for the aggregate with property since it is * Original sha:66d3b31778
* Authored by Stéphane Campinas <stephane.campinas@gmail.com> on 2016-12-21T23:04:12Z **Commit 39:** filter out incompatble aggregate options and auto select the option if there is only one * Original sha:1bd472755d
* Authored by Stéphane Campinas <stephane.campinas@gmail.com> on 2016-12-28T17:14:21Z **Commit 40:** simplified condition * Original sha:5babf9a512
* Authored by Stéphane Campinas <stephane.campinas@gmail.com> on 2016-12-28T23:49:46Z **Commit 41:** lint * Original sha:7372fced4b
* Authored by Stéphane Campinas <stephane.campinas@gmail.com> on 2016-12-30T19:43:08Z **Commit 42:** fix min/max/sum/average aggregate of elements having only null/undefined values * Original sha:0791be7914
* Authored by Stéphane Campinas <stephane.campinas@gmail.com> on 2017-01-03T10:40:16Z **Commit 43:** added top_hits metric to heatmap visualization * Original sha:c967a2344d
* Authored by Stéphane Campinas <stephane.campinas@gmail.com> on 2017-01-03T11:16:56Z
This commit is contained in:
parent
6020d157ee
commit
bc61824fd8
31 changed files with 1043 additions and 285 deletions
|
@ -65,7 +65,7 @@ export default function HeatmapVisType(Private) {
|
|||
title: 'Value',
|
||||
min: 1,
|
||||
max: 1,
|
||||
aggFilter: ['count', 'avg', 'median', 'sum', 'min', 'max', 'cardinality', 'std_dev'],
|
||||
aggFilter: ['count', 'avg', 'median', 'sum', 'min', 'max', 'cardinality', 'std_dev', 'top_hits'],
|
||||
defaults: [
|
||||
{ schema: 'metric', type: 'count' }
|
||||
]
|
||||
|
|
|
@ -70,7 +70,7 @@ export default function HistogramVisType(Private) {
|
|||
title: 'Dot Size',
|
||||
min: 0,
|
||||
max: 1,
|
||||
aggFilter: ['count', 'avg', 'sum', 'min', 'max', 'cardinality']
|
||||
aggFilter: ['count', 'avg', 'sum', 'min', 'max', 'cardinality', 'top_hits']
|
||||
},
|
||||
{
|
||||
group: 'buckets',
|
||||
|
|
|
@ -44,7 +44,7 @@ export default function HistogramVisType(Private) {
|
|||
title: 'Slice Size',
|
||||
min: 1,
|
||||
max: 1,
|
||||
aggFilter: ['sum', 'count', 'cardinality'],
|
||||
aggFilter: ['sum', 'count', 'cardinality', 'top_hits'],
|
||||
defaults: [
|
||||
{ schema: 'metric', type: 'count' }
|
||||
]
|
||||
|
|
|
@ -89,7 +89,7 @@ export default function TileMapVisType(Private, getAppState, courier, config) {
|
|||
title: 'Value',
|
||||
min: 1,
|
||||
max: 1,
|
||||
aggFilter: ['count', 'avg', 'sum', 'min', 'max', 'cardinality'],
|
||||
aggFilter: ['count', 'avg', 'sum', 'min', 'max', 'cardinality', 'top_hits'],
|
||||
defaults: [
|
||||
{ schema: 'metric', type: 'count' }
|
||||
]
|
||||
|
|
|
@ -82,11 +82,18 @@ uiModules
|
|||
// build collection of agg params html
|
||||
type.params.forEach(function (param, i) {
|
||||
let aggParam;
|
||||
let fields;
|
||||
// if field param exists, compute allowed fields
|
||||
if (param.name === 'field') {
|
||||
fields = $aggParamEditorsScope.indexedFields;
|
||||
} else if (param.type === 'field') {
|
||||
fields = $aggParamEditorsScope[`${param.name}Options`] = getIndexedFields(param);
|
||||
}
|
||||
|
||||
if ($aggParamEditorsScope.indexedFields) {
|
||||
const hasIndexedFields = $aggParamEditorsScope.indexedFields.length > 0;
|
||||
if (fields) {
|
||||
const hasIndexedFields = fields.length > 0;
|
||||
const isExtraParam = i > 0;
|
||||
if (!hasIndexedFields && isExtraParam) { // don't draw the rest of the options if their are no indexed fields.
|
||||
if (!hasIndexedFields && isExtraParam) { // don't draw the rest of the options if there are no indexed fields.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -133,6 +140,31 @@ uiModules
|
|||
.append(param.editor)
|
||||
.get(0);
|
||||
}
|
||||
|
||||
function getIndexedFields(param) {
|
||||
let fields = _.filter($scope.agg.vis.indexPattern.fields.raw, 'aggregatable');
|
||||
const fieldTypes = param.filterFieldTypes;
|
||||
|
||||
if (fieldTypes) {
|
||||
const filter = _.isFunction(fieldTypes) ? fieldTypes.bind(this, $scope.agg.vis) : fieldTypes;
|
||||
fields = $filter('fieldType')(fields, filter);
|
||||
fields = $filter('orderBy')(fields, ['type', 'name']);
|
||||
}
|
||||
|
||||
return new IndexedArray({
|
||||
|
||||
/**
|
||||
* @type {Array}
|
||||
*/
|
||||
index: ['name'],
|
||||
|
||||
/**
|
||||
* [group description]
|
||||
* @type {Array}
|
||||
*/
|
||||
initialSet: fields
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
|
|
@ -1,100 +0,0 @@
|
|||
import initDefaultFieldProps from '../init_default_field_props';
|
||||
import expect from 'expect.js';
|
||||
import _ from 'lodash';
|
||||
let fields;
|
||||
|
||||
const testData = [
|
||||
{
|
||||
'name': 'ip',
|
||||
'type': 'ip'
|
||||
}, {
|
||||
'name': '@timestamp',
|
||||
'type': 'date'
|
||||
}, {
|
||||
'name': 'agent',
|
||||
'type': 'string'
|
||||
}, {
|
||||
'name': 'bytes',
|
||||
'type': 'number'
|
||||
},
|
||||
{
|
||||
'name': 'geo.coordinates',
|
||||
'type': 'geo_point'
|
||||
}
|
||||
];
|
||||
|
||||
describe('initDefaultFieldProps', function () {
|
||||
|
||||
beforeEach(function () {
|
||||
fields = _.cloneDeep(testData);
|
||||
});
|
||||
|
||||
it('should throw an error if no argument is passed or the argument is not an array', function () {
|
||||
expect(initDefaultFieldProps).to.throwException(/requires an array argument/);
|
||||
expect(initDefaultFieldProps).withArgs({}).to.throwException(/requires an array argument/);
|
||||
});
|
||||
|
||||
it('should set the same defaults for everything but strings', function () {
|
||||
const results = initDefaultFieldProps(fields);
|
||||
_.forEach(results, function (field) {
|
||||
if (field.type !== 'string') {
|
||||
expect(field).to.have.property('indexed', true);
|
||||
expect(field).to.have.property('analyzed', false);
|
||||
expect(field).to.have.property('doc_values', true);
|
||||
expect(field).to.have.property('scripted', false);
|
||||
expect(field).to.have.property('count', 0);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should make string fields analyzed', function () {
|
||||
const results = initDefaultFieldProps(fields);
|
||||
_.forEach(results, function (field) {
|
||||
if (field.type === 'string' && !_.contains(field.name, 'keyword')) {
|
||||
expect(field).to.have.property('indexed', true);
|
||||
expect(field).to.have.property('analyzed', true);
|
||||
expect(field).to.have.property('doc_values', false);
|
||||
expect(field).to.have.property('scripted', false);
|
||||
expect(field).to.have.property('count', 0);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should create an extra raw non-analyzed field for strings', function () {
|
||||
const results = initDefaultFieldProps(fields);
|
||||
const rawField = _.find(results, function (field) {
|
||||
return _.contains(field.name, 'keyword');
|
||||
});
|
||||
expect(rawField).to.have.property('indexed', true);
|
||||
expect(rawField).to.have.property('analyzed', false);
|
||||
expect(rawField).to.have.property('doc_values', true);
|
||||
expect(rawField).to.have.property('scripted', false);
|
||||
expect(rawField).to.have.property('count', 0);
|
||||
});
|
||||
|
||||
it('should apply some overrides to metafields', function () {
|
||||
const results = initDefaultFieldProps([{name: '_source'}, {name: '_timestamp'}]);
|
||||
const expected = [
|
||||
{
|
||||
name: '_source',
|
||||
indexed: false,
|
||||
analyzed: false,
|
||||
doc_values: false,
|
||||
count: 0,
|
||||
scripted: false,
|
||||
type: '_source'
|
||||
},
|
||||
{
|
||||
name: '_timestamp',
|
||||
indexed: true,
|
||||
analyzed: false,
|
||||
doc_values: false,
|
||||
count: 0,
|
||||
scripted: false,
|
||||
type: 'date'
|
||||
}
|
||||
];
|
||||
|
||||
expect(_.isEqual(expected, results)).to.be.ok();
|
||||
});
|
||||
});
|
|
@ -1,50 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
import mappingOverrides from './mapping_overrides';
|
||||
|
||||
module.exports = function initDefaultFieldProps(fields) {
|
||||
if (fields === undefined || !_.isArray(fields)) {
|
||||
throw new Error('requires an array argument');
|
||||
}
|
||||
|
||||
const results = [];
|
||||
|
||||
_.forEach(fields, function (field) {
|
||||
const newField = _.cloneDeep(field);
|
||||
results.push(newField);
|
||||
|
||||
if (newField.type === 'string') {
|
||||
_.defaults(newField, {
|
||||
indexed: true,
|
||||
analyzed: true,
|
||||
doc_values: false,
|
||||
scripted: false,
|
||||
count: 0
|
||||
});
|
||||
|
||||
results.push({
|
||||
name: newField.name + '.keyword',
|
||||
type: 'string',
|
||||
indexed: true,
|
||||
analyzed: false,
|
||||
doc_values: true,
|
||||
scripted: false,
|
||||
count: 0
|
||||
});
|
||||
}
|
||||
else {
|
||||
_.defaults(newField, {
|
||||
indexed: true,
|
||||
analyzed: false,
|
||||
doc_values: true,
|
||||
scripted: false,
|
||||
count: 0
|
||||
});
|
||||
}
|
||||
|
||||
if (mappingOverrides[newField.name]) {
|
||||
_.assign(newField, mappingOverrides[newField.name]);
|
||||
}
|
||||
});
|
||||
|
||||
return results;
|
||||
};
|
|
@ -1,38 +0,0 @@
|
|||
export default {
|
||||
_source: {
|
||||
type: '_source',
|
||||
indexed: false,
|
||||
analyzed: false,
|
||||
doc_values: false
|
||||
},
|
||||
_index: {
|
||||
type: 'string',
|
||||
indexed: false,
|
||||
analyzed: false,
|
||||
doc_values: false
|
||||
},
|
||||
_type: {
|
||||
type: 'string',
|
||||
indexed: false,
|
||||
analyzed: false,
|
||||
doc_values: false
|
||||
},
|
||||
_id: {
|
||||
type: 'string',
|
||||
indexed: false,
|
||||
analyzed: false,
|
||||
doc_values: false
|
||||
},
|
||||
_timestamp: {
|
||||
type: 'date',
|
||||
indexed: true,
|
||||
analyzed: false,
|
||||
doc_values: false
|
||||
},
|
||||
_score: {
|
||||
type: 'number',
|
||||
indexed: false,
|
||||
analyzed: false,
|
||||
doc_values: false
|
||||
}
|
||||
};
|
|
@ -20,15 +20,8 @@ describe('metric vis', function () {
|
|||
it('should set the metric label and value', function () {
|
||||
$scope.processTableGroups({
|
||||
tables: [{
|
||||
columns: [{title: 'Count'}],
|
||||
rows: [[4301021]],
|
||||
aggConfig: function () {
|
||||
return {
|
||||
fieldFormatter: function () {
|
||||
return formatter;
|
||||
}
|
||||
};
|
||||
}
|
||||
columns: [{ title: 'Count' }],
|
||||
rows: [[ { toString: () => formatter(4301021) } ]]
|
||||
}]
|
||||
});
|
||||
|
||||
|
@ -44,14 +37,7 @@ describe('metric vis', function () {
|
|||
{title: '1st percentile of bytes'},
|
||||
{title: '99th percentile of bytes'}
|
||||
],
|
||||
rows: [[182, 445842.4634666484]],
|
||||
aggConfig: function () {
|
||||
return {
|
||||
fieldFormatter: function () {
|
||||
return formatter;
|
||||
}
|
||||
};
|
||||
}
|
||||
rows: [[ { toString: () => formatter(182) }, { toString: () => formatter(445842.4634666484) } ]]
|
||||
}]
|
||||
});
|
||||
|
||||
|
|
|
@ -17,14 +17,11 @@ module.controller('KbnMetricVisController', function ($scope, $element, Private)
|
|||
$scope.processTableGroups = function (tableGroups) {
|
||||
tableGroups.tables.forEach(function (table) {
|
||||
table.columns.forEach(function (column, i) {
|
||||
const fieldFormatter = table.aggConfig(column).fieldFormatter();
|
||||
let value = table.rows[0][i];
|
||||
|
||||
value = isInvalid(value) ? '?' : fieldFormatter(value);
|
||||
const value = table.rows[0][i];
|
||||
|
||||
metrics.push({
|
||||
label: column.title,
|
||||
value: value
|
||||
value: value.toString('html')
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -32,8 +29,12 @@ module.controller('KbnMetricVisController', function ($scope, $element, Private)
|
|||
|
||||
$scope.$watch('esResponse', function (resp) {
|
||||
if (resp) {
|
||||
const options = {
|
||||
asAggConfigResults: true
|
||||
};
|
||||
|
||||
metrics.length = 0;
|
||||
$scope.processTableGroups(tabifyAggResponse($scope.vis, resp));
|
||||
$scope.processTableGroups(tabifyAggResponse($scope.vis, resp, options));
|
||||
$element.trigger('renderComplete');
|
||||
}
|
||||
});
|
||||
|
|
|
@ -5,7 +5,7 @@ function stubbedLogstashFields() {
|
|||
// | | |aggregatable
|
||||
// | | | |searchable
|
||||
// name type | | | | |metadata
|
||||
['bytes', 'number', true, true, true, true, { count: 10 } ],
|
||||
['bytes', 'number', true, true, true, true, { count: 10, docValues: true } ],
|
||||
['ssl', 'boolean', true, true, true, true, { count: 20 } ],
|
||||
['@timestamp', 'date', true, true, true, true, { count: 30 } ],
|
||||
['time', 'date', true, true, true, true, { count: 30 } ],
|
||||
|
@ -20,6 +20,7 @@ function stubbedLogstashFields() {
|
|||
['geo.coordinates', 'geo_point', true, true, true, true ],
|
||||
['extension', 'string', true, true, true, true ],
|
||||
['machine.os', 'string', true, true, true, true ],
|
||||
['machine.os.raw', 'string', true, false, true, true, { docValues: true } ],
|
||||
['geo.src', 'string', true, true, true, true ],
|
||||
['_id', 'string', false, false, true, true ],
|
||||
['_type', 'string', false, false, true, true ],
|
||||
|
@ -41,6 +42,7 @@ function stubbedLogstashFields() {
|
|||
] = row;
|
||||
|
||||
const {
|
||||
docValues = false,
|
||||
count = 0,
|
||||
script,
|
||||
lang = script ? 'expression' : undefined,
|
||||
|
@ -50,6 +52,7 @@ function stubbedLogstashFields() {
|
|||
return {
|
||||
name,
|
||||
type,
|
||||
doc_values: docValues,
|
||||
indexed,
|
||||
analyzed,
|
||||
aggregatable,
|
||||
|
|
|
@ -8,6 +8,7 @@ import getComputedFields from 'ui/index_patterns/_get_computed_fields';
|
|||
import RegistryFieldFormatsProvider from 'ui/registry/field_formats';
|
||||
import IndexPatternsFlattenHitProvider from 'ui/index_patterns/_flatten_hit';
|
||||
import IndexPatternsFieldProvider from 'ui/index_patterns/_field';
|
||||
|
||||
export default function (Private) {
|
||||
let fieldFormats = Private(RegistryFieldFormatsProvider);
|
||||
let flattenHit = Private(IndexPatternsFlattenHitProvider);
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
describe('Terms Agg', function () {
|
||||
describe('order agg editor UI', function () {
|
||||
it('defaults to the first metric agg');
|
||||
it('adds "custom metric" option');
|
||||
it('lists all metric agg responses');
|
||||
it('lists individual values of a multi-value metric');
|
||||
it('selects "custom metric" if there are no metric aggs');
|
||||
it('is emptied if the selected metric is removed');
|
||||
it('displays a metric editor if "custom metric" is selected');
|
||||
it('saves the "custom metric" to state and refreshes from it');
|
||||
it('invalidates the form if the metric agg form is not complete');
|
||||
});
|
||||
});
|
156
src/ui/public/agg_types/__tests__/buckets/terms.js
Normal file
156
src/ui/public/agg_types/__tests__/buckets/terms.js
Normal file
|
@ -0,0 +1,156 @@
|
|||
import expect from 'expect.js';
|
||||
import ngMock from 'ng_mock';
|
||||
import AggTypesIndexProvider from 'ui/agg_types/index';
|
||||
|
||||
describe('Terms Agg', function () {
|
||||
describe('order agg editor UI', function () {
|
||||
|
||||
let $rootScope;
|
||||
|
||||
function init({ responseValueAggs = [] }) {
|
||||
ngMock.module('kibana');
|
||||
ngMock.inject(function (Private, $controller, _$rootScope_) {
|
||||
const terms = Private(AggTypesIndexProvider).byName.terms;
|
||||
const orderAggController = terms.params.byName.orderAgg.controller;
|
||||
|
||||
$rootScope = _$rootScope_;
|
||||
$rootScope.agg = {
|
||||
id: 'test',
|
||||
params: {},
|
||||
type: terms,
|
||||
vis: {
|
||||
aggs: []
|
||||
}
|
||||
};
|
||||
$rootScope.responseValueAggs = responseValueAggs;
|
||||
$controller(orderAggController, { $scope: $rootScope });
|
||||
$rootScope.$digest();
|
||||
});
|
||||
}
|
||||
|
||||
it('defaults to the first metric agg', function () {
|
||||
init({
|
||||
responseValueAggs: [
|
||||
{
|
||||
id: 'agg1',
|
||||
type: {
|
||||
name: 'count'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'agg2',
|
||||
type: {
|
||||
name: 'count'
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
expect($rootScope.agg.params.orderBy).to.be('agg1');
|
||||
});
|
||||
|
||||
it('defaults to the first metric agg that is compatible with the terms bucket', function () {
|
||||
init({
|
||||
responseValueAggs: [
|
||||
{
|
||||
id: 'agg1',
|
||||
type: {
|
||||
name: 'top_hits'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'agg2',
|
||||
type: {
|
||||
name: 'percentiles'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'agg3',
|
||||
type: {
|
||||
name: 'median'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'agg4',
|
||||
type: {
|
||||
name: 'std_dev'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'agg5',
|
||||
type: {
|
||||
name: 'count'
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
expect($rootScope.agg.params.orderBy).to.be('agg5');
|
||||
});
|
||||
|
||||
it('defaults to the _term metric if no agg is compatible', function () {
|
||||
init({
|
||||
responseValueAggs: [
|
||||
{
|
||||
id: 'agg1',
|
||||
type: {
|
||||
name: 'top_hits'
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
expect($rootScope.agg.params.orderBy).to.be('_term');
|
||||
});
|
||||
|
||||
it('selects _term if there are no metric aggs', function () {
|
||||
init({});
|
||||
expect($rootScope.agg.params.orderBy).to.be('_term');
|
||||
});
|
||||
|
||||
it('selects _term if the selected metric becomes incompatible', function () {
|
||||
init({
|
||||
responseValueAggs: [
|
||||
{
|
||||
id: 'agg1',
|
||||
type: {
|
||||
name: 'count'
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
expect($rootScope.agg.params.orderBy).to.be('agg1');
|
||||
$rootScope.responseValueAggs = [
|
||||
{
|
||||
id: 'agg1',
|
||||
type: {
|
||||
name: 'top_hits'
|
||||
}
|
||||
}
|
||||
];
|
||||
$rootScope.$digest();
|
||||
expect($rootScope.agg.params.orderBy).to.be('_term');
|
||||
});
|
||||
|
||||
it('selects _term if the selected metric is removed', function () {
|
||||
init({
|
||||
responseValueAggs: [
|
||||
{
|
||||
id: 'agg1',
|
||||
type: {
|
||||
name: 'count'
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
expect($rootScope.agg.params.orderBy).to.be('agg1');
|
||||
$rootScope.responseValueAggs = [];
|
||||
$rootScope.$digest();
|
||||
expect($rootScope.agg.params.orderBy).to.be('_term');
|
||||
});
|
||||
|
||||
it('adds "custom metric" option');
|
||||
it('lists all metric agg responses');
|
||||
it('lists individual values of a multi-value metric');
|
||||
it('displays a metric editor if "custom metric" is selected');
|
||||
it('saves the "custom metric" to state and refreshes from it');
|
||||
it('invalidates the form if the metric agg form is not complete');
|
||||
});
|
||||
});
|
342
src/ui/public/agg_types/__tests__/metrics/top_hit.js
Normal file
342
src/ui/public/agg_types/__tests__/metrics/top_hit.js
Normal file
|
@ -0,0 +1,342 @@
|
|||
import _ from 'lodash';
|
||||
import expect from 'expect.js';
|
||||
import ngMock from 'ng_mock';
|
||||
import TopHitProvider from 'ui/agg_types/metrics/top_hit';
|
||||
import VisProvider from 'ui/vis';
|
||||
import StubbedIndexPattern from 'fixtures/stubbed_logstash_index_pattern';
|
||||
|
||||
describe('Top hit metric', function () {
|
||||
let aggDsl;
|
||||
let topHitMetric;
|
||||
let aggConfig;
|
||||
|
||||
function init({ field, sortOrder = 'desc', aggregate = 'concat', size = 1 }) {
|
||||
ngMock.module('kibana');
|
||||
ngMock.inject(function (Private) {
|
||||
const Vis = Private(VisProvider);
|
||||
const indexPattern = Private(StubbedIndexPattern);
|
||||
topHitMetric = Private(TopHitProvider);
|
||||
|
||||
const params = {};
|
||||
if (field) {
|
||||
params.field = field;
|
||||
}
|
||||
params.sortOrder = {
|
||||
val: sortOrder
|
||||
};
|
||||
params.aggregate = {
|
||||
val: aggregate
|
||||
};
|
||||
params.size = size;
|
||||
const vis = new Vis(indexPattern, {
|
||||
title: 'New Visualization',
|
||||
type: 'metric',
|
||||
params: {
|
||||
fontSize: 60,
|
||||
handleNoResults: true
|
||||
},
|
||||
aggs: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'top_hits',
|
||||
schema: 'metric',
|
||||
params
|
||||
}
|
||||
],
|
||||
listeners: {}
|
||||
});
|
||||
|
||||
// Grab the aggConfig off the vis (we don't actually use the vis for anything else)
|
||||
aggConfig = vis.aggs[0];
|
||||
aggDsl = aggConfig.toDsl();
|
||||
});
|
||||
}
|
||||
|
||||
it('should return a label prefixed with Last if sorting in descending order', function () {
|
||||
init({ field: 'bytes' });
|
||||
expect(topHitMetric.makeLabel(aggConfig)).to.eql('Last bytes');
|
||||
});
|
||||
|
||||
it('should return a label prefixed with First if sorting in ascending order', function () {
|
||||
init({
|
||||
field: 'bytes',
|
||||
sortOrder: 'asc'
|
||||
});
|
||||
expect(topHitMetric.makeLabel(aggConfig)).to.eql('First bytes');
|
||||
});
|
||||
|
||||
it('should request the _source field', function () {
|
||||
init({ field: '_source' });
|
||||
expect(aggDsl.top_hits._source).to.be(true);
|
||||
expect(aggDsl.top_hits.docvalue_fields).to.be(undefined);
|
||||
});
|
||||
|
||||
it('should request both for the source and doc_values fields', function () {
|
||||
init({ field: 'bytes' });
|
||||
expect(aggDsl.top_hits._source).to.be('bytes');
|
||||
expect(aggDsl.top_hits.docvalue_fields).to.eql([ 'bytes' ]);
|
||||
});
|
||||
|
||||
it('should only request for the source if the field does not have the doc_values property', function () {
|
||||
init({ field: 'ssl' });
|
||||
expect(aggDsl.top_hits._source).to.be('ssl');
|
||||
expect(aggDsl.top_hits.docvalue_fields).to.be(undefined);
|
||||
});
|
||||
|
||||
describe('try to get the value from the top hit', function () {
|
||||
it('should return null if there is no hit', function () {
|
||||
const bucket = {
|
||||
'1': {
|
||||
hits: {
|
||||
hits: []
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
init({ field: '@tags' });
|
||||
expect(topHitMetric.getValue(aggConfig, bucket)).to.be(null);
|
||||
});
|
||||
|
||||
it('should return undefined if the field does not appear in the source', function () {
|
||||
const bucket = {
|
||||
'1': {
|
||||
hits: {
|
||||
hits: [
|
||||
{
|
||||
_source: {
|
||||
bytes: 123
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
init({ field: '@tags' });
|
||||
expect(topHitMetric.getValue(aggConfig, bucket)).to.be(undefined);
|
||||
});
|
||||
|
||||
it('should return the field value from the top hit', function () {
|
||||
const bucket = {
|
||||
'1': {
|
||||
hits: {
|
||||
hits: [
|
||||
{
|
||||
_source: {
|
||||
'@tags': 'aaa'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
init({ field: '@tags' });
|
||||
expect(topHitMetric.getValue(aggConfig, bucket)).to.be('aaa');
|
||||
});
|
||||
|
||||
it('should return the object if the field value is an object', function () {
|
||||
const bucket = {
|
||||
'1': {
|
||||
hits: {
|
||||
hits: [
|
||||
{
|
||||
_source: {
|
||||
'@tags': {
|
||||
label: 'aaa'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
init({ field: '@tags' });
|
||||
expect(topHitMetric.getValue(aggConfig, bucket)).to.eql({ label: 'aaa' });
|
||||
});
|
||||
|
||||
it('should return an array if the field has more than one values', function () {
|
||||
const bucket = {
|
||||
'1': {
|
||||
hits: {
|
||||
hits: [
|
||||
{
|
||||
_source: {
|
||||
'@tags': [ 'aaa', 'bbb' ]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
init({ field: '@tags' });
|
||||
expect(topHitMetric.getValue(aggConfig, bucket)).to.eql([ 'aaa', 'bbb' ]);
|
||||
});
|
||||
|
||||
it('should get the value from the doc_values field if the source does not have that field', function () {
|
||||
const bucket = {
|
||||
'1': {
|
||||
hits: {
|
||||
hits: [
|
||||
{
|
||||
_source: {
|
||||
'machine.os': 'linux'
|
||||
},
|
||||
fields: {
|
||||
'machine.os.raw': [ 'linux' ]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
init({ field: 'machine.os.raw' });
|
||||
expect(topHitMetric.getValue(aggConfig, bucket)).to.be('linux');
|
||||
});
|
||||
|
||||
it('should return undefined if the field is not in the source nor in the doc_values field', function () {
|
||||
const bucket = {
|
||||
'1': {
|
||||
hits: {
|
||||
hits: [
|
||||
{
|
||||
_source: {
|
||||
bytes: 12345
|
||||
},
|
||||
fields: {
|
||||
bytes: 12345
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
init({ field: 'machine.os.raw' });
|
||||
expect(topHitMetric.getValue(aggConfig, bucket)).to.be(undefined);
|
||||
});
|
||||
|
||||
describe('Multivalued field and first/last X docs', function () {
|
||||
it('should return a label prefixed with Last X docs if sorting in descending order', function () {
|
||||
init({
|
||||
field: 'bytes',
|
||||
size: 2
|
||||
});
|
||||
expect(topHitMetric.makeLabel(aggConfig)).to.eql('Last 2 bytes');
|
||||
});
|
||||
|
||||
it('should return a label prefixed with First X docs if sorting in ascending order', function () {
|
||||
init({
|
||||
field: 'bytes',
|
||||
size: 2,
|
||||
sortOrder: 'asc'
|
||||
});
|
||||
expect(topHitMetric.makeLabel(aggConfig)).to.eql('First 2 bytes');
|
||||
});
|
||||
|
||||
[
|
||||
{
|
||||
description: 'concat values with a comma',
|
||||
type: 'concat',
|
||||
data: [ 1, 2, 3 ],
|
||||
result: [ 1, 2, 3 ]
|
||||
},
|
||||
{
|
||||
description: 'sum up the values',
|
||||
type: 'sum',
|
||||
data: [ 1, 2, 3 ],
|
||||
result: 6
|
||||
},
|
||||
{
|
||||
description: 'take the minimum value',
|
||||
type: 'min',
|
||||
data: [ 1, 2, 3 ],
|
||||
result: 1
|
||||
},
|
||||
{
|
||||
description: 'take the maximum value',
|
||||
type: 'max',
|
||||
data: [ 1, 2, 3 ],
|
||||
result: 3
|
||||
},
|
||||
{
|
||||
description: 'take the average value',
|
||||
type: 'average',
|
||||
data: [ 1, 2, 3 ],
|
||||
result: 2
|
||||
},
|
||||
{
|
||||
description: 'support null/undefined',
|
||||
type: 'min',
|
||||
data: [ undefined, null ],
|
||||
result: null
|
||||
},
|
||||
{
|
||||
description: 'support null/undefined',
|
||||
type: 'max',
|
||||
data: [ undefined, null ],
|
||||
result: null
|
||||
},
|
||||
{
|
||||
description: 'support null/undefined',
|
||||
type: 'sum',
|
||||
data: [ undefined, null ],
|
||||
result: null
|
||||
},
|
||||
{
|
||||
description: 'support null/undefined',
|
||||
type: 'average',
|
||||
data: [ undefined, null ],
|
||||
result: null
|
||||
}
|
||||
]
|
||||
.forEach(agg => {
|
||||
it(`should return the result of the ${agg.type} aggregation over the last doc - ${agg.description}`, function () {
|
||||
const bucket = {
|
||||
'1': {
|
||||
hits: {
|
||||
hits: [
|
||||
{
|
||||
_source: {
|
||||
bytes: agg.data
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
init({ field: 'bytes', aggregate: agg.type });
|
||||
expect(topHitMetric.getValue(aggConfig, bucket)).to.eql(agg.result);
|
||||
});
|
||||
|
||||
it(`should return the result of the ${agg.type} aggregation over the last X docs - ${agg.description}`, function () {
|
||||
const bucket = {
|
||||
'1': {
|
||||
hits: {
|
||||
hits: [
|
||||
{
|
||||
_source: {
|
||||
bytes: _.dropRight(agg.data, 1)
|
||||
}
|
||||
},
|
||||
{
|
||||
_source: {
|
||||
bytes: _.last(agg.data)
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
init({ field: 'bytes', aggregate: agg.type });
|
||||
expect(topHitMetric.getValue(aggConfig, bucket)).to.eql(agg.result);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,18 +1,22 @@
|
|||
import _ from 'lodash';
|
||||
import expect from 'expect.js';
|
||||
import { reject } from 'lodash';
|
||||
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 +28,34 @@ describe('Field', function () {
|
|||
expect(aggParam).to.be.a(BaseAggParam);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFieldOptions', function () {
|
||||
it('should return only aggregatable fields by default', function () {
|
||||
const aggParam = new FieldAggParam({
|
||||
name: 'field'
|
||||
});
|
||||
|
||||
const fields = aggParam.getFieldOptions({
|
||||
getIndexPattern: () => indexPattern
|
||||
});
|
||||
expect(fields).to.not.have.length(0);
|
||||
for (const field of fields) {
|
||||
expect(field.aggregatable).to.be(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('should return all fields if onlyAggregatable is false', function () {
|
||||
const aggParam = new FieldAggParam({
|
||||
name: 'field'
|
||||
});
|
||||
|
||||
aggParam.onlyAggregatable = false;
|
||||
|
||||
const fields = aggParam.getFieldOptions({
|
||||
getIndexPattern: () => indexPattern
|
||||
});
|
||||
const nonAggregatableFields = reject(fields, 'aggregatable');
|
||||
expect(nonAggregatableFields).to.not.be.empty();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -16,12 +16,13 @@ export default function TermsAggDefinition(Private) {
|
|||
let createFilter = Private(AggTypesBucketsCreateFilterTermsProvider);
|
||||
const routeBasedNotifier = Private(routeBasedNotifierProvider);
|
||||
|
||||
const aggFilter = ['!top_hits', '!percentiles', '!median', '!std_dev'];
|
||||
let orderAggSchema = (new Schemas([
|
||||
{
|
||||
group: 'none',
|
||||
name: 'orderAgg',
|
||||
title: 'Order Agg',
|
||||
aggFilter: ['!percentiles', '!median', '!std_dev']
|
||||
aggFilter: aggFilter
|
||||
}
|
||||
])).all[0];
|
||||
|
||||
|
@ -94,9 +95,15 @@ export default function TermsAggDefinition(Private) {
|
|||
$scope.$watch('responseValueAggs', updateOrderAgg);
|
||||
$scope.$watch('agg.params.orderBy', updateOrderAgg);
|
||||
|
||||
// Returns true if the agg is not compatible with the terms bucket
|
||||
$scope.rejectAgg = function (agg) {
|
||||
// aggFilter elements all starts with a '!'
|
||||
// so the index of agg.type.name in a filter is 1 if it is included
|
||||
return Boolean(aggFilter.find((filter) => filter.indexOf(agg.type.name) === 1));
|
||||
};
|
||||
|
||||
function updateOrderAgg() {
|
||||
let agg = $scope.agg;
|
||||
let aggs = agg.vis.aggs;
|
||||
let params = agg.params;
|
||||
let orderBy = params.orderBy;
|
||||
let paramDef = agg.type.params.byName.orderAgg;
|
||||
|
@ -105,7 +112,11 @@ export default function TermsAggDefinition(Private) {
|
|||
if (!orderBy && prevOrderBy === INIT) {
|
||||
// abort until we get the responseValueAggs
|
||||
if (!$scope.responseValueAggs) return;
|
||||
params.orderBy = (_.first($scope.responseValueAggs) || { id: 'custom' }).id;
|
||||
let respAgg = _($scope.responseValueAggs).filter((agg) => !$scope.rejectAgg(agg)).first();
|
||||
if (!respAgg) {
|
||||
respAgg = { id: '_term' };
|
||||
}
|
||||
params.orderBy = respAgg.id;
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -115,15 +126,10 @@ export default function TermsAggDefinition(Private) {
|
|||
// we aren't creating a custom aggConfig
|
||||
if (!orderBy || orderBy !== 'custom') {
|
||||
params.orderAgg = null;
|
||||
|
||||
if (orderBy === '_term') {
|
||||
params.orderBy = '_term';
|
||||
return;
|
||||
}
|
||||
|
||||
// ensure that orderBy is set to a valid agg
|
||||
if (!_.find($scope.responseValueAggs, { id: orderBy })) {
|
||||
params.orderBy = null;
|
||||
const respAgg = _($scope.responseValueAggs).filter((agg) => !$scope.rejectAgg(agg)).find({ id: orderBy });
|
||||
if (!respAgg) {
|
||||
params.orderBy = '_term';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
Field
|
||||
</label>
|
||||
|
||||
<span class="pull-right text-warning hintbox-label" ng-show="agg.params.field.analyzed"
|
||||
<span class="pull-right text-warning hintbox-label" ng-show="aggParam.showAnalyzedWarning && agg.params.field.analyzed"
|
||||
ng-click="showAnalyzedFieldWarning = !showAnalyzedFieldWarning">
|
||||
<i class="fa fa-warning"></i> Analyzed Field
|
||||
</span>
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
<option
|
||||
ng-repeat="respAgg in responseValueAggs track by respAgg.id"
|
||||
value="{{respAgg.id}}"
|
||||
ng-disabled="rejectAgg(respAgg)"
|
||||
ng-selected="agg.params.orderBy === respAgg.id">
|
||||
metric: {{safeMakeLabel(respAgg)}}
|
||||
</option>
|
||||
|
@ -27,4 +28,4 @@
|
|||
group-name="'metrics'">
|
||||
</vis-editor-agg-params>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
37
src/ui/public/agg_types/controls/top_aggregate_and_size.html
Normal file
37
src/ui/public/agg_types/controls/top_aggregate_and_size.html
Normal file
|
@ -0,0 +1,37 @@
|
|||
<div ng-controller="aggParam.controller" class="vis-editor-agg-form-row">
|
||||
<div class="form-group">
|
||||
<label>
|
||||
Aggregate With
|
||||
<kbn-info
|
||||
info="Choose a strategy for combining multiple hits or a multi-valued field into a single metric."
|
||||
placement="right">
|
||||
</kbn-info>
|
||||
</label>
|
||||
|
||||
<select
|
||||
required
|
||||
name="aggregate"
|
||||
ng-model="agg.params.aggregate"
|
||||
ng-options="opt as opt.display for opt in options| orderBy: 'display' track by opt.val"
|
||||
class="form-control"
|
||||
></select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>
|
||||
Size
|
||||
<kbn-info
|
||||
info="Request top-K hits. Multiple hits will be combined via 'aggregate with'."
|
||||
placement="right">
|
||||
</kbn-info>
|
||||
</label>
|
||||
|
||||
<input
|
||||
required
|
||||
name="size"
|
||||
ng-model="agg.params.size"
|
||||
class="form-control"
|
||||
type="number"
|
||||
min="1"
|
||||
></input>
|
||||
</div>
|
||||
</div>
|
29
src/ui/public/agg_types/controls/top_sort.html
Normal file
29
src/ui/public/agg_types/controls/top_sort.html
Normal file
|
@ -0,0 +1,29 @@
|
|||
<div class="form-group">
|
||||
<label for="sort">
|
||||
Sort On
|
||||
</label>
|
||||
|
||||
<select
|
||||
class="form-control"
|
||||
name="sortField"
|
||||
required
|
||||
ng-model="agg.params.sortField"
|
||||
ng-show="sortFieldOptions.length"
|
||||
auto-select-if-only-one="sortFieldOptions"
|
||||
ng-options="field as field.displayName group by field.type for field in sortFieldOptions">
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>
|
||||
Order
|
||||
</label>
|
||||
|
||||
<select
|
||||
name="sortOrder"
|
||||
ng-model="agg.params.sortOrder"
|
||||
required
|
||||
ng-options="opt as opt.display for opt in aggParam.options track by opt.val"
|
||||
class="form-control">
|
||||
</select>
|
||||
</div>
|
|
@ -6,6 +6,7 @@ import AggTypesMetricsSumProvider from 'ui/agg_types/metrics/sum';
|
|||
import AggTypesMetricsMedianProvider from 'ui/agg_types/metrics/median';
|
||||
import AggTypesMetricsMinProvider from 'ui/agg_types/metrics/min';
|
||||
import AggTypesMetricsMaxProvider from 'ui/agg_types/metrics/max';
|
||||
import AggTypesMetricsTopHitProvider from 'ui/agg_types/metrics/top_hit';
|
||||
import AggTypesMetricsStdDeviationProvider from 'ui/agg_types/metrics/std_deviation';
|
||||
import AggTypesMetricsCardinalityProvider from 'ui/agg_types/metrics/cardinality';
|
||||
import AggTypesMetricsPercentilesProvider from 'ui/agg_types/metrics/percentiles';
|
||||
|
@ -32,7 +33,8 @@ export default function AggTypeService(Private) {
|
|||
Private(AggTypesMetricsStdDeviationProvider),
|
||||
Private(AggTypesMetricsCardinalityProvider),
|
||||
Private(AggTypesMetricsPercentilesProvider),
|
||||
Private(AggTypesMetricsPercentileRanksProvider)
|
||||
Private(AggTypesMetricsPercentileRanksProvider),
|
||||
Private(AggTypesMetricsTopHitProvider)
|
||||
],
|
||||
buckets: [
|
||||
Private(AggTypesBucketsDateHistogramProvider),
|
||||
|
|
203
src/ui/public/agg_types/metrics/top_hit.js
Normal file
203
src/ui/public/agg_types/metrics/top_hit.js
Normal file
|
@ -0,0 +1,203 @@
|
|||
import _ from 'lodash';
|
||||
import MetricAggTypeProvider from 'ui/agg_types/metrics/metric_agg_type';
|
||||
import RegistryFieldFormatsProvider from 'ui/registry/field_formats';
|
||||
import topSortEditor from 'ui/agg_types/controls/top_sort.html';
|
||||
import aggregateAndSizeEditor from 'ui/agg_types/controls/top_aggregate_and_size.html';
|
||||
|
||||
export default function AggTypeMetricTopProvider(Private) {
|
||||
const MetricAggType = Private(MetricAggTypeProvider);
|
||||
const fieldFormats = Private(RegistryFieldFormatsProvider);
|
||||
|
||||
const isNumber = function (type) {
|
||||
return type === 'number';
|
||||
};
|
||||
|
||||
return new MetricAggType({
|
||||
name: 'top_hits',
|
||||
title: 'Top Hit',
|
||||
makeLabel: function (aggConfig) {
|
||||
let prefix = aggConfig.params.sortOrder.val === 'desc' ? 'Last' : 'First';
|
||||
if (aggConfig.params.size !== 1) {
|
||||
prefix += ` ${aggConfig.params.size}`;
|
||||
}
|
||||
return `${prefix} ${aggConfig.params.field.displayName}`;
|
||||
},
|
||||
params: [
|
||||
{
|
||||
name: 'field',
|
||||
onlyAggregatable: false,
|
||||
showAnalyzedWarning: false,
|
||||
filterFieldTypes: function (vis, value) {
|
||||
if (vis.type.name === 'table' || vis.type.name === 'metric') {
|
||||
return true;
|
||||
}
|
||||
return value === 'number';
|
||||
},
|
||||
write(agg, output) {
|
||||
const field = agg.params.field;
|
||||
output.params = {};
|
||||
|
||||
if (field.scripted) {
|
||||
output.params.script_fields = {
|
||||
[ field.name ]: {
|
||||
script: {
|
||||
inline: field.script,
|
||||
lang: field.lang
|
||||
}
|
||||
}
|
||||
};
|
||||
} else {
|
||||
if (field.doc_values) {
|
||||
output.params.docvalue_fields = [ field.name ];
|
||||
}
|
||||
output.params._source = field.name === '_source' ? true : field.name;
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'aggregate',
|
||||
type: 'optioned',
|
||||
editor: aggregateAndSizeEditor,
|
||||
options: [
|
||||
{
|
||||
display: 'Min',
|
||||
isCompatibleType: isNumber,
|
||||
isCompatibleVis: _.constant(true),
|
||||
disabled: true,
|
||||
val: 'min'
|
||||
},
|
||||
{
|
||||
display: 'Max',
|
||||
isCompatibleType: isNumber,
|
||||
isCompatibleVis: _.constant(true),
|
||||
disabled: true,
|
||||
val: 'max'
|
||||
},
|
||||
{
|
||||
display: 'Sum',
|
||||
isCompatibleType: isNumber,
|
||||
isCompatibleVis: _.constant(true),
|
||||
disabled: true,
|
||||
val: 'sum'
|
||||
},
|
||||
{
|
||||
display: 'Average',
|
||||
isCompatibleType: isNumber,
|
||||
isCompatibleVis: _.constant(true),
|
||||
disabled: true,
|
||||
val: 'average'
|
||||
},
|
||||
{
|
||||
display: 'Concatenate',
|
||||
isCompatibleType: _.constant(true),
|
||||
isCompatibleVis: function (name) {
|
||||
return name === 'metric' || name === 'table';
|
||||
},
|
||||
disabled: true,
|
||||
val: 'concat'
|
||||
}
|
||||
],
|
||||
controller: function ($scope) {
|
||||
$scope.options = [];
|
||||
$scope.$watchGroup([ 'agg.vis.type.name', 'agg.params.field.type' ], function ([ visName, fieldType ]) {
|
||||
if (fieldType && visName) {
|
||||
$scope.options = _.filter($scope.aggParam.options, option => {
|
||||
return option.isCompatibleVis(visName) && option.isCompatibleType(fieldType);
|
||||
});
|
||||
if ($scope.options.length === 1) {
|
||||
$scope.agg.params.aggregate = $scope.options[0];
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
write: _.noop
|
||||
},
|
||||
{
|
||||
name: 'size',
|
||||
editor: null, // size setting is done together with the aggregation setting
|
||||
default: 1
|
||||
},
|
||||
{
|
||||
name: 'sortField',
|
||||
type: 'field',
|
||||
editor: null,
|
||||
filterFieldTypes: [ 'number', 'date', 'ip', 'string' ],
|
||||
default: function (agg) {
|
||||
return agg.vis.indexPattern.timeFieldName;
|
||||
},
|
||||
write: _.noop // prevent default write, it is handled below
|
||||
},
|
||||
{
|
||||
name: 'sortOrder',
|
||||
type: 'optioned',
|
||||
default: 'desc',
|
||||
editor: topSortEditor,
|
||||
options: [
|
||||
{ display: 'Descending', val: 'desc' },
|
||||
{ display: 'Ascending', val: 'asc' }
|
||||
],
|
||||
write(agg, output) {
|
||||
const sortField = agg.params.sortField;
|
||||
const sortOrder = agg.params.sortOrder;
|
||||
|
||||
if (sortField.scripted) {
|
||||
output.params.sort = [
|
||||
{
|
||||
_script: {
|
||||
script: {
|
||||
inline: sortField.script,
|
||||
lang: sortField.lang
|
||||
},
|
||||
type: sortField.type,
|
||||
order: sortOrder.val
|
||||
}
|
||||
}
|
||||
];
|
||||
} else {
|
||||
output.params.sort = [
|
||||
{
|
||||
[ sortField.name ]: {
|
||||
order: sortOrder.val
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
getValue(agg, bucket) {
|
||||
const hits = _.get(bucket, `${agg.id}.hits.hits`);
|
||||
if (!hits || !hits.length) {
|
||||
return null;
|
||||
}
|
||||
const path = agg.params.field.name;
|
||||
|
||||
let values = _(hits).map(hit => {
|
||||
return path === '_source' ? hit._source : agg.vis.indexPattern.flattenHit(hit, true)[path];
|
||||
})
|
||||
.flatten()
|
||||
.value();
|
||||
|
||||
if (values.length === 1) {
|
||||
values = values[0];
|
||||
}
|
||||
|
||||
if (_.isArray(values)) {
|
||||
if (!_.compact(values).length) {
|
||||
return null;
|
||||
}
|
||||
switch (agg.params.aggregate.val) {
|
||||
case 'max':
|
||||
return _.max(values);
|
||||
case 'min':
|
||||
return _.min(values);
|
||||
case 'sum':
|
||||
return _.sum(values);
|
||||
case 'average':
|
||||
return _.sum(values) / values.length;
|
||||
}
|
||||
}
|
||||
return values;
|
||||
}
|
||||
});
|
||||
}
|
|
@ -7,7 +7,7 @@ import IndexedArray from 'ui/indexed_array';
|
|||
import Notifier from 'ui/notify/notifier';
|
||||
|
||||
export default function FieldAggParamFactory(Private, $filter) {
|
||||
let BaseAggParam = Private(AggTypesParamTypesBaseProvider);
|
||||
const BaseAggParam = Private(AggTypesParamTypesBaseProvider);
|
||||
const notifier = new Notifier();
|
||||
|
||||
_.class(FieldAggParam).inherits(BaseAggParam);
|
||||
|
@ -18,6 +18,10 @@ export default function FieldAggParamFactory(Private, $filter) {
|
|||
FieldAggParam.prototype.editor = editorHtml;
|
||||
FieldAggParam.prototype.scriptable = true;
|
||||
FieldAggParam.prototype.filterFieldTypes = '*';
|
||||
// retain only the fields with the aggregatable property if the onlyAggregatable option is true
|
||||
FieldAggParam.prototype.onlyAggregatable = true;
|
||||
// show a warning about the field being analyzed
|
||||
FieldAggParam.prototype.showAnalyzedWarning = true;
|
||||
|
||||
/**
|
||||
* Called to serialize values for saving an aggConfig object
|
||||
|
@ -36,14 +40,20 @@ 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);
|
||||
}
|
||||
|
||||
if (this.filterFieldTypes) {
|
||||
fields = $filter('fieldType')(fields, this.filterFieldTypes);
|
||||
let filters = this.filterFieldTypes;
|
||||
if (_.isFunction(this.filterFieldTypes)) {
|
||||
filters = this.filterFieldTypes.bind(this, aggConfig.vis);
|
||||
}
|
||||
fields = $filter('fieldType')(fields, filters);
|
||||
fields = $filter('orderBy')(fields, ['type', 'name']);
|
||||
}
|
||||
|
||||
|
@ -88,7 +98,7 @@ export default function FieldAggParamFactory(Private, $filter) {
|
|||
* @return {undefined}
|
||||
*/
|
||||
FieldAggParam.prototype.write = function (aggConfig, output) {
|
||||
let field = aggConfig.getField();
|
||||
const field = aggConfig.getField();
|
||||
|
||||
if (!field) {
|
||||
throw new TypeError('"field" is a required parameter');
|
||||
|
@ -105,4 +115,4 @@ export default function FieldAggParamFactory(Private, $filter) {
|
|||
};
|
||||
|
||||
return FieldAggParam;
|
||||
};
|
||||
}
|
||||
|
|
58
src/ui/public/filters/__tests__/prop_filter.js
Normal file
58
src/ui/public/filters/__tests__/prop_filter.js
Normal file
|
@ -0,0 +1,58 @@
|
|||
import expect from 'expect.js';
|
||||
import propFilter from 'ui/filters/_prop_filter';
|
||||
|
||||
describe('prop filter', function () {
|
||||
let nameFilter;
|
||||
|
||||
beforeEach(function () {
|
||||
nameFilter = propFilter('name');
|
||||
});
|
||||
|
||||
function getObjects(...names) {
|
||||
const count = new Map();
|
||||
const objects = [];
|
||||
|
||||
for (const name of names) {
|
||||
if (!count.has(name)) {
|
||||
count.set(name, 1);
|
||||
}
|
||||
objects.push({
|
||||
name: name,
|
||||
title: `${name} ${count.get(name)}`
|
||||
});
|
||||
count.set(name, count.get(name) + 1);
|
||||
}
|
||||
return objects;
|
||||
}
|
||||
|
||||
it('should keep only the tables', function () {
|
||||
const objects = getObjects('table', 'table', 'pie');
|
||||
expect(nameFilter(objects, 'table')).to.eql(getObjects('table', 'table'));
|
||||
});
|
||||
|
||||
it('should support comma-separated values', function () {
|
||||
const objects = getObjects('table', 'line', 'pie');
|
||||
expect(nameFilter(objects, 'table,line')).to.eql(getObjects('table', 'line'));
|
||||
});
|
||||
|
||||
it('should support an array of values', function () {
|
||||
const objects = getObjects('table', 'line', 'pie');
|
||||
expect(nameFilter(objects, [ 'table', 'line' ])).to.eql(getObjects('table', 'line'));
|
||||
});
|
||||
|
||||
it('should return all objects', function () {
|
||||
const objects = getObjects('table', 'line', 'pie');
|
||||
expect(nameFilter(objects, '*')).to.eql(objects);
|
||||
});
|
||||
|
||||
it('should allow negation', function () {
|
||||
const objects = getObjects('table', 'line', 'pie');
|
||||
expect(nameFilter(objects, [ '!line' ])).to.eql(getObjects('table', 'pie'));
|
||||
});
|
||||
|
||||
it('should support a function for specifying what should be kept', function () {
|
||||
const objects = getObjects('table', 'line', 'pie');
|
||||
const line = (value) => value === 'line';
|
||||
expect(nameFilter(objects, line)).to.eql(getObjects('line'));
|
||||
});
|
||||
});
|
|
@ -13,13 +13,18 @@ function propFilter(prop) {
|
|||
* must contain
|
||||
*
|
||||
* @param {array} list - array of items to filter
|
||||
* @param {array|string} filters - the values to match against the list. Can be
|
||||
* an array, a single value as a string, or a comma
|
||||
* -seperated list of items
|
||||
* @param {function|array|string} filters - the values to match against the list
|
||||
* - if a function, it is expected to take the field property as argument and returns true to keep it.
|
||||
* - Can be also an array, a single value as a string, or a comma-seperated list of items
|
||||
* @return {array} - the filtered list
|
||||
*/
|
||||
return function (list, filters) {
|
||||
if (!filters) return filters;
|
||||
|
||||
if (_.isFunction(filters)) {
|
||||
return list.filter((item) => filters(item[prop]));
|
||||
}
|
||||
|
||||
if (!_.isArray(filters)) filters = filters.split(',');
|
||||
if (_.contains(filters, '*')) return list;
|
||||
|
||||
|
|
|
@ -4,18 +4,17 @@ import ngMock from 'ng_mock';
|
|||
import IndexPatternsFlattenHitProvider from 'ui/index_patterns/_flatten_hit';
|
||||
|
||||
describe('IndexPattern#flattenHit()', function () {
|
||||
|
||||
|
||||
let flattenHit;
|
||||
let config;
|
||||
let hit;
|
||||
let flat;
|
||||
|
||||
beforeEach(ngMock.module('kibana'));
|
||||
beforeEach(ngMock.inject(function (Private, $injector) {
|
||||
let indexPattern = {
|
||||
fields: {
|
||||
byName: {
|
||||
'tags.text': { type: 'string' },
|
||||
'tags.label': { type: 'string' },
|
||||
'message': { type: 'string' },
|
||||
'geo.coordinates': { type: 'geo_point' },
|
||||
'geo.dest': { type: 'string' },
|
||||
|
@ -33,7 +32,12 @@ describe('IndexPattern#flattenHit()', function () {
|
|||
}
|
||||
};
|
||||
|
||||
flattenHit = Private(IndexPatternsFlattenHitProvider)(indexPattern).uncached;
|
||||
const cachedFlatten = Private(IndexPatternsFlattenHitProvider)(indexPattern);
|
||||
flattenHit = function (hit, deep = false) {
|
||||
delete hit.$$_flattened;
|
||||
return cachedFlatten(hit, deep);
|
||||
};
|
||||
|
||||
config = $injector.get('config');
|
||||
|
||||
hit = {
|
||||
|
@ -46,7 +50,10 @@ describe('IndexPattern#flattenHit()', function () {
|
|||
},
|
||||
bytes: 10039103,
|
||||
'@timestamp': (new Date()).toString(),
|
||||
tags: [{ text: 'foo' }, { text: 'bar' }],
|
||||
tags: [
|
||||
{ text: 'foo', label: [ 'FOO1', 'FOO2' ] },
|
||||
{ text: 'bar', label: 'BAR' }
|
||||
],
|
||||
groups: ['loners'],
|
||||
noMapping: true,
|
||||
team: [
|
||||
|
@ -61,11 +68,11 @@ describe('IndexPattern#flattenHit()', function () {
|
|||
random: [0.12345]
|
||||
}
|
||||
};
|
||||
|
||||
flat = flattenHit(hit);
|
||||
}));
|
||||
|
||||
it('flattens keys as far down as the mapping goes', function () {
|
||||
const flat = flattenHit(hit);
|
||||
|
||||
expect(flat).to.have.property('geo.coordinates', hit._source.geo.coordinates);
|
||||
expect(flat).to.not.have.property('geo.coordinates.lat');
|
||||
expect(flat).to.not.have.property('geo.coordinates.lon');
|
||||
|
@ -77,22 +84,42 @@ describe('IndexPattern#flattenHit()', function () {
|
|||
});
|
||||
|
||||
it('flattens keys not in the mapping', function () {
|
||||
const flat = flattenHit(hit);
|
||||
|
||||
expect(flat).to.have.property('noMapping', true);
|
||||
expect(flat).to.have.property('groups');
|
||||
expect(flat.groups).to.eql(['loners']);
|
||||
});
|
||||
|
||||
it('flattens conflicting types in the mapping', function () {
|
||||
const flat = flattenHit(hit);
|
||||
|
||||
expect(flat).to.not.have.property('user');
|
||||
expect(flat).to.have.property('user.name', hit._source.user.name);
|
||||
expect(flat).to.have.property('user.id', hit._source.user.id);
|
||||
});
|
||||
|
||||
it('preserves objects in arrays', function () {
|
||||
it('should preserve objects in arrays if deep argument is false', function () {
|
||||
const flat = flattenHit(hit);
|
||||
|
||||
expect(flat).to.have.property('tags', hit._source.tags);
|
||||
});
|
||||
|
||||
it('should expand objects in arrays if deep argument is true', function () {
|
||||
const flat = flattenHit(hit, true);
|
||||
|
||||
expect(flat['tags.text']).to.be.eql([ 'foo', 'bar' ]);
|
||||
});
|
||||
|
||||
it('should support arrays when expanding objects in arrays if deep argument is true', function () {
|
||||
const flat = flattenHit(hit, true);
|
||||
|
||||
expect(flat['tags.label']).to.be.eql([ 'FOO1', 'FOO2', 'BAR' ]);
|
||||
});
|
||||
|
||||
it('does not enter into nested fields', function () {
|
||||
const flat = flattenHit(hit);
|
||||
|
||||
expect(flat).to.have.property('team', hit._source.team);
|
||||
expect(flat).to.not.have.property('team.name');
|
||||
expect(flat).to.not.have.property('team.role');
|
||||
|
@ -101,24 +128,28 @@ describe('IndexPattern#flattenHit()', function () {
|
|||
});
|
||||
|
||||
it('unwraps script fields', function () {
|
||||
const flat = flattenHit(hit);
|
||||
|
||||
expect(flat).to.have.property('delta', 42);
|
||||
});
|
||||
|
||||
it('assumes that all fields are "computed fields"', function () {
|
||||
const flat = flattenHit(hit);
|
||||
|
||||
expect(flat).to.have.property('random', 0.12345);
|
||||
});
|
||||
|
||||
it('ignores fields that start with an _ and are not in the metaFields', function () {
|
||||
config.set('metaFields', ['_metaKey']);
|
||||
hit.fields._notMetaKey = [100];
|
||||
flat = flattenHit(hit);
|
||||
const flat = flattenHit(hit);
|
||||
expect(flat).to.not.have.property('_notMetaKey');
|
||||
});
|
||||
|
||||
it('includes underscore-prefixed keys that are in the metaFields', function () {
|
||||
config.set('metaFields', ['_metaKey']);
|
||||
hit.fields._metaKey = [100];
|
||||
flat = flattenHit(hit);
|
||||
const flat = flattenHit(hit);
|
||||
expect(flat).to.have.property('_metaKey', 100);
|
||||
});
|
||||
|
||||
|
@ -126,7 +157,7 @@ describe('IndexPattern#flattenHit()', function () {
|
|||
hit.fields._metaKey = [100];
|
||||
|
||||
config.set('metaFields', ['_metaKey']);
|
||||
flat = flattenHit(hit);
|
||||
let flat = flattenHit(hit);
|
||||
expect(flat).to.have.property('_metaKey', 100);
|
||||
|
||||
config.set('metaFields', []);
|
||||
|
@ -137,7 +168,7 @@ describe('IndexPattern#flattenHit()', function () {
|
|||
it('handles fields that are not arrays, like _timestamp', function () {
|
||||
hit.fields._metaKey = 20000;
|
||||
config.set('metaFields', ['_metaKey']);
|
||||
flat = flattenHit(hit);
|
||||
const flat = flattenHit(hit);
|
||||
expect(flat).to.have.property('_metaKey', 20000);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import _ from 'lodash';
|
||||
|
||||
// Takes a hit, merges it with any stored/scripted fields, and with the metaFields
|
||||
// returns a flattened version
|
||||
export default function FlattenHitProvider(config) {
|
||||
|
@ -8,23 +9,38 @@ export default function FlattenHitProvider(config) {
|
|||
metaFields = value;
|
||||
});
|
||||
|
||||
function flattenHit(indexPattern, hit) {
|
||||
let flat = {};
|
||||
function flattenHit(indexPattern, hit, deep) {
|
||||
const flat = {};
|
||||
|
||||
// recursively merge _source
|
||||
let fields = indexPattern.fields.byName;
|
||||
const fields = indexPattern.fields.byName;
|
||||
(function flatten(obj, keyPrefix) {
|
||||
keyPrefix = keyPrefix ? keyPrefix + '.' : '';
|
||||
_.forOwn(obj, function (val, key) {
|
||||
key = keyPrefix + key;
|
||||
|
||||
if (flat[key] !== void 0) return;
|
||||
if (deep) {
|
||||
const isNestedField = fields[key] && fields[key].type === 'nested';
|
||||
const isArrayOfObjects = _.isArray(val) && _.isPlainObject(_.first(val));
|
||||
if (isArrayOfObjects && !isNestedField) {
|
||||
_.each(val, v => flatten(v, key));
|
||||
return;
|
||||
}
|
||||
} else if (flat[key] !== void 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let hasValidMapping = (fields[key] && fields[key].type !== 'conflict');
|
||||
let isValue = !_.isPlainObject(val);
|
||||
const hasValidMapping = fields[key] && fields[key].type !== 'conflict';
|
||||
const isValue = !_.isPlainObject(val);
|
||||
|
||||
if (hasValidMapping || isValue) {
|
||||
flat[key] = val;
|
||||
if (!flat[key]) {
|
||||
flat[key] = val;
|
||||
} else if (_.isArray(flat[key])) {
|
||||
flat[key].push(val);
|
||||
} else {
|
||||
flat[key] = [ flat[key], val ];
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -48,13 +64,8 @@ export default function FlattenHitProvider(config) {
|
|||
}
|
||||
|
||||
return function flattenHitWrapper(indexPattern) {
|
||||
function cachedFlatten(hit) {
|
||||
return hit.$$_flattened || (hit.$$_flattened = flattenHit(indexPattern, hit));
|
||||
}
|
||||
|
||||
cachedFlatten.uncached = _.partial(flattenHit, indexPattern);
|
||||
|
||||
return cachedFlatten;
|
||||
return function cachedFlatten(hit, deep = false) {
|
||||
return hit.$$_flattened || (hit.$$_flattened = flattenHit(indexPattern, hit, deep));
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
@ -27,6 +27,11 @@ describe('_source formatting', function () {
|
|||
convertHtml = format.getConverterFor('html');
|
||||
}));
|
||||
|
||||
it('should use the text content type if a field is not passed', function () {
|
||||
const hit = _.first(hits);
|
||||
expect(convertHtml(hit._source)).to.be(JSON.stringify(hit._source));
|
||||
});
|
||||
|
||||
it('uses the _source, field, and hit to create a <dl>', function () {
|
||||
let hit = _.first(hits);
|
||||
let $dl = $(convertHtml(hit._source, indexPattern.fields.byName._source, hit));
|
||||
|
|
|
@ -18,7 +18,7 @@ export default function _SourceFormatProvider(Private, shortDotsFilter) {
|
|||
Source.prototype._convert = {
|
||||
text: angular.toJson,
|
||||
html: function sourceToHtml(source, field, hit) {
|
||||
if (!field) return this.getConverter('text')(source, field, hit);
|
||||
if (!field) return this.getConverterFor('text')(source, field, hit);
|
||||
|
||||
let highlights = (hit && hit.highlight) || {};
|
||||
let formatted = field.indexPattern.formatHit(hit);
|
||||
|
|
|
@ -152,7 +152,13 @@ export default function AggConfigFactory(Private, fieldTypeFilter) {
|
|||
const fieldOptions = this.getFieldOptions();
|
||||
|
||||
if (fieldOptions) {
|
||||
field = fieldOptions.byName[this.fieldName()] || null;
|
||||
let prevField = fieldOptions.byName[this.fieldName()] || null;
|
||||
let filters = fieldOptions.filterFieldTypes;
|
||||
if (_.isFunction(fieldOptions.filterFieldTypes)) {
|
||||
filters = fieldOptions.filterFieldTypes.bind(this, this.vis);
|
||||
}
|
||||
let fieldOpts = fieldTypeFilter(this.vis.indexPattern.fields, filters);
|
||||
field = _.contains(fieldOpts, prevField) ? prevField : null;
|
||||
}
|
||||
|
||||
return this.fillDefaults({ row: this.params.row, field: field });
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue