diff --git a/src/core_plugins/kbn_vislib_vis_types/public/heatmap.js b/src/core_plugins/kbn_vislib_vis_types/public/heatmap.js index 718aa32755317..53316d79529c2 100644 --- a/src/core_plugins/kbn_vislib_vis_types/public/heatmap.js +++ b/src/core_plugins/kbn_vislib_vis_types/public/heatmap.js @@ -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' } ] diff --git a/src/core_plugins/kbn_vislib_vis_types/public/line.js b/src/core_plugins/kbn_vislib_vis_types/public/line.js index 0576ec45a9a6b..2f74f681a5d3d 100644 --- a/src/core_plugins/kbn_vislib_vis_types/public/line.js +++ b/src/core_plugins/kbn_vislib_vis_types/public/line.js @@ -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', diff --git a/src/core_plugins/kbn_vislib_vis_types/public/pie.js b/src/core_plugins/kbn_vislib_vis_types/public/pie.js index 0947e957b09a6..6ec170695fdc9 100644 --- a/src/core_plugins/kbn_vislib_vis_types/public/pie.js +++ b/src/core_plugins/kbn_vislib_vis_types/public/pie.js @@ -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' } ] diff --git a/src/core_plugins/kbn_vislib_vis_types/public/tile_map.js b/src/core_plugins/kbn_vislib_vis_types/public/tile_map.js index 6605479c0798c..9ec130f84737f 100644 --- a/src/core_plugins/kbn_vislib_vis_types/public/tile_map.js +++ b/src/core_plugins/kbn_vislib_vis_types/public/tile_map.js @@ -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' } ] diff --git a/src/core_plugins/kibana/public/visualize/editor/agg_params.js b/src/core_plugins/kibana/public/visualize/editor/agg_params.js index 12db500fd7cad..c1b9baf3c5f29 100644 --- a/src/core_plugins/kibana/public/visualize/editor/agg_params.js +++ b/src/core_plugins/kibana/public/visualize/editor/agg_params.js @@ -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`] = param.getFieldOptions($scope.agg); + } - 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; } } diff --git a/src/core_plugins/metric_vis/public/__tests__/metric_vis_controller.js b/src/core_plugins/metric_vis/public/__tests__/metric_vis_controller.js index 2d9515db7bd7f..c07b3cfc4bd33 100644 --- a/src/core_plugins/metric_vis/public/__tests__/metric_vis_controller.js +++ b/src/core_plugins/metric_vis/public/__tests__/metric_vis_controller.js @@ -21,14 +21,7 @@ describe('metric vis', function () { $scope.processTableGroups({ tables: [{ columns: [{ title: 'Count' }], - rows: [[4301021]], - aggConfig: function () { - return { - fieldFormatter: function () { - return formatter; - } - }; - } + 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) } ]] }] }); diff --git a/src/core_plugins/metric_vis/public/metric_vis_controller.js b/src/core_plugins/metric_vis/public/metric_vis_controller.js index c9efb01d417a9..e3fddf42a36a2 100644 --- a/src/core_plugins/metric_vis/public/metric_vis_controller.js +++ b/src/core_plugins/metric_vis/public/metric_vis_controller.js @@ -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'); } }); diff --git a/src/fixtures/logstash_fields.js b/src/fixtures/logstash_fields.js index 4cdfdd43d4aa7..b44d822a6fd03 100644 --- a/src/fixtures/logstash_fields.js +++ b/src/fixtures/logstash_fields.js @@ -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, diff --git a/src/test_utils/stub_index_pattern.js b/src/test_utils/stub_index_pattern.js index 64ff63c3d6d5c..c14a08a30c50a 100644 --- a/src/test_utils/stub_index_pattern.js +++ b/src/test_utils/stub_index_pattern.js @@ -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) { const fieldFormats = Private(RegistryFieldFormatsProvider); const flattenHit = Private(IndexPatternsFlattenHitProvider); diff --git a/src/ui/public/agg_types/__tests__/buckets/_terms.js b/src/ui/public/agg_types/__tests__/buckets/_terms.js deleted file mode 100644 index ce117c7d96b08..0000000000000 --- a/src/ui/public/agg_types/__tests__/buckets/_terms.js +++ /dev/null @@ -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'); - }); -}); diff --git a/src/ui/public/agg_types/__tests__/buckets/terms.js b/src/ui/public/agg_types/__tests__/buckets/terms.js new file mode 100644 index 0000000000000..d5fd937b0b3d8 --- /dev/null +++ b/src/ui/public/agg_types/__tests__/buckets/terms.js @@ -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'); + }); +}); diff --git a/src/ui/public/agg_types/__tests__/metrics/top_hit.js b/src/ui/public/agg_types/__tests__/metrics/top_hit.js new file mode 100644 index 0000000000000..04429a447787c --- /dev/null +++ b/src/ui/public/agg_types/__tests__/metrics/top_hit.js @@ -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); + }); + }); + }); + }); +}); 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 cf9927ef19ffa..72550f3461f67 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,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(); + }); + }); }); diff --git a/src/ui/public/agg_types/buckets/terms.js b/src/ui/public/agg_types/buckets/terms.js index 30e66f8630e43..886bf183d4fe3 100644 --- a/src/ui/public/agg_types/buckets/terms.js +++ b/src/ui/public/agg_types/buckets/terms.js @@ -16,12 +16,13 @@ export default function TermsAggDefinition(Private) { const createFilter = Private(AggTypesBucketsCreateFilterTermsProvider); const routeBasedNotifier = Private(routeBasedNotifierProvider); + const aggFilter = ['!top_hits', '!percentiles', '!median', '!std_dev']; const 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() { const agg = $scope.agg; - const aggs = agg.vis.aggs; const params = agg.params; const orderBy = params.orderBy; const 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; } diff --git a/src/ui/public/agg_types/controls/field.html b/src/ui/public/agg_types/controls/field.html index 14d0b56d98467..5ef83e18a3219 100644 --- a/src/ui/public/agg_types/controls/field.html +++ b/src/ui/public/agg_types/controls/field.html @@ -3,7 +3,7 @@ Field - Analyzed Field diff --git a/src/ui/public/agg_types/controls/order_agg.html b/src/ui/public/agg_types/controls/order_agg.html index 2b44fe77fbd74..a407660380939 100644 --- a/src/ui/public/agg_types/controls/order_agg.html +++ b/src/ui/public/agg_types/controls/order_agg.html @@ -9,6 +9,7 @@ @@ -27,4 +28,4 @@ group-name="'metrics'"> - \ No newline at end of file + diff --git a/src/ui/public/agg_types/controls/top_aggregate_and_size.html b/src/ui/public/agg_types/controls/top_aggregate_and_size.html new file mode 100644 index 0000000000000..76e490ddc9970 --- /dev/null +++ b/src/ui/public/agg_types/controls/top_aggregate_and_size.html @@ -0,0 +1,37 @@ +