other and missing bucket support for terms agg (#15525)

This commit is contained in:
Peter Pisljar 2018-01-05 15:12:58 +01:00 committed by GitHub
parent b709ee989a
commit 2fd41d5a6b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 662 additions and 37 deletions

View file

@ -9,7 +9,8 @@ export function HierarchicalTransformAggregationProvider() {
agg,
parent && parent.aggConfigResult,
metric.getValue(bucket),
agg.getKey(bucket)
agg.getKey(bucket),
bucket.filters
);
const branch = {

View file

@ -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;

View file

@ -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';

View file

@ -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) {

View file

@ -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' }
});
});
});
});

View file

@ -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';

View file

@ -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;
}

View 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 };
};

View file

@ -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;

View file

@ -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>

View file

@ -49,6 +49,7 @@ export function FilterBarClickHandlerProvider(Notifier, Private) {
}
}
})
.flatten()
.filter(Boolean)
.value();

View file

@ -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) {

View file

@ -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;
});
}
};

View file

@ -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);
});
});
});
});
}

View file

@ -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');