other and missing bucket support for terms agg (#15525)
This commit is contained in:
parent
b709ee989a
commit
2fd41d5a6b
|
@ -9,7 +9,8 @@ export function HierarchicalTransformAggregationProvider() {
|
|||
agg,
|
||||
parent && parent.aggConfigResult,
|
||||
metric.getValue(bucket),
|
||||
agg.getKey(bucket)
|
||||
agg.getKey(bucket),
|
||||
bucket.filters
|
||||
);
|
||||
|
||||
const branch = {
|
||||
|
|
|
@ -77,7 +77,7 @@ export function BuildHierarchicalDataProvider(Private, Notifier) {
|
|||
if (!_.isEmpty(displayName)) split.label += ': ' + displayName;
|
||||
|
||||
split.tooltipFormatter = tooltipFormatter(raw.columns);
|
||||
const aggConfigResult = new AggConfigResult(firstAgg, null, null, firstAgg.getKey(bucket));
|
||||
const aggConfigResult = new AggConfigResult(firstAgg, null, null, firstAgg.getKey(bucket), bucket.filters);
|
||||
split.split = { aggConfig: firstAgg, aggConfigResult: aggConfigResult, key: bucket.key };
|
||||
_.each(split.slices.children, function (child) {
|
||||
child.aggConfigResult.$parent = aggConfigResult;
|
||||
|
|
|
@ -194,7 +194,7 @@ export function TabbedAggResponseWriterProvider(Private) {
|
|||
newList.unshift(injected);
|
||||
}
|
||||
|
||||
const newAcr = new AggConfigResult(acr.aggConfig, newList[0], acr.value, acr.aggConfig.getKey(acr));
|
||||
const newAcr = new AggConfigResult(acr.aggConfig, newList[0], acr.value, acr.aggConfig.getKey(acr), acr.filters);
|
||||
newList.unshift(newAcr);
|
||||
|
||||
// and replace the acr in the row buffer if its there
|
||||
|
@ -215,9 +215,9 @@ export function TabbedAggResponseWriterProvider(Private) {
|
|||
* @param {function} block - the function to run while this value is in the row
|
||||
* @return {any} - the value that was added
|
||||
*/
|
||||
TabbedAggResponseWriter.prototype.cell = function (agg, value, block) {
|
||||
TabbedAggResponseWriter.prototype.cell = function (agg, value, block, filters) {
|
||||
if (this.asAggConfigResults) {
|
||||
value = new AggConfigResult(agg, this.acrStack[0], value, value);
|
||||
value = new AggConfigResult(agg, this.acrStack[0], value, value, filters);
|
||||
}
|
||||
|
||||
const staskResult = this.asAggConfigResults && value.type === 'bucket';
|
||||
|
|
|
@ -46,7 +46,7 @@ export function AggResponseTabifyProvider(Private, Notifier) {
|
|||
buckets.forEach(function (subBucket, key) {
|
||||
write.cell(agg, agg.getKey(subBucket, key), function () {
|
||||
collectBucket(write, subBucket, agg.getKey(subBucket, key), aggScale);
|
||||
});
|
||||
}, subBucket.filters);
|
||||
});
|
||||
}
|
||||
} else if (write.partialRows && write.metricsForAllBuckets && write.minimalColumns) {
|
||||
|
|
|
@ -0,0 +1,260 @@
|
|||
import expect from 'expect.js';
|
||||
import ngMock from 'ng_mock';
|
||||
import { OtherBucketHelperProvider } from 'ui/agg_types/buckets/_terms_other_bucket_helper';
|
||||
import { VisProvider } from 'ui/vis';
|
||||
import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
|
||||
|
||||
const visConfigSingleTerm = {
|
||||
type: 'pie',
|
||||
aggs: [
|
||||
{
|
||||
type: 'terms',
|
||||
schema: 'segment',
|
||||
params: { field: 'machine.os.raw', otherBucket: true, missingBucket: true }
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const visConfigNestedTerm = {
|
||||
type: 'pie',
|
||||
aggs: [
|
||||
{
|
||||
type: 'terms',
|
||||
schema: 'segment',
|
||||
params: { field: 'geo.src', size: 2, otherBucket: false, missingBucket: false }
|
||||
}, {
|
||||
type: 'terms',
|
||||
schema: 'segment',
|
||||
params: { field: 'machine.os.raw', size: 2, otherBucket: true, missingBucket: true }
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const singleTermResponse = {
|
||||
'took': 10,
|
||||
'timed_out': false,
|
||||
'_shards': {
|
||||
'total': 1, 'successful': 1, 'skipped': 0, 'failed': 0
|
||||
}, 'hits': {
|
||||
'total': 14005, 'max_score': 0, 'hits': []
|
||||
}, 'aggregations': {
|
||||
'1': {
|
||||
'doc_count_error_upper_bound': 0,
|
||||
'sum_other_doc_count': 8325,
|
||||
'buckets': [
|
||||
{ 'key': 'ios', 'doc_count': 2850 },
|
||||
{ 'key': 'win xp', 'doc_count': 2830 },
|
||||
{ 'key': '__missing__', 'doc_count': 1430 }
|
||||
]
|
||||
}
|
||||
}, 'status': 200
|
||||
};
|
||||
|
||||
const nestedTermResponse = {
|
||||
'took': 10,
|
||||
'timed_out': false,
|
||||
'_shards': {
|
||||
'total': 1, 'successful': 1, 'skipped': 0, 'failed': 0
|
||||
}, 'hits': {
|
||||
'total': 14005, 'max_score': 0, 'hits': []
|
||||
}, 'aggregations': {
|
||||
'1': {
|
||||
'doc_count_error_upper_bound': 0,
|
||||
'sum_other_doc_count': 8325,
|
||||
'buckets': [
|
||||
{
|
||||
'2': {
|
||||
'doc_count_error_upper_bound': 0,
|
||||
'sum_other_doc_count': 8325,
|
||||
'buckets': [
|
||||
{ 'key': 'ios', 'doc_count': 2850 },
|
||||
{ 'key': 'win xp', 'doc_count': 2830 },
|
||||
{ 'key': '__missing__', 'doc_count': 1430 }
|
||||
]
|
||||
},
|
||||
key: 'US',
|
||||
doc_count: 2850
|
||||
}, {
|
||||
'2': {
|
||||
'doc_count_error_upper_bound': 0,
|
||||
'sum_other_doc_count': 8325,
|
||||
'buckets': [
|
||||
{ 'key': 'ios', 'doc_count': 1850 },
|
||||
{ 'key': 'win xp', 'doc_count': 1830 },
|
||||
{ 'key': '__missing__', 'doc_count': 130 }
|
||||
]
|
||||
},
|
||||
key: 'IN',
|
||||
doc_count: 2830
|
||||
}
|
||||
]
|
||||
}
|
||||
}, 'status': 200
|
||||
};
|
||||
|
||||
const singleOtherResponse = {
|
||||
'took': 3,
|
||||
'timed_out': false,
|
||||
'_shards': { 'total': 1, 'successful': 1, 'skipped': 0, 'failed': 0 },
|
||||
'hits': { 'total': 14005, 'max_score': 0, 'hits': [] },
|
||||
'aggregations': {
|
||||
'other-filter': {
|
||||
'buckets': { '': { 'doc_count': 2805 } }
|
||||
}
|
||||
}, 'status': 200
|
||||
};
|
||||
|
||||
const nestedOtherResponse = {
|
||||
'took': 3,
|
||||
'timed_out': false,
|
||||
'_shards': { 'total': 1, 'successful': 1, 'skipped': 0, 'failed': 0 },
|
||||
'hits': { 'total': 14005, 'max_score': 0, 'hits': [] },
|
||||
'aggregations': {
|
||||
'other-filter': {
|
||||
'buckets': { '-US': { 'doc_count': 2805 }, '-IN': { 'doc_count': 2804 } }
|
||||
}
|
||||
}, 'status': 200
|
||||
};
|
||||
|
||||
describe('Terms Agg Other bucket helper', () => {
|
||||
|
||||
let otherBucketHelper;
|
||||
let vis;
|
||||
|
||||
function init(aggConfig) {
|
||||
ngMock.module('kibana');
|
||||
ngMock.inject((Private) => {
|
||||
const Vis = Private(VisProvider);
|
||||
const indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider);
|
||||
otherBucketHelper = Private(OtherBucketHelperProvider);
|
||||
|
||||
vis = new Vis(indexPattern, aggConfig);
|
||||
});
|
||||
}
|
||||
|
||||
describe('buildOtherBucketAgg', () => {
|
||||
|
||||
it('returns a function', () => {
|
||||
init(visConfigSingleTerm);
|
||||
const agg = otherBucketHelper.buildOtherBucketAgg(vis.aggs, vis.aggs[0], singleTermResponse);
|
||||
expect(agg).to.be.a('function');
|
||||
});
|
||||
|
||||
it('correctly builds query with single terms agg', () => {
|
||||
init(visConfigSingleTerm);
|
||||
const agg = otherBucketHelper.buildOtherBucketAgg(vis.aggs, vis.aggs[0], singleTermResponse)();
|
||||
const expectedResponse = {
|
||||
aggs: undefined,
|
||||
filters: {
|
||||
filters: {
|
||||
'': {
|
||||
'bool': {
|
||||
'must': [{
|
||||
'exists': {
|
||||
'field': 'machine.os.raw',
|
||||
}
|
||||
}],
|
||||
'filter': [],
|
||||
'should': [],
|
||||
'must_not': [
|
||||
{ 'match_phrase': { 'machine.os.raw': { 'query': 'ios' } } },
|
||||
{ 'match_phrase': { 'machine.os.raw': { 'query': 'win xp' } } }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
expect(agg['other-filter']).to.eql(expectedResponse);
|
||||
});
|
||||
|
||||
it('correctly builds query for nested terms agg', () => {
|
||||
init(visConfigNestedTerm);
|
||||
const agg = otherBucketHelper.buildOtherBucketAgg(vis.aggs, vis.aggs[1], nestedTermResponse)();
|
||||
const expectedResponse = {
|
||||
'other-filter': {
|
||||
aggs: undefined,
|
||||
'filters': {
|
||||
'filters': {
|
||||
'-IN': {
|
||||
'bool': {
|
||||
'must': [
|
||||
{ match_phrase: { 'geo.src': { 'query': 'IN' } } },
|
||||
{
|
||||
'exists': {
|
||||
'field': 'machine.os.raw',
|
||||
}
|
||||
}
|
||||
], 'filter': [],
|
||||
'should': [],
|
||||
'must_not': [
|
||||
{ 'match_phrase': { 'machine.os.raw': { 'query': 'ios' } } },
|
||||
{ 'match_phrase': { 'machine.os.raw': { 'query': 'win xp' } } }
|
||||
]
|
||||
}
|
||||
}, '-US': {
|
||||
'bool': {
|
||||
'must': [
|
||||
{ 'match_phrase': { 'geo.src': { 'query': 'US' } } },
|
||||
{
|
||||
'exists': {
|
||||
'field': 'machine.os.raw',
|
||||
}
|
||||
}
|
||||
], 'filter': [], 'should': [], 'must_not': [
|
||||
{ 'match_phrase': { 'machine.os.raw': { 'query': 'ios' } } },
|
||||
{ 'match_phrase': { 'machine.os.raw': { 'query': 'win xp' } } }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
expect(agg).to.eql(expectedResponse);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mergeOtherBucketAggResponse', () => {
|
||||
it('correctly merges other bucket with single terms agg', () => {
|
||||
init(visConfigSingleTerm);
|
||||
const otherAggConfig = otherBucketHelper.buildOtherBucketAgg(vis.aggs, vis.aggs[0], singleTermResponse)();
|
||||
const mergedResponse = otherBucketHelper
|
||||
.mergeOtherBucketAggResponse(vis.aggs, singleTermResponse, singleOtherResponse, vis.aggs[0], otherAggConfig);
|
||||
|
||||
expect(mergedResponse.aggregations['1'].buckets[3].key).to.equal('Other');
|
||||
expect(mergedResponse.aggregations['1'].buckets[3].filters.length).to.equal(2);
|
||||
});
|
||||
|
||||
it('correctly merges other bucket with nested terms agg', () => {
|
||||
init(visConfigNestedTerm);
|
||||
const otherAggConfig = otherBucketHelper.buildOtherBucketAgg(vis.aggs, vis.aggs[1], nestedTermResponse)();
|
||||
const mergedResponse = otherBucketHelper
|
||||
.mergeOtherBucketAggResponse(vis.aggs, nestedTermResponse, nestedOtherResponse, vis.aggs[1], otherAggConfig);
|
||||
|
||||
expect(mergedResponse.aggregations['1'].buckets[1]['2'].buckets[3].key).to.equal('Other');
|
||||
expect(mergedResponse.aggregations['1'].buckets[1]['2'].buckets[3].filters.length).to.equal(2);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('updateMissingBucket', () => {
|
||||
it('correctly updates missing bucket key', () => {
|
||||
init(visConfigNestedTerm);
|
||||
const updatedResponse = otherBucketHelper.updateMissingBucket(singleTermResponse, vis.aggs, vis.aggs[0]);
|
||||
expect(updatedResponse.aggregations['1'].buckets.find(bucket => bucket.key === 'Missing')).to.not.be('undefined');
|
||||
});
|
||||
|
||||
it('correctly sets the bucket filter', () => {
|
||||
const updatedResponse = otherBucketHelper.updateMissingBucket(singleTermResponse, vis.aggs, vis.aggs[0]);
|
||||
const missingBucket = updatedResponse.aggregations['1'].buckets.find(bucket => bucket.key === 'Missing');
|
||||
expect(missingBucket.filters).to.not.be('undefined');
|
||||
expect(missingBucket.filters[0]).to.eql({
|
||||
meta: { index: 'logstash-*', negate: true },
|
||||
exists: { field: 'geo.src' }
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -5,6 +5,7 @@ import './agg_params';
|
|||
import './buckets/_histogram';
|
||||
import './buckets/_geo_hash';
|
||||
import './buckets/_range';
|
||||
import './buckets/_terms_other_bucket_helper';
|
||||
import './buckets/date_histogram/_editor';
|
||||
import './buckets/date_histogram/_params';
|
||||
import { AggTypesIndexProvider } from 'ui/agg_types/index';
|
||||
|
|
|
@ -140,6 +140,18 @@ export function AggTypesAggTypeProvider(Private) {
|
|||
*/
|
||||
this.decorateAggConfig = config.decorateAggConfig || null;
|
||||
|
||||
/**
|
||||
* A function that needs to be called after the main request has been made
|
||||
* and should return an updated response
|
||||
* @param aggConfigs - agg config array used to produce main request
|
||||
* @param aggConfig - AggConfig that requested the post flight request
|
||||
* @param searchSourceAggs - SearchSource aggregation configuration
|
||||
* @param resp - Response to the main request
|
||||
* @param nestedSearchSource - the new SearchSource that will be used to make post flight request
|
||||
* @return {Promise}
|
||||
*/
|
||||
this.postFlightRequest = config.postFlightRequest || _.identity;
|
||||
|
||||
if (config.getFormat) {
|
||||
this.getFormat = config.getFormat;
|
||||
}
|
||||
|
|
186
src/ui/public/agg_types/buckets/_terms_other_bucket_helper.js
Normal file
186
src/ui/public/agg_types/buckets/_terms_other_bucket_helper.js
Normal file
|
@ -0,0 +1,186 @@
|
|||
import _ from 'lodash';
|
||||
import { VisAggConfigProvider } from 'ui/vis/agg_config';
|
||||
import { buildPhrasesFilter } from 'ui/filter_manager/lib/phrases';
|
||||
import { buildExistsFilter } from 'ui/filter_manager/lib/exists';
|
||||
import { buildQueryFromFilters } from 'ui/courier/data_source/build_query/from_filters';
|
||||
|
||||
/**
|
||||
* walks the aggregation DSL and returns DSL starting at aggregation with id of startFromAggId
|
||||
* @param aggNestedDsl: aggregation config DSL (top level)
|
||||
* @param startFromId: id of an aggregation from where we want to get the nested DSL
|
||||
*/
|
||||
const getNestedAggDSL = (aggNestedDsl, startFromAggId) => {
|
||||
if (aggNestedDsl[startFromAggId]) return aggNestedDsl[startFromAggId];
|
||||
return getNestedAggDSL(_.values(aggNestedDsl)[0].aggs, startFromAggId);
|
||||
};
|
||||
|
||||
/**
|
||||
* returns buckets from response for a specific other bucket
|
||||
* @param aggConfigs: configuration for the aggregations
|
||||
* @param response: response from elasticsearch
|
||||
* @param aggWithOtherBucket: AggConfig of the aggregation with other bucket enabled
|
||||
* @param key: key from the other bucket request for a specific other bucket
|
||||
*/
|
||||
const getAggResultBuckets = (aggConfigs, response, aggWithOtherBucket, key) => {
|
||||
const keyParts = key.split('-');
|
||||
let responseAgg = response;
|
||||
for (const i in keyParts) {
|
||||
if (keyParts[i]) {
|
||||
const agg = _.values(responseAgg)[0];
|
||||
const aggKey = _.keys(responseAgg)[0];
|
||||
const aggConfig = _.find(aggConfigs, agg => agg.id === aggKey);
|
||||
const bucket = _.find(agg.buckets, (bucket, bucketObjKey) => {
|
||||
const bucketKey = aggConfig.getKey(bucket, Number.isInteger(bucketObjKey) ? null : bucketObjKey).toString();
|
||||
return bucketKey === keyParts[i];
|
||||
});
|
||||
if (bucket) {
|
||||
responseAgg = bucket;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (responseAgg[aggWithOtherBucket.id]) return responseAgg[aggWithOtherBucket.id].buckets;
|
||||
return [];
|
||||
};
|
||||
|
||||
/**
|
||||
* gets all the missing buckets in our response for a specific aggregation id
|
||||
* @param responseAggs: array of aggregations from response
|
||||
* @param aggId: id of the aggregation with missing bucket
|
||||
*/
|
||||
const getAggConfigResultMissingBuckets = (responseAggs, aggId) => {
|
||||
const missingKey = '__missing__';
|
||||
let resultBuckets = [];
|
||||
if (responseAggs[aggId]) {
|
||||
const matchingBucket = responseAggs[aggId].buckets.find(bucket => bucket.key === missingKey);
|
||||
if (matchingBucket) resultBuckets.push(matchingBucket);
|
||||
return resultBuckets;
|
||||
}
|
||||
_.each(responseAggs, agg => {
|
||||
if (agg.buckets) {
|
||||
_.each(agg.buckets, bucket => {
|
||||
resultBuckets = [
|
||||
...resultBuckets,
|
||||
...getAggConfigResultMissingBuckets(bucket, aggId, missingKey)
|
||||
];
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return resultBuckets;
|
||||
};
|
||||
|
||||
/**
|
||||
* gets all the terms that are NOT in the other bucket
|
||||
* @param requestAgg: an aggregation we are looking at
|
||||
* @param key: the key for this specific other bucket
|
||||
* @param otherAgg: AggConfig of the aggregation with other bucket
|
||||
*/
|
||||
const getOtherAggTerms = (requestAgg, key, otherAgg) => {
|
||||
return requestAgg['other-filter'].filters.filters[key].bool.must_not.filter(filter =>
|
||||
filter.match_phrase && filter.match_phrase[otherAgg.params.field.name]
|
||||
).map(filter =>
|
||||
filter.match_phrase[otherAgg.params.field.name].query
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export const OtherBucketHelperProvider = (Private) => {
|
||||
const AggConfig = Private(VisAggConfigProvider);
|
||||
|
||||
const buildOtherBucketAgg = (aggConfigs, aggWithOtherBucket, response) => {
|
||||
const bucketAggs = aggConfigs.filter(agg => agg.type.type === 'buckets');
|
||||
const index = bucketAggs.findIndex(agg => agg.id === aggWithOtherBucket.id);
|
||||
const aggs = aggConfigs.toDsl();
|
||||
|
||||
// create filters aggregation
|
||||
const filterAgg = new AggConfig(aggConfigs[index].vis, {
|
||||
type: 'filters',
|
||||
id: 'other',
|
||||
schema: {
|
||||
group: 'buckets'
|
||||
}
|
||||
});
|
||||
|
||||
// nest all the child aggregations of aggWithOtherBucket
|
||||
const resultAgg = {
|
||||
aggs: getNestedAggDSL(aggs, aggWithOtherBucket.id).aggs,
|
||||
filters: filterAgg.toDsl(),
|
||||
};
|
||||
|
||||
// create filters for all parent aggregation buckets
|
||||
const walkBucketTree = (aggIndex, aggs, aggId, filters, key) => {
|
||||
const agg = aggs[aggId];
|
||||
const newAggIndex = aggIndex + 1;
|
||||
const newAgg = bucketAggs[newAggIndex];
|
||||
const currentAgg = bucketAggs[aggIndex];
|
||||
if (aggIndex < index) {
|
||||
_.each(agg.buckets, (bucket, bucketObjKey) => {
|
||||
const bucketKey = currentAgg.getKey(bucket, Number.isInteger(bucketObjKey) ? null : bucketObjKey);
|
||||
const filter = _.cloneDeep(bucket.filter) || currentAgg.createFilter(bucketKey);
|
||||
const newFilters = [...filters, filter];
|
||||
walkBucketTree(newAggIndex, bucket, newAgg.id, newFilters, `${key}-${bucketKey.toString()}`);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!aggWithOtherBucket.params.missingBucket || agg.buckets.some(bucket => bucket.key === '__missing__')) {
|
||||
filters.push(buildExistsFilter(aggWithOtherBucket.params.field, aggWithOtherBucket.params.field.indexPattern));
|
||||
}
|
||||
|
||||
// create not filters for all the buckets
|
||||
_.each(agg.buckets, bucket => {
|
||||
if (bucket.key === '__missing__') return;
|
||||
const filter = currentAgg.createFilter(bucket.key);
|
||||
filter.meta.negate = true;
|
||||
filters.push(filter);
|
||||
});
|
||||
|
||||
resultAgg.filters.filters[key] = {
|
||||
bool: buildQueryFromFilters(filters, _.noop)
|
||||
};
|
||||
};
|
||||
walkBucketTree(0, response.aggregations, bucketAggs[0].id, [], '');
|
||||
|
||||
return () => {
|
||||
return {
|
||||
'other-filter': resultAgg
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
const mergeOtherBucketAggResponse = (aggsConfig, response, otherResponse, otherAgg, requestAgg) => {
|
||||
const updatedResponse = _.cloneDeep(response);
|
||||
_.each(otherResponse.aggregations['other-filter'].buckets, (bucket, key) => {
|
||||
if (!bucket.doc_count) return;
|
||||
const bucketKey = key.replace(/^-/, '');
|
||||
const aggResultBuckets = getAggResultBuckets(aggsConfig, updatedResponse.aggregations, otherAgg, bucketKey);
|
||||
const requestFilterTerms = getOtherAggTerms(requestAgg, key, otherAgg);
|
||||
|
||||
const phraseFilter = buildPhrasesFilter(otherAgg.params.field, requestFilterTerms, otherAgg.params.field.indexPattern);
|
||||
phraseFilter.meta.negate = true;
|
||||
bucket.filters = [ phraseFilter ];
|
||||
bucket.key = otherAgg.params.otherBucketLabel;
|
||||
|
||||
if (aggResultBuckets.some(bucket => bucket.key === '__missing__')) {
|
||||
bucket.filters.push(buildExistsFilter(otherAgg.params.field, otherAgg.params.field.indexPattern));
|
||||
}
|
||||
|
||||
aggResultBuckets.push(bucket);
|
||||
});
|
||||
return updatedResponse;
|
||||
};
|
||||
|
||||
const updateMissingBucket = (response, aggConfigs, agg) => {
|
||||
const updatedResponse = _.cloneDeep(response);
|
||||
const aggResultBuckets = getAggConfigResultMissingBuckets(updatedResponse.aggregations, agg.id);
|
||||
aggResultBuckets.forEach(bucket => {
|
||||
bucket.key = agg.params.missingBucketLabel;
|
||||
const existsFilter = buildExistsFilter(agg.params.field, agg.params.field.indexPattern);
|
||||
existsFilter.meta.negate = true;
|
||||
bucket.filters = [ existsFilter ];
|
||||
});
|
||||
return updatedResponse;
|
||||
};
|
||||
|
||||
return { buildOtherBucketAgg, mergeOtherBucketAggResponse, updateMissingBucket };
|
||||
};
|
|
@ -6,6 +6,7 @@ import { AggTypesBucketsCreateFilterTermsProvider } from 'ui/agg_types/buckets/c
|
|||
import orderAggTemplate from 'ui/agg_types/controls/order_agg.html';
|
||||
import orderAndSizeTemplate from 'ui/agg_types/controls/order_and_size.html';
|
||||
import { RouteBasedNotifierProvider } from 'ui/route_based_notifier';
|
||||
import { OtherBucketHelperProvider } from './_terms_other_bucket_helper';
|
||||
|
||||
export function AggTypesBucketsTermsProvider(Private) {
|
||||
const BucketAggType = Private(AggTypesBucketsBucketAggTypeProvider);
|
||||
|
@ -13,6 +14,7 @@ export function AggTypesBucketsTermsProvider(Private) {
|
|||
const Schemas = Private(VisSchemasProvider);
|
||||
const createFilter = Private(AggTypesBucketsCreateFilterTermsProvider);
|
||||
const routeBasedNotifier = Private(RouteBasedNotifierProvider);
|
||||
const { buildOtherBucketAgg, mergeOtherBucketAggResponse, updateMissingBucket } = Private(OtherBucketHelperProvider);
|
||||
|
||||
const aggFilter = [
|
||||
'!top_hits', '!percentiles', '!median', '!std_dev',
|
||||
|
@ -60,11 +62,40 @@ export function AggTypesBucketsTermsProvider(Private) {
|
|||
return agg.getFieldDisplayName() + ': ' + params.order.display;
|
||||
},
|
||||
createFilter: createFilter,
|
||||
postFlightRequest: async (resp, aggConfigs, aggConfig, nestedSearchSource) => {
|
||||
if (aggConfig.params.otherBucket) {
|
||||
const filterAgg = buildOtherBucketAgg(aggConfigs, aggConfig, resp);
|
||||
nestedSearchSource.set('aggs', filterAgg);
|
||||
const response = await nestedSearchSource.fetchAsRejectablePromise();
|
||||
resp = mergeOtherBucketAggResponse(aggConfigs, resp, response, aggConfig, filterAgg());
|
||||
}
|
||||
if (aggConfig.params.missingBucket) {
|
||||
resp = updateMissingBucket(resp, aggConfigs, aggConfig);
|
||||
}
|
||||
return resp;
|
||||
},
|
||||
params: [
|
||||
{
|
||||
name: 'field',
|
||||
filterFieldTypes: ['number', 'boolean', 'date', 'ip', 'string']
|
||||
},
|
||||
{
|
||||
name: 'otherBucket',
|
||||
default: false,
|
||||
write: _.noop
|
||||
}, {
|
||||
name: 'otherBucketLabel',
|
||||
default: 'Other',
|
||||
write: _.noop
|
||||
}, {
|
||||
name: 'missingBucket',
|
||||
default: false,
|
||||
write: _.noop
|
||||
}, {
|
||||
name: 'missingBucketLabel',
|
||||
default: 'Missing',
|
||||
write: _.noop
|
||||
},
|
||||
{
|
||||
name: 'exclude',
|
||||
type: 'string',
|
||||
|
@ -170,6 +201,10 @@ export function AggTypesBucketsTermsProvider(Private) {
|
|||
output.params.valueType = agg.getField().type === 'number' ? 'float' : agg.getField().type;
|
||||
}
|
||||
|
||||
if (agg.params.missingBucket) {
|
||||
output.params.missing = '__missing__';
|
||||
}
|
||||
|
||||
if (!orderAgg) {
|
||||
order[agg.params.orderBy || '_count'] = dir;
|
||||
return;
|
||||
|
|
|
@ -1,25 +1,79 @@
|
|||
<div class="vis-editor-agg-form-row">
|
||||
<div ng-if="agg.type.params.byName.order" class="form-group">
|
||||
<label for="visEditorOrderByOrder{{agg.id}}">Order</label>
|
||||
<select
|
||||
id="visEditorOrderByOrder{{agg.id}}"
|
||||
name="order"
|
||||
ng-model="agg.params.order"
|
||||
required
|
||||
ng-options="opt as opt.display for opt in aggParam.options"
|
||||
class="form-control">
|
||||
</select>
|
||||
<div>
|
||||
<div class="vis-editor-agg-form-row">
|
||||
<div ng-if="agg.type.params.byName.order" class="form-group">
|
||||
<label for="visEditorOrderByOrder{{agg.id}}">Order</label>
|
||||
<select
|
||||
id="visEditorOrderByOrder{{agg.id}}"
|
||||
name="order"
|
||||
ng-model="agg.params.order"
|
||||
required
|
||||
ng-options="opt as opt.display for opt in aggParam.options"
|
||||
class="form-control">
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="visEditorOrderBySize{{agg.id}}">Size</label>
|
||||
<input
|
||||
id="visEditorOrderBySize{{agg.id}}"
|
||||
name="size"
|
||||
ng-model="agg.params.size"
|
||||
required
|
||||
class="form-control"
|
||||
type="number"
|
||||
min="1"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="visEditorOrderBySize{{agg.id}}">Size</label>
|
||||
<input
|
||||
id="visEditorOrderBySize{{agg.id}}"
|
||||
name="size"
|
||||
ng-model="agg.params.size"
|
||||
required
|
||||
class="form-control"
|
||||
type="number"
|
||||
min="1"
|
||||
>
|
||||
<div class="vis-editor-agg-form-row">
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input type="checkbox"
|
||||
class="kuiCheckBox"
|
||||
name="showOther"
|
||||
ng-model="agg.params.otherBucket">
|
||||
Group other values in separate bucket
|
||||
<kbn-info info="Values not in the top N are grouped in this bucket. To include documents with missing values, enable 'Show missing values'."></kbn-info>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="vis-editor-agg-form-row" ng-if="agg.params.otherBucket">
|
||||
<div class="form-group">
|
||||
<label>
|
||||
Label for other bucket
|
||||
</label>
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
ng-model="agg.params.otherBucketLabel"
|
||||
class="form-control kuiSideBarInput"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="vis-editor-agg-form-row">
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input type="checkbox"
|
||||
class="kuiCheckBox"
|
||||
name="showMissing"
|
||||
ng-model="agg.params.missingBucket">
|
||||
Show missing values
|
||||
<kbn-info info="When enabled, include documents with missing values in the search. If this bucket is in the top N, it appears in the chart. If not in the top N, and you enable 'Group other values in separate bucket', Elasticsearch adds the missing values to the 'other' bucket."></kbn-info>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="vis-editor-agg-form-row" ng-show="agg.params.missingBucket">
|
||||
<div class="form-group">
|
||||
<label>
|
||||
Label for missing values
|
||||
</label>
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
ng-model="agg.params.missingBucketLabel"
|
||||
class="form-control kuiSideBarInput"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -49,6 +49,7 @@ export function FilterBarClickHandlerProvider(Notifier, Private) {
|
|||
}
|
||||
}
|
||||
})
|
||||
.flatten()
|
||||
.filter(Boolean)
|
||||
.value();
|
||||
|
||||
|
|
|
@ -3,10 +3,11 @@ import chrome from 'ui/chrome';
|
|||
let i = 0;
|
||||
|
||||
// eslint-disable-next-line @elastic/kibana-custom/no-default-export
|
||||
export default function AggConfigResult(aggConfig, parent, value, key) {
|
||||
export default function AggConfigResult(aggConfig, parent, value, key, filters) {
|
||||
this.key = key;
|
||||
this.value = value;
|
||||
this.aggConfig = aggConfig;
|
||||
this.filters = filters;
|
||||
this.$parent = parent;
|
||||
this.$order = ++i;
|
||||
|
||||
|
@ -34,7 +35,7 @@ AggConfigResult.prototype.getPath = function () {
|
|||
* @returns {object} Elasticsearch filter
|
||||
*/
|
||||
AggConfigResult.prototype.createFilter = function () {
|
||||
return this.aggConfig.createFilter(this.key);
|
||||
return this.filters || this.aggConfig.createFilter(this.key);
|
||||
};
|
||||
|
||||
AggConfigResult.prototype.toString = function (contentType) {
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import _ from 'lodash';
|
||||
|
||||
import { SearchSourceProvider } from 'ui/courier/data_source/search_source';
|
||||
import { VisRequestHandlersRegistryProvider } from 'ui/registry/vis_request_handlers';
|
||||
|
||||
const CourierRequestHandlerProvider = function (Private, courier, timefilter) {
|
||||
const SearchSource = Private(SearchSourceProvider);
|
||||
|
||||
return {
|
||||
name: 'courier',
|
||||
handler: function (vis, appState, uiState, queryFilter, searchSource) {
|
||||
|
@ -35,15 +37,20 @@ const CourierRequestHandlerProvider = function (Private, courier, timefilter) {
|
|||
};
|
||||
|
||||
searchSource.rawResponse = resp;
|
||||
resolve(resp);
|
||||
|
||||
resolve(_.cloneDeep(resp));
|
||||
}).catch(e => reject(e));
|
||||
|
||||
courier.fetch();
|
||||
} else {
|
||||
resolve(searchSource.rawResponse);
|
||||
resolve(_.cloneDeep(searchSource.rawResponse));
|
||||
}
|
||||
|
||||
|
||||
}).then(async resp => {
|
||||
for (const agg of vis.aggs) {
|
||||
const nestedSearchSource = new SearchSource().inherits(searchSource);
|
||||
resp = await agg.type.postFlightRequest(resp, vis.aggs, agg, nestedSearchSource);
|
||||
}
|
||||
return resp;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -3,11 +3,11 @@ import expect from 'expect.js';
|
|||
export default function ({ getService, getPageObjects }) {
|
||||
const log = getService('log');
|
||||
const PageObjects = getPageObjects(['common', 'visualize', 'header', 'settings']);
|
||||
const fromTime = '2015-09-19 06:31:44.000';
|
||||
const toTime = '2015-09-23 18:31:44.000';
|
||||
|
||||
describe('visualize app', function describeIndexTests() {
|
||||
before(function () {
|
||||
const fromTime = '2015-09-19 06:31:44.000';
|
||||
const toTime = '2015-09-23 18:31:44.000';
|
||||
|
||||
log.debug('navigateToApp visualize');
|
||||
return PageObjects.common.navigateToUrl('visualize', 'new')
|
||||
|
@ -110,6 +110,58 @@ export default function ({ getService, getPageObjects }) {
|
|||
expect(data.trim().split('\n')).to.eql(expectedTableData);
|
||||
});
|
||||
});
|
||||
|
||||
it('should show other and missing bucket', function () {
|
||||
const expectedTableData = [ 'win 8', 'win xp', 'win 7', 'ios', 'Missing', 'Other' ];
|
||||
|
||||
return PageObjects.common.navigateToUrl('visualize', 'new')
|
||||
.then(function () {
|
||||
log.debug('clickPieChart');
|
||||
return PageObjects.visualize.clickPieChart();
|
||||
})
|
||||
.then(function clickNewSearch() {
|
||||
return PageObjects.visualize.clickNewSearch();
|
||||
})
|
||||
.then(function setAbsoluteRange() {
|
||||
log.debug('Set absolute time range from \"' + fromTime + '\" to \"' + toTime + '\"');
|
||||
return PageObjects.header.setAbsoluteRange(fromTime, toTime);
|
||||
})
|
||||
.then(function () {
|
||||
log.debug('select bucket Split Slices');
|
||||
return PageObjects.visualize.clickBucket('Split Slices');
|
||||
})
|
||||
.then(function () {
|
||||
log.debug('Click aggregation Histogram');
|
||||
return PageObjects.visualize.selectAggregation('Terms');
|
||||
})
|
||||
.then(function () {
|
||||
log.debug('Click field memory');
|
||||
return PageObjects.visualize.selectField('machine.os.raw');
|
||||
})
|
||||
.then(function () {
|
||||
return PageObjects.visualize.toggleOtherBucket();
|
||||
})
|
||||
.then(function () {
|
||||
return PageObjects.visualize.toggleMissingBucket();
|
||||
})
|
||||
.then(function () {
|
||||
log.debug('clickGo');
|
||||
return PageObjects.visualize.clickGo();
|
||||
})
|
||||
.then(function waitForVisualization() {
|
||||
return PageObjects.header.waitUntilLoadingHasFinished();
|
||||
})
|
||||
.then(function sleep() {
|
||||
return PageObjects.common.sleep(1003);
|
||||
})
|
||||
.then(function () {
|
||||
return PageObjects.visualize.getPieChartLabels();
|
||||
})
|
||||
.then(function (pieData) {
|
||||
log.debug('pieData.length = ' + pieData.length);
|
||||
expect(pieData).to.eql(expectedTableData);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -385,6 +385,14 @@ export function VisualizePageProvider({ getService, getPageObjects }) {
|
|||
await input.type(newValue);
|
||||
}
|
||||
|
||||
async toggleOtherBucket() {
|
||||
return await find.clickByCssSelector('input[name="showOther"]');
|
||||
}
|
||||
|
||||
async toggleMissingBucket() {
|
||||
return await find.clickByCssSelector('input[name="showMissing"]');
|
||||
}
|
||||
|
||||
async clickGo() {
|
||||
await testSubjects.click('visualizeEditorRenderButton');
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
|
@ -602,6 +610,13 @@ export function VisualizePageProvider({ getService, getPageObjects }) {
|
|||
return await Promise.all(getChartTypesPromises);
|
||||
}
|
||||
|
||||
async getPieChartLabels() {
|
||||
const chartTypes = await find.allByCssSelector('path.slice', defaultFindTimeout * 2);
|
||||
|
||||
const getChartTypesPromises = chartTypes.map(async chart => await chart.getAttribute('data-label'));
|
||||
return await Promise.all(getChartTypesPromises);
|
||||
}
|
||||
|
||||
async getChartAreaWidth() {
|
||||
const rect = await retry.try(async () => find.byCssSelector('clipPath rect'));
|
||||
return await rect.getAttribute('width');
|
||||
|
|
Loading…
Reference in a new issue