[data.search.aggs]: Expression functions for bucket agg types (#64772)

* [data.search.aggs]: Expression functions for bucket agg types - ranges agg types + significant terms

* new portion of changes

* add geo_tile_fn

* add geo_hash_fn

* Update src/plugins/data/public/search/aggs/buckets/filter_fn.ts

Co-authored-by: Luke Elmers <lukeelmers@gmail.com>

* Update src/plugins/data/public/search/aggs/buckets/geo_tile_fn.ts

Co-authored-by: Luke Elmers <lukeelmers@gmail.com>

* Update src/plugins/data/public/search/aggs/buckets/geo_hash_fn.ts

Co-authored-by: Luke Elmers <lukeelmers@gmail.com>

* Update src/plugins/data/public/search/aggs/buckets/ip_range_fn.ts

Co-authored-by: Luke Elmers <lukeelmers@gmail.com>

* Update src/plugins/data/public/search/aggs/buckets/geo_hash_fn.ts

Co-authored-by: Luke Elmers <lukeelmers@gmail.com>

* Update src/plugins/data/public/search/aggs/buckets/geo_hash_fn.ts

Co-authored-by: Luke Elmers <lukeelmers@gmail.com>

* Update src/plugins/data/public/search/aggs/buckets/geo_hash_fn.ts

Co-authored-by: Luke Elmers <lukeelmers@gmail.com>

* create BaseAggParams

* add filters_fn

* add histogram / date_histogram expression functions

* cleanup

* terms - order should be optional

* add custom label params

Co-authored-by: Luke Elmers <lukeelmers@gmail.com>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Alexey Antonov 2020-05-05 11:11:21 +03:00 committed by GitHub
parent d77d2e4b77
commit 6349575ec1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 2551 additions and 78 deletions

View file

@ -14,6 +14,7 @@ import { Component } from 'react';
import { CoreSetup } from 'src/core/public';
import { CoreStart } from 'kibana/public';
import { CoreStart as CoreStart_2 } from 'src/core/public';
import { Ensure } from '@kbn/utility-types';
import { EuiButtonEmptyProps } from '@elastic/eui';
import { EuiComboBoxProps } from '@elastic/eui';
import { EuiConfirmModalProps } from '@elastic/eui';

View file

@ -19,7 +19,7 @@
import _ from 'lodash';
import { i18n } from '@kbn/i18n';
import { Assign } from '@kbn/utility-types';
import { Assign, Ensure } from '@kbn/utility-types';
import { ExpressionAstFunction, ExpressionAstArgument } from 'src/plugins/expressions/public';
import { IAggType } from './agg_type';
import { writeParams } from './agg_params';
@ -31,17 +31,22 @@ import { FieldFormatsStart } from '../../field_formats';
type State = string | number | boolean | null | undefined | SerializableState;
interface SerializableState {
/** @internal **/
export interface SerializableState {
[key: string]: State | State[];
}
export interface AggConfigSerialized {
type: string;
enabled?: boolean;
id?: string;
params?: SerializableState;
schema?: string;
}
/** @internal **/
export type AggConfigSerialized = Ensure<
{
type: string;
enabled?: boolean;
id?: string;
params?: SerializableState;
schema?: string;
},
SerializableState
>;
export interface AggConfigDependencies {
fieldFormats: FieldFormatsStart;

View file

@ -105,6 +105,29 @@ export const getAggTypes = ({
],
});
/** Buckets: **/
import { aggFilter } from './buckets/filter_fn';
import { aggFilters } from './buckets/filters_fn';
import { aggSignificantTerms } from './buckets/significant_terms_fn';
import { aggIpRange } from './buckets/ip_range_fn';
import { aggDateRange } from './buckets/date_range_fn';
import { aggRange } from './buckets/range_fn';
import { aggGeoTile } from './buckets/geo_tile_fn';
import { aggGeoHash } from './buckets/geo_hash_fn';
import { aggHistogram } from './buckets/histogram_fn';
import { aggDateHistogram } from './buckets/date_histogram_fn';
import { aggTerms } from './buckets/terms_fn';
export const getAggTypesFunctions = () => [aggTerms];
export const getAggTypesFunctions = () => [
aggFilter,
aggFilters,
aggSignificantTerms,
aggIpRange,
aggDateRange,
aggRange,
aggGeoTile,
aggGeoHash,
aggDateHistogram,
aggHistogram,
aggTerms,
];

View file

@ -27,7 +27,7 @@ import { BucketAggType, IBucketAggConfig } from './bucket_agg_type';
import { BUCKET_TYPES } from './bucket_agg_types';
import { createFilterDateHistogram } from './create_filter/date_histogram';
import { intervalOptions } from './_interval_options';
import { dateHistogramInterval } from '../../../../common';
import { dateHistogramInterval, TimeRange } from '../../../../common';
import { writeParams } from '../agg_params';
import { isMetricAggType } from '../metrics/metric_agg_type';
@ -35,6 +35,8 @@ import { FIELD_FORMAT_IDS, KBN_FIELD_TYPES } from '../../../../common';
import { TimefilterContract } from '../../../query';
import { QuerySetup } from '../../../query/query_service';
import { GetInternalStartServicesFn } from '../../../types';
import { BaseAggParams } from '../types';
import { ExtendedBounds } from './lib/extended_bounds';
const detectedTimezone = moment.tz.guess();
const tzOffset = moment().format('Z');
@ -67,6 +69,19 @@ export function isDateHistogramBucketAggConfig(agg: any): agg is IBucketDateHist
return Boolean(agg.buckets);
}
export interface AggParamsDateHistogram extends BaseAggParams {
field?: string;
timeRange?: TimeRange;
useNormalizedEsInterval?: boolean;
scaleMetricValues?: boolean;
interval?: string;
time_zone?: string;
drop_partials?: boolean;
format?: string;
min_doc_count?: number;
extended_bounds?: ExtendedBounds;
}
export const getDateHistogramBucketAgg = ({
uiSettings,
query,
@ -89,6 +104,7 @@ export const getDateHistogramBucketAgg = ({
}
const field = agg.getFieldDisplayName();
return i18n.translate('data.search.aggs.buckets.dateHistogramLabel', {
defaultMessage: '{fieldName} per {intervalDescription}',
values: {

View file

@ -0,0 +1,120 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { functionWrapper } from '../test_helpers';
import { aggDateHistogram } from './date_histogram_fn';
describe('agg_expression_functions', () => {
describe('aggDateHistogram', () => {
const fn = functionWrapper(aggDateHistogram());
test('fills in defaults when only required args are provided', () => {
const actual = fn({});
expect(actual).toMatchInlineSnapshot(`
Object {
"type": "agg_type",
"value": Object {
"enabled": true,
"id": undefined,
"params": Object {
"customLabel": undefined,
"drop_partials": undefined,
"extended_bounds": undefined,
"field": undefined,
"format": undefined,
"interval": undefined,
"json": undefined,
"min_doc_count": undefined,
"scaleMetricValues": undefined,
"timeRange": undefined,
"time_zone": undefined,
"useNormalizedEsInterval": undefined,
},
"schema": undefined,
"type": "date_histogram",
},
}
`);
});
test('includes optional params when they are provided', () => {
const actual = fn({
field: 'field',
timeRange: JSON.stringify({
from: 'from',
to: 'to',
}),
useNormalizedEsInterval: true,
scaleMetricValues: true,
interval: 'interval',
time_zone: 'time_zone',
drop_partials: false,
format: 'format',
min_doc_count: 1,
extended_bounds: JSON.stringify({
min: 1,
max: 2,
}),
});
expect(actual.value).toMatchInlineSnapshot(`
Object {
"enabled": true,
"id": undefined,
"params": Object {
"customLabel": undefined,
"drop_partials": false,
"extended_bounds": Object {
"max": 2,
"min": 1,
},
"field": "field",
"format": "format",
"interval": "interval",
"json": undefined,
"min_doc_count": 1,
"scaleMetricValues": true,
"timeRange": Object {
"from": "from",
"to": "to",
},
"time_zone": "time_zone",
"useNormalizedEsInterval": true,
},
"schema": undefined,
"type": "date_histogram",
}
`);
});
test('correctly parses json string argument', () => {
const actual = fn({
json: '{ "foo": true }',
});
expect(actual.value.params.json).toEqual({ foo: true });
expect(() => {
fn({
json: '/// intentionally malformed json ///',
});
}).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`);
});
});
});

View file

@ -0,0 +1,155 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { i18n } from '@kbn/i18n';
import { Assign } from '@kbn/utility-types';
import { ExpressionFunctionDefinition } from '../../../../../expressions/public';
import { AggExpressionType, AggExpressionFunctionArgs, BUCKET_TYPES } from '../';
import { getParsedValue } from '../utils/get_parsed_value';
const fnName = 'aggDateHistogram';
type Input = any;
type AggArgs = AggExpressionFunctionArgs<typeof BUCKET_TYPES.DATE_HISTOGRAM>;
type Arguments = Assign<AggArgs, { timeRange?: string; extended_bounds?: string }>;
type Output = AggExpressionType;
type FunctionDefinition = ExpressionFunctionDefinition<typeof fnName, Input, Arguments, Output>;
export const aggDateHistogram = (): FunctionDefinition => ({
name: fnName,
help: i18n.translate('data.search.aggs.function.buckets.dateHistogram.help', {
defaultMessage: 'Generates a serialized agg config for a Histogram agg',
}),
type: 'agg_type',
args: {
id: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.dateHistogram.id.help', {
defaultMessage: 'ID for this aggregation',
}),
},
enabled: {
types: ['boolean'],
default: true,
help: i18n.translate('data.search.aggs.buckets.dateHistogram.enabled.help', {
defaultMessage: 'Specifies whether this aggregation should be enabled',
}),
},
schema: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.dateHistogram.schema.help', {
defaultMessage: 'Schema to use for this aggregation',
}),
},
field: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.dateHistogram.field.help', {
defaultMessage: 'Field to use for this aggregation',
}),
},
useNormalizedEsInterval: {
types: ['boolean'],
help: i18n.translate('data.search.aggs.buckets.dateHistogram.useNormalizedEsInterval.help', {
defaultMessage: 'Specifies whether to use useNormalizedEsInterval for this aggregation',
}),
},
time_zone: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.dateHistogram.timeZone.help', {
defaultMessage: 'Time zone to use for this aggregation',
}),
},
format: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.dateHistogram.format.help', {
defaultMessage: 'Format to use for this aggregation',
}),
},
scaleMetricValues: {
types: ['boolean'],
help: i18n.translate('data.search.aggs.buckets.dateHistogram.scaleMetricValues.help', {
defaultMessage: 'Specifies whether to use scaleMetricValues for this aggregation',
}),
},
interval: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.dateHistogram.interval.help', {
defaultMessage: 'Interval to use for this aggregation',
}),
},
timeRange: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.dateHistogram.timeRange.help', {
defaultMessage: 'Time Range to use for this aggregation',
}),
},
min_doc_count: {
types: ['number'],
help: i18n.translate('data.search.aggs.buckets.dateHistogram.minDocCount.help', {
defaultMessage: 'Minimum document count to use for this aggregation',
}),
},
drop_partials: {
types: ['boolean'],
help: i18n.translate('data.search.aggs.buckets.dateHistogram.dropPartials.help', {
defaultMessage: 'Specifies whether to use drop_partials for this aggregation',
}),
},
extended_bounds: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.dateHistogram.extendedBounds.help', {
defaultMessage:
'With extended_bounds setting, you now can "force" the histogram aggregation to start building buckets on a specific min value and also keep on building buckets up to a max value ',
}),
},
json: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.dateHistogram.json.help', {
defaultMessage: 'Advanced json to include when the agg is sent to Elasticsearch',
}),
},
customLabel: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.dateHistogram.customLabel.help', {
defaultMessage: 'Represents a custom label for this aggregation',
}),
},
},
fn: (input, args) => {
const { id, enabled, schema, ...rest } = args;
return {
type: 'agg_type',
value: {
id,
enabled,
schema,
type: BUCKET_TYPES.DATE_HISTOGRAM,
params: {
...rest,
timeRange: getParsedValue(args, 'timeRange'),
extended_bounds: getParsedValue(args, 'extended_bounds'),
json: getParsedValue(args, 'json'),
},
},
};
},
});

View file

@ -29,6 +29,7 @@ import { convertDateRangeToString, DateRangeKey } from './lib/date_range';
import { KBN_FIELD_TYPES, FieldFormat, TEXT_CONTEXT_TYPE } from '../../../../common';
import { GetInternalStartServicesFn } from '../../../types';
import { BaseAggParams } from '../types';
const dateRangeTitle = i18n.translate('data.search.aggs.buckets.dateRangeTitle', {
defaultMessage: 'Date Range',
@ -39,6 +40,12 @@ export interface DateRangeBucketAggDependencies {
getInternalStartServices: GetInternalStartServicesFn;
}
export interface AggParamsDateRange extends BaseAggParams {
field?: string;
ranges?: DateRangeKey[];
time_zone?: string;
}
export const getDateRangeBucketAgg = ({
uiSettings,
getInternalStartServices,

View file

@ -0,0 +1,101 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { functionWrapper } from '../test_helpers';
import { aggDateRange } from './date_range_fn';
describe('agg_expression_functions', () => {
describe('aggDateRange', () => {
const fn = functionWrapper(aggDateRange());
test('fills in defaults when only required args are provided', () => {
const actual = fn({});
expect(actual).toMatchInlineSnapshot(`
Object {
"type": "agg_type",
"value": Object {
"enabled": true,
"id": undefined,
"params": Object {
"customLabel": undefined,
"field": undefined,
"json": undefined,
"ranges": undefined,
"time_zone": undefined,
},
"schema": undefined,
"type": "date_range",
},
}
`);
});
test('includes optional params when they are provided', () => {
const actual = fn({
field: 'date_field',
time_zone: 'UTC +3',
ranges: JSON.stringify([
{ from: 'now-1w/w', to: 'now' },
{ from: 1588163532470, to: 1588163532481 },
]),
});
expect(actual.value).toMatchInlineSnapshot(`
Object {
"enabled": true,
"id": undefined,
"params": Object {
"customLabel": undefined,
"field": "date_field",
"json": undefined,
"ranges": Array [
Object {
"from": "now-1w/w",
"to": "now",
},
Object {
"from": 1588163532470,
"to": 1588163532481,
},
],
"time_zone": "UTC +3",
},
"schema": undefined,
"type": "date_range",
}
`);
});
test('correctly parses json string argument', () => {
const actual = fn({
field: 'date_field',
json: '{ "foo": true }',
});
expect(actual.value.params.json).toEqual({ foo: true });
expect(() => {
fn({
field: 'date_field',
json: '/// intentionally malformed json ///',
});
}).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`);
});
});
});

View file

@ -0,0 +1,111 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { i18n } from '@kbn/i18n';
import { Assign } from '@kbn/utility-types';
import { ExpressionFunctionDefinition } from '../../../../../expressions/public';
import { AggExpressionType, AggExpressionFunctionArgs, BUCKET_TYPES } from '../';
import { getParsedValue } from '../utils/get_parsed_value';
const fnName = 'aggDateRange';
type Input = any;
type AggArgs = AggExpressionFunctionArgs<typeof BUCKET_TYPES.DATE_RANGE>;
type Arguments = Assign<AggArgs, { ranges?: string }>;
type Output = AggExpressionType;
type FunctionDefinition = ExpressionFunctionDefinition<typeof fnName, Input, Arguments, Output>;
export const aggDateRange = (): FunctionDefinition => ({
name: fnName,
help: i18n.translate('data.search.aggs.function.buckets.dateRange.help', {
defaultMessage: 'Generates a serialized agg config for a Date Range agg',
}),
type: 'agg_type',
args: {
id: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.dateRange.id.help', {
defaultMessage: 'ID for this aggregation',
}),
},
enabled: {
types: ['boolean'],
default: true,
help: i18n.translate('data.search.aggs.buckets.dateRange.enabled.help', {
defaultMessage: 'Specifies whether this aggregation should be enabled',
}),
},
schema: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.dateRange.schema.help', {
defaultMessage: 'Schema to use for this aggregation',
}),
},
field: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.dateRange.field.help', {
defaultMessage: 'Field to use for this aggregation',
}),
},
ranges: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.dateRange.ranges.help', {
defaultMessage: 'Serialized ranges to use for this aggregation.',
}),
},
time_zone: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.dateRange.timeZone.help', {
defaultMessage: 'Time zone to use for this aggregation.',
}),
},
json: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.dateRange.json.help', {
defaultMessage: 'Advanced json to include when the agg is sent to Elasticsearch',
}),
},
customLabel: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.dateRange.customLabel.help', {
defaultMessage: 'Represents a custom label for this aggregation',
}),
},
},
fn: (input, args) => {
const { id, enabled, schema, ...rest } = args;
return {
type: 'agg_type',
value: {
id,
enabled,
schema,
type: BUCKET_TYPES.DATE_RANGE,
params: {
...rest,
json: getParsedValue(args, 'json'),
ranges: getParsedValue(args, 'ranges'),
},
},
};
},
});

View file

@ -21,6 +21,8 @@ import { i18n } from '@kbn/i18n';
import { BucketAggType } from './bucket_agg_type';
import { BUCKET_TYPES } from './bucket_agg_types';
import { GetInternalStartServicesFn } from '../../../types';
import { GeoBoundingBox } from './lib/geo_point';
import { BaseAggParams } from '../types';
const filterTitle = i18n.translate('data.search.aggs.buckets.filterTitle', {
defaultMessage: 'Filter',
@ -30,6 +32,10 @@ export interface FilterBucketAggDependencies {
getInternalStartServices: GetInternalStartServicesFn;
}
export interface AggParamsFilter extends BaseAggParams {
geo_bounding_box?: GeoBoundingBox;
}
export const getFilterBucketAgg = ({ getInternalStartServices }: FilterBucketAggDependencies) =>
new BucketAggType(
{

View file

@ -0,0 +1,85 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { functionWrapper } from '../test_helpers';
import { aggFilter } from './filter_fn';
describe('agg_expression_functions', () => {
describe('aggFilter', () => {
const fn = functionWrapper(aggFilter());
test('fills in defaults when only required args are provided', () => {
const actual = fn({});
expect(actual).toMatchInlineSnapshot(`
Object {
"type": "agg_type",
"value": Object {
"enabled": true,
"id": undefined,
"params": Object {
"customLabel": undefined,
"geo_bounding_box": undefined,
"json": undefined,
},
"schema": undefined,
"type": "filter",
},
}
`);
});
test('includes optional params when they are provided', () => {
const actual = fn({
geo_bounding_box: JSON.stringify({
wkt: 'BBOX (-74.1, -71.12, 40.73, 40.01)',
}),
});
expect(actual.value).toMatchInlineSnapshot(`
Object {
"enabled": true,
"id": undefined,
"params": Object {
"customLabel": undefined,
"geo_bounding_box": Object {
"wkt": "BBOX (-74.1, -71.12, 40.73, 40.01)",
},
"json": undefined,
},
"schema": undefined,
"type": "filter",
}
`);
});
test('correctly parses json string argument', () => {
const actual = fn({
json: '{ "foo": true }',
});
expect(actual.value.params.json).toEqual({ foo: true });
expect(() => {
fn({
json: '/// intentionally malformed json ///',
});
}).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`);
});
});
});

View file

@ -0,0 +1,99 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { i18n } from '@kbn/i18n';
import { Assign } from '@kbn/utility-types';
import { ExpressionFunctionDefinition } from '../../../../../expressions/public';
import { AggExpressionType, AggExpressionFunctionArgs, BUCKET_TYPES } from '../';
import { getParsedValue } from '../utils/get_parsed_value';
const fnName = 'aggFilter';
type Input = any;
type AggArgs = AggExpressionFunctionArgs<typeof BUCKET_TYPES.FILTER>;
type Arguments = Assign<AggArgs, { geo_bounding_box?: string }>;
type Output = AggExpressionType;
type FunctionDefinition = ExpressionFunctionDefinition<typeof fnName, Input, Arguments, Output>;
export const aggFilter = (): FunctionDefinition => ({
name: fnName,
help: i18n.translate('data.search.aggs.function.buckets.filter.help', {
defaultMessage: 'Generates a serialized agg config for a Filter agg',
}),
type: 'agg_type',
args: {
id: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.filter.id.help', {
defaultMessage: 'ID for this aggregation',
}),
},
enabled: {
types: ['boolean'],
default: true,
help: i18n.translate('data.search.aggs.buckets.filter.enabled.help', {
defaultMessage: 'Specifies whether this aggregation should be enabled',
}),
},
schema: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.filter.schema.help', {
defaultMessage: 'Schema to use for this aggregation',
}),
},
geo_bounding_box: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.filter.geoBoundingBox.help', {
defaultMessage: 'Filter results based on a point location within a bounding box',
}),
},
json: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.filter.json.help', {
defaultMessage: 'Advanced json to include when the agg is sent to Elasticsearch',
}),
},
customLabel: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.filter.customLabel.help', {
defaultMessage: 'Represents a custom label for this aggregation',
}),
},
},
fn: (input, args) => {
const { id, enabled, schema, ...rest } = args;
return {
type: 'agg_type',
value: {
id,
enabled,
schema,
type: BUCKET_TYPES.FILTER,
params: {
...rest,
json: getParsedValue(args, 'json'),
geo_bounding_box: getParsedValue(args, 'geo_bounding_box'),
},
},
};
},
});

View file

@ -29,6 +29,7 @@ import { Storage } from '../../../../../../plugins/kibana_utils/public';
import { getEsQueryConfig, buildEsQuery, Query } from '../../../../common';
import { getQueryLog } from '../../../query';
import { GetInternalStartServicesFn } from '../../../types';
import { BaseAggParams } from '../types';
const filtersTitle = i18n.translate('data.search.aggs.buckets.filtersTitle', {
defaultMessage: 'Filters',
@ -47,6 +48,13 @@ export interface FiltersBucketAggDependencies {
getInternalStartServices: GetInternalStartServicesFn;
}
export interface AggParamsFilters extends BaseAggParams {
filters?: Array<{
input: Query;
label: string;
}>;
}
export const getFiltersBucketAgg = ({
uiSettings,
getInternalStartServices,

View file

@ -0,0 +1,91 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { functionWrapper } from '../test_helpers';
import { aggFilters } from './filters_fn';
describe('agg_expression_functions', () => {
describe('aggFilters', () => {
const fn = functionWrapper(aggFilters());
test('fills in defaults when only required args are provided', () => {
const actual = fn({});
expect(actual).toMatchInlineSnapshot(`
Object {
"type": "agg_type",
"value": Object {
"enabled": true,
"id": undefined,
"params": Object {
"filters": undefined,
"json": undefined,
},
"schema": undefined,
"type": "filters",
},
}
`);
});
test('includes optional params when they are provided', () => {
const actual = fn({
filters: JSON.stringify([
{
query: 'query',
language: 'lucene',
label: 'test',
},
]),
});
expect(actual.value).toMatchInlineSnapshot(`
Object {
"enabled": true,
"id": undefined,
"params": Object {
"filters": Array [
Object {
"label": "test",
"language": "lucene",
"query": "query",
},
],
"json": undefined,
},
"schema": undefined,
"type": "filters",
}
`);
});
test('correctly parses json string argument', () => {
const actual = fn({
json: '{ "foo": true }',
});
expect(actual.value.params.json).toEqual({ foo: true });
expect(() => {
fn({
json: '/// intentionally malformed json ///',
});
}).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`);
});
});
});

View file

@ -0,0 +1,93 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { i18n } from '@kbn/i18n';
import { Assign } from '@kbn/utility-types';
import { ExpressionFunctionDefinition } from '../../../../../expressions/public';
import { AggExpressionType, AggExpressionFunctionArgs, BUCKET_TYPES } from '../';
import { getParsedValue } from '../utils/get_parsed_value';
const fnName = 'aggFilters';
type Input = any;
type AggArgs = AggExpressionFunctionArgs<typeof BUCKET_TYPES.FILTERS>;
type Arguments = Assign<AggArgs, { filters?: string }>;
type Output = AggExpressionType;
type FunctionDefinition = ExpressionFunctionDefinition<typeof fnName, Input, Arguments, Output>;
export const aggFilters = (): FunctionDefinition => ({
name: fnName,
help: i18n.translate('data.search.aggs.function.buckets.filters.help', {
defaultMessage: 'Generates a serialized agg config for a Filter agg',
}),
type: 'agg_type',
args: {
id: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.filters.id.help', {
defaultMessage: 'ID for this aggregation',
}),
},
enabled: {
types: ['boolean'],
default: true,
help: i18n.translate('data.search.aggs.buckets.filters.enabled.help', {
defaultMessage: 'Specifies whether this aggregation should be enabled',
}),
},
schema: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.filters.schema.help', {
defaultMessage: 'Schema to use for this aggregation',
}),
},
filters: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.filters.filters.help', {
defaultMessage: 'Filters to use for this aggregation',
}),
},
json: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.filters.json.help', {
defaultMessage: 'Advanced json to include when the agg is sent to Elasticsearch',
}),
},
},
fn: (input, args) => {
const { id, enabled, schema, ...rest } = args;
return {
type: 'agg_type',
value: {
id,
enabled,
schema,
type: BUCKET_TYPES.FILTERS,
params: {
...rest,
filters: getParsedValue(args, 'filters'),
json: getParsedValue(args, 'json'),
},
},
};
},
});

View file

@ -22,6 +22,8 @@ import { BucketAggType, IBucketAggConfig } from './bucket_agg_type';
import { KBN_FIELD_TYPES } from '../../../../common';
import { BUCKET_TYPES } from './bucket_agg_types';
import { GetInternalStartServicesFn } from '../../../types';
import { GeoBoundingBox } from './lib/geo_point';
import { BaseAggParams } from '../types';
const defaultBoundingBox = {
top_left: { lat: 1, lon: 1 },
@ -38,6 +40,15 @@ export interface GeoHashBucketAggDependencies {
getInternalStartServices: GetInternalStartServicesFn;
}
export interface AggParamsGeoHash extends BaseAggParams {
field: string;
autoPrecision?: boolean;
precision?: number;
useGeocentroid?: boolean;
isFilteredByCollar?: boolean;
boundingBox?: GeoBoundingBox;
}
export const getGeoHashBucketAgg = ({ getInternalStartServices }: GeoHashBucketAggDependencies) =>
new BucketAggType<IBucketAggConfig>(
{

View file

@ -0,0 +1,112 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { functionWrapper } from '../test_helpers';
import { aggGeoHash } from './geo_hash_fn';
describe('agg_expression_functions', () => {
describe('aggGeoHash', () => {
const fn = functionWrapper(aggGeoHash());
test('fills in defaults when only required args are provided', () => {
const actual = fn({
field: 'geo_field',
});
expect(actual).toMatchInlineSnapshot(`
Object {
"type": "agg_type",
"value": Object {
"enabled": true,
"id": undefined,
"params": Object {
"autoPrecision": undefined,
"boundingBox": undefined,
"customLabel": undefined,
"field": "geo_field",
"isFilteredByCollar": undefined,
"json": undefined,
"precision": undefined,
"useGeocentroid": undefined,
},
"schema": undefined,
"type": "geohash_grid",
},
}
`);
});
test('includes optional params when they are provided', () => {
const actual = fn({
field: 'geo_field',
autoPrecision: false,
precision: 10,
useGeocentroid: true,
isFilteredByCollar: false,
boundingBox: JSON.stringify({
top_left: [-74.1, 40.73],
bottom_right: [-71.12, 40.01],
}),
});
expect(actual.value).toMatchInlineSnapshot(`
Object {
"enabled": true,
"id": undefined,
"params": Object {
"autoPrecision": false,
"boundingBox": Object {
"bottom_right": Array [
-71.12,
40.01,
],
"top_left": Array [
-74.1,
40.73,
],
},
"customLabel": undefined,
"field": "geo_field",
"isFilteredByCollar": false,
"json": undefined,
"precision": 10,
"useGeocentroid": true,
},
"schema": undefined,
"type": "geohash_grid",
}
`);
});
test('correctly parses json string argument', () => {
const actual = fn({
field: 'geo_field',
json: '{ "foo": true }',
});
expect(actual.value.params.json).toEqual({ foo: true });
expect(() => {
fn({
field: 'geo_field',
json: '/// intentionally malformed json ///',
});
}).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`);
});
});
});

View file

@ -0,0 +1,129 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { i18n } from '@kbn/i18n';
import { Assign } from '@kbn/utility-types';
import { ExpressionFunctionDefinition } from '../../../../../expressions/public';
import { AggExpressionType, AggExpressionFunctionArgs, BUCKET_TYPES } from '../';
import { getParsedValue } from '../utils/get_parsed_value';
const fnName = 'aggGeoHash';
type Input = any;
type AggArgs = AggExpressionFunctionArgs<typeof BUCKET_TYPES.GEOHASH_GRID>;
type Arguments = Assign<AggArgs, { boundingBox?: string }>;
type Output = AggExpressionType;
type FunctionDefinition = ExpressionFunctionDefinition<typeof fnName, Input, Arguments, Output>;
export const aggGeoHash = (): FunctionDefinition => ({
name: fnName,
help: i18n.translate('data.search.aggs.function.buckets.geoHash.help', {
defaultMessage: 'Generates a serialized agg config for a Geo Hash agg',
}),
type: 'agg_type',
args: {
id: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.geoHash.id.help', {
defaultMessage: 'ID for this aggregation',
}),
},
enabled: {
types: ['boolean'],
default: true,
help: i18n.translate('data.search.aggs.buckets.geoHash.enabled.help', {
defaultMessage: 'Specifies whether this aggregation should be enabled',
}),
},
schema: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.geoHash.schema.help', {
defaultMessage: 'Schema to use for this aggregation',
}),
},
field: {
types: ['string'],
required: true,
help: i18n.translate('data.search.aggs.buckets.geoHash.field.help', {
defaultMessage: 'Field to use for this aggregation',
}),
},
useGeocentroid: {
types: ['boolean'],
help: i18n.translate('data.search.aggs.buckets.geoHash.useGeocentroid.help', {
defaultMessage: 'Specifies whether to use geocentroid for this aggregation',
}),
},
autoPrecision: {
types: ['boolean'],
help: i18n.translate('data.search.aggs.buckets.geoHash.autoPrecision.help', {
defaultMessage: 'Specifies whether to use auto precision for this aggregation',
}),
},
isFilteredByCollar: {
types: ['boolean'],
help: i18n.translate('data.search.aggs.buckets.geoHash.isFilteredByCollar.help', {
defaultMessage: 'Specifies whether to filter by collar',
}),
},
boundingBox: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.geoHash.boundingBox.help', {
defaultMessage: 'Filter results based on a point location within a bounding box',
}),
},
precision: {
types: ['number'],
help: i18n.translate('data.search.aggs.buckets.geoHash.precision.help', {
defaultMessage: 'Precision to use for this aggregation.',
}),
},
json: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.geoHash.json.help', {
defaultMessage: 'Advanced json to include when the agg is sent to Elasticsearch',
}),
},
customLabel: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.geoHash.customLabel.help', {
defaultMessage: 'Represents a custom label for this aggregation',
}),
},
},
fn: (input, args) => {
const { id, enabled, schema, ...rest } = args;
return {
type: 'agg_type',
value: {
id,
enabled,
schema,
type: BUCKET_TYPES.GEOHASH_GRID,
params: {
...rest,
boundingBox: getParsedValue(args, 'boundingBox'),
json: getParsedValue(args, 'json'),
},
},
};
},
});

View file

@ -25,6 +25,7 @@ import { BUCKET_TYPES } from './bucket_agg_types';
import { KBN_FIELD_TYPES } from '../../../../common';
import { METRIC_TYPES } from '../metrics/metric_agg_types';
import { GetInternalStartServicesFn } from '../../../types';
import { BaseAggParams } from '../types';
export interface GeoTitleBucketAggDependencies {
getInternalStartServices: GetInternalStartServicesFn;
@ -34,6 +35,12 @@ const geotileGridTitle = i18n.translate('data.search.aggs.buckets.geotileGridTit
defaultMessage: 'Geotile',
});
export interface AggParamsGeoTile extends BaseAggParams {
field: string;
useGeocentroid?: boolean;
precision?: number;
}
export const getGeoTitleBucketAgg = ({ getInternalStartServices }: GeoTitleBucketAggDependencies) =>
new BucketAggType(
{

View file

@ -0,0 +1,91 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { functionWrapper } from '../test_helpers';
import { aggGeoTile } from './geo_tile_fn';
describe('agg_expression_functions', () => {
describe('aggGeoTile', () => {
const fn = functionWrapper(aggGeoTile());
test('fills in defaults when only required args are provided', () => {
const actual = fn({
field: 'geo_field',
});
expect(actual).toMatchInlineSnapshot(`
Object {
"type": "agg_type",
"value": Object {
"enabled": true,
"id": undefined,
"params": Object {
"customLabel": undefined,
"field": "geo_field",
"json": undefined,
"precision": undefined,
"useGeocentroid": undefined,
},
"schema": undefined,
"type": "geotile_grid",
},
}
`);
});
test('includes optional params when they are provided', () => {
const actual = fn({
field: 'geo_field',
useGeocentroid: false,
precision: 10,
});
expect(actual.value).toMatchInlineSnapshot(`
Object {
"enabled": true,
"id": undefined,
"params": Object {
"customLabel": undefined,
"field": "geo_field",
"json": undefined,
"precision": 10,
"useGeocentroid": false,
},
"schema": undefined,
"type": "geotile_grid",
}
`);
});
test('correctly parses json string argument', () => {
const actual = fn({
field: 'geo_field',
json: '{ "foo": true }',
});
expect(actual.value.params.json).toEqual({ foo: true });
expect(() => {
fn({
field: 'geo_field',
json: '/// intentionally malformed json ///',
});
}).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`);
});
});
});

View file

@ -0,0 +1,108 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { i18n } from '@kbn/i18n';
import { ExpressionFunctionDefinition } from '../../../../../expressions/public';
import { AggExpressionType, AggExpressionFunctionArgs, BUCKET_TYPES } from '../';
import { getParsedValue } from '../utils/get_parsed_value';
const fnName = 'aggGeoTile';
type Input = any;
type AggArgs = AggExpressionFunctionArgs<typeof BUCKET_TYPES.GEOTILE_GRID>;
type Output = AggExpressionType;
type FunctionDefinition = ExpressionFunctionDefinition<typeof fnName, Input, AggArgs, Output>;
export const aggGeoTile = (): FunctionDefinition => ({
name: fnName,
help: i18n.translate('data.search.aggs.function.buckets.geoTile.help', {
defaultMessage: 'Generates a serialized agg config for a Geo Tile agg',
}),
type: 'agg_type',
args: {
id: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.geoTile.id.help', {
defaultMessage: 'ID for this aggregation',
}),
},
enabled: {
types: ['boolean'],
default: true,
help: i18n.translate('data.search.aggs.buckets.geoTile.enabled.help', {
defaultMessage: 'Specifies whether this aggregation should be enabled',
}),
},
schema: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.geoTile.schema.help', {
defaultMessage: 'Schema to use for this aggregation',
}),
},
field: {
types: ['string'],
required: true,
help: i18n.translate('data.search.aggs.buckets.geoTile.field.help', {
defaultMessage: 'Field to use for this aggregation',
}),
},
useGeocentroid: {
types: ['boolean'],
help: i18n.translate('data.search.aggs.buckets.geoTile.useGeocentroid.help', {
defaultMessage: 'Specifies whether to use geocentroid for this aggregation',
}),
},
precision: {
types: ['number'],
help: i18n.translate('data.search.aggs.buckets.geoTile.precision.help', {
defaultMessage: 'Precision to use for this aggregation.',
}),
},
json: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.geoTile.json.help', {
defaultMessage: 'Advanced json to include when the agg is sent to Elasticsearch',
}),
},
customLabel: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.geoTile.customLabel.help', {
defaultMessage: 'Represents a custom label for this aggregation',
}),
},
},
fn: (input, args) => {
const { id, enabled, schema, ...rest } = args;
return {
type: 'agg_type',
value: {
id,
enabled,
schema,
type: BUCKET_TYPES.GEOTILE_GRID,
params: {
...rest,
json: getParsedValue(args, 'json'),
},
},
};
},
});

View file

@ -26,6 +26,8 @@ import { createFilterHistogram } from './create_filter/histogram';
import { BUCKET_TYPES } from './bucket_agg_types';
import { KBN_FIELD_TYPES } from '../../../../common';
import { GetInternalStartServicesFn } from '../../../types';
import { BaseAggParams } from '../types';
import { ExtendedBounds } from './lib/extended_bounds';
export interface AutoBounds {
min: number;
@ -42,6 +44,15 @@ export interface IBucketHistogramAggConfig extends IBucketAggConfig {
getAutoBounds: () => AutoBounds;
}
export interface AggParamsHistogram extends BaseAggParams {
field: string;
interval: string;
intervalBase?: number;
min_doc_count?: boolean;
has_extended_bounds?: boolean;
extended_bounds?: ExtendedBounds;
}
export const getHistogramBucketAgg = ({
uiSettings,
getInternalStartServices,

View file

@ -0,0 +1,109 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { functionWrapper } from '../test_helpers';
import { aggHistogram } from './histogram_fn';
describe('agg_expression_functions', () => {
describe('aggHistogram', () => {
const fn = functionWrapper(aggHistogram());
test('fills in defaults when only required args are provided', () => {
const actual = fn({
field: 'field',
interval: '10',
});
expect(actual).toMatchInlineSnapshot(`
Object {
"type": "agg_type",
"value": Object {
"enabled": true,
"id": undefined,
"params": Object {
"customLabel": undefined,
"extended_bounds": undefined,
"field": "field",
"has_extended_bounds": undefined,
"interval": "10",
"intervalBase": undefined,
"json": undefined,
"min_doc_count": undefined,
},
"schema": undefined,
"type": "histogram",
},
}
`);
});
test('includes optional params when they are provided', () => {
const actual = fn({
field: 'field',
interval: '10',
intervalBase: 1,
min_doc_count: false,
has_extended_bounds: false,
extended_bounds: JSON.stringify({
min: 1,
max: 2,
}),
});
expect(actual.value).toMatchInlineSnapshot(`
Object {
"enabled": true,
"id": undefined,
"params": Object {
"customLabel": undefined,
"extended_bounds": Object {
"max": 2,
"min": 1,
},
"field": "field",
"has_extended_bounds": false,
"interval": "10",
"intervalBase": 1,
"json": undefined,
"min_doc_count": false,
},
"schema": undefined,
"type": "histogram",
}
`);
});
test('correctly parses json string argument', () => {
const actual = fn({
field: 'field',
interval: '10',
json: '{ "foo": true }',
});
expect(actual.value.params.json).toEqual({ foo: true });
expect(() => {
fn({
field: 'field',
interval: '10',
json: '/// intentionally malformed json ///',
});
}).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`);
});
});
});

View file

@ -0,0 +1,132 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { i18n } from '@kbn/i18n';
import { Assign } from '@kbn/utility-types';
import { ExpressionFunctionDefinition } from '../../../../../expressions/public';
import { AggExpressionType, AggExpressionFunctionArgs, BUCKET_TYPES } from '../';
import { getParsedValue } from '../utils/get_parsed_value';
const fnName = 'aggHistogram';
type Input = any;
type AggArgs = AggExpressionFunctionArgs<typeof BUCKET_TYPES.HISTOGRAM>;
type Arguments = Assign<AggArgs, { extended_bounds?: string }>;
type Output = AggExpressionType;
type FunctionDefinition = ExpressionFunctionDefinition<typeof fnName, Input, Arguments, Output>;
export const aggHistogram = (): FunctionDefinition => ({
name: fnName,
help: i18n.translate('data.search.aggs.function.buckets.histogram.help', {
defaultMessage: 'Generates a serialized agg config for a Histogram agg',
}),
type: 'agg_type',
args: {
id: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.histogram.id.help', {
defaultMessage: 'ID for this aggregation',
}),
},
enabled: {
types: ['boolean'],
default: true,
help: i18n.translate('data.search.aggs.buckets.histogram.enabled.help', {
defaultMessage: 'Specifies whether this aggregation should be enabled',
}),
},
schema: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.histogram.schema.help', {
defaultMessage: 'Schema to use for this aggregation',
}),
},
field: {
types: ['string'],
required: true,
help: i18n.translate('data.search.aggs.buckets.histogram.field.help', {
defaultMessage: 'Field to use for this aggregation',
}),
},
interval: {
types: ['string'],
required: true,
help: i18n.translate('data.search.aggs.buckets.histogram.interval.help', {
defaultMessage: 'Interval to use for this aggregation',
}),
},
intervalBase: {
types: ['number'],
help: i18n.translate('data.search.aggs.buckets.histogram.intervalBase.help', {
defaultMessage: 'IntervalBase to use for this aggregation',
}),
},
min_doc_count: {
types: ['boolean'],
help: i18n.translate('data.search.aggs.buckets.histogram.minDocCount.help', {
defaultMessage: 'Specifies whether to use min_doc_count for this aggregation',
}),
},
has_extended_bounds: {
types: ['boolean'],
help: i18n.translate('data.search.aggs.buckets.histogram.hasExtendedBounds.help', {
defaultMessage: 'Specifies whether to use has_extended_bounds for this aggregation',
}),
},
extended_bounds: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.histogram.extendedBounds.help', {
defaultMessage:
'With extended_bounds setting, you now can "force" the histogram aggregation to start building buckets on a specific min value and also keep on building buckets up to a max value ',
}),
},
json: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.histogram.json.help', {
defaultMessage: 'Advanced json to include when the agg is sent to Elasticsearch',
}),
},
customLabel: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.histogram.customLabel.help', {
defaultMessage: 'Represents a custom label for this aggregation',
}),
},
},
fn: (input, args) => {
const { id, enabled, schema, ...rest } = args;
return {
type: 'agg_type',
value: {
id,
enabled,
schema,
type: BUCKET_TYPES.HISTOGRAM,
params: {
...rest,
extended_bounds: getParsedValue(args, 'extended_bounds'),
json: getParsedValue(args, 'json'),
},
},
};
},
});

View file

@ -19,11 +19,18 @@
export * from './_interval_options';
export * from './bucket_agg_types';
export * from './histogram';
export * from './date_histogram';
export * from './date_range';
export * from './range';
export * from './filter';
export * from './filters';
export * from './geo_tile';
export * from './geo_hash';
export * from './ip_range';
export * from './lib/cidr_mask';
export * from './lib/date_range';
export * from './lib/ip_range';
export * from './migrate_include_exclude_format';
export * from './significant_terms';
export * from './terms';

View file

@ -23,18 +23,38 @@ import { BucketAggType } from './bucket_agg_type';
import { BUCKET_TYPES } from './bucket_agg_types';
import { createFilterIpRange } from './create_filter/ip_range';
import { IpRangeKey, convertIPRangeToString } from './lib/ip_range';
import {
convertIPRangeToString,
IpRangeKey,
RangeIpRangeAggKey,
CidrMaskIpRangeAggKey,
} from './lib/ip_range';
import { KBN_FIELD_TYPES, FieldFormat, TEXT_CONTEXT_TYPE } from '../../../../common';
import { GetInternalStartServicesFn } from '../../../types';
import { BaseAggParams } from '../types';
const ipRangeTitle = i18n.translate('data.search.aggs.buckets.ipRangeTitle', {
defaultMessage: 'IPv4 Range',
});
export enum IP_RANGE_TYPES {
FROM_TO = 'fromTo',
MASK = 'mask',
}
export interface IpRangeBucketAggDependencies {
getInternalStartServices: GetInternalStartServicesFn;
}
export interface AggParamsIpRange extends BaseAggParams {
field: string;
ipRangeType?: IP_RANGE_TYPES;
ranges?: Partial<{
[IP_RANGE_TYPES.FROM_TO]: RangeIpRangeAggKey[];
[IP_RANGE_TYPES.MASK]: CidrMaskIpRangeAggKey[];
}>;
}
export const getIpRangeBucketAgg = ({ getInternalStartServices }: IpRangeBucketAggDependencies) =>
new BucketAggType(
{
@ -42,7 +62,7 @@ export const getIpRangeBucketAgg = ({ getInternalStartServices }: IpRangeBucketA
title: ipRangeTitle,
createFilter: createFilterIpRange,
getKey(bucket, key, agg): IpRangeKey {
if (agg.params.ipRangeType === 'mask') {
if (agg.params.ipRangeType === IP_RANGE_TYPES.MASK) {
return { type: 'mask', mask: key };
}
return { type: 'range', from: bucket.from, to: bucket.to };
@ -74,7 +94,7 @@ export const getIpRangeBucketAgg = ({ getInternalStartServices }: IpRangeBucketA
},
{
name: 'ipRangeType',
default: 'fromTo',
default: IP_RANGE_TYPES.FROM_TO,
write: noop,
},
{
@ -90,7 +110,7 @@ export const getIpRangeBucketAgg = ({ getInternalStartServices }: IpRangeBucketA
const ipRangeType = aggConfig.params.ipRangeType;
let ranges = aggConfig.params.ranges[ipRangeType];
if (ipRangeType === 'fromTo') {
if (ipRangeType === IP_RANGE_TYPES.FROM_TO) {
ranges = map(ranges, (range: any) => omit(range, isNull));
}

View file

@ -0,0 +1,102 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { functionWrapper } from '../test_helpers';
import { IP_RANGE_TYPES } from './ip_range';
import { aggIpRange } from './ip_range_fn';
describe('agg_expression_functions', () => {
describe('aggIpRange', () => {
const fn = functionWrapper(aggIpRange());
test('fills in defaults when only required args are provided', () => {
const actual = fn({
field: 'ip_field',
});
expect(actual).toMatchInlineSnapshot(`
Object {
"type": "agg_type",
"value": Object {
"enabled": true,
"id": undefined,
"params": Object {
"customLabel": undefined,
"field": "ip_field",
"ipRangeType": undefined,
"json": undefined,
"ranges": undefined,
},
"schema": undefined,
"type": "ip_range",
},
}
`);
});
test('includes optional params when they are provided', () => {
const actual = fn({
field: 'ip_field',
ipRangeType: IP_RANGE_TYPES.MASK,
ranges: JSON.stringify({
mask: [{ mask: '10.0.0.0/25' }],
}),
});
expect(actual.value).toMatchInlineSnapshot(`
Object {
"enabled": true,
"id": undefined,
"params": Object {
"customLabel": undefined,
"field": "ip_field",
"ipRangeType": "mask",
"json": undefined,
"ranges": Object {
"mask": Array [
Object {
"mask": "10.0.0.0/25",
},
],
},
},
"schema": undefined,
"type": "ip_range",
}
`);
});
test('correctly parses json string argument', () => {
const actual = fn({
field: 'ip_field',
ipRangeType: IP_RANGE_TYPES.MASK,
json: '{ "foo": true }',
});
expect(actual.value.params.json).toEqual({ foo: true });
expect(() => {
fn({
field: 'ip_field',
ipRangeType: IP_RANGE_TYPES.FROM_TO,
json: '/// intentionally malformed json ///',
});
}).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`);
});
});
});

View file

@ -0,0 +1,114 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { i18n } from '@kbn/i18n';
import { Assign } from '@kbn/utility-types';
import { ExpressionFunctionDefinition } from '../../../../../expressions/public';
import { AggExpressionType, AggExpressionFunctionArgs, BUCKET_TYPES } from '../';
import { getParsedValue } from '../utils/get_parsed_value';
const fnName = 'aggIpRange';
type Input = any;
type AggArgs = AggExpressionFunctionArgs<typeof BUCKET_TYPES.IP_RANGE>;
type Arguments = Assign<AggArgs, { ranges?: string; ipRangeType?: string }>;
type Output = AggExpressionType;
type FunctionDefinition = ExpressionFunctionDefinition<typeof fnName, Input, Arguments, Output>;
export const aggIpRange = (): FunctionDefinition => ({
name: fnName,
help: i18n.translate('data.search.aggs.function.buckets.ipRange.help', {
defaultMessage: 'Generates a serialized agg config for a Ip Range agg',
}),
type: 'agg_type',
args: {
id: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.ipRange.id.help', {
defaultMessage: 'ID for this aggregation',
}),
},
enabled: {
types: ['boolean'],
default: true,
help: i18n.translate('data.search.aggs.buckets.ipRange.enabled.help', {
defaultMessage: 'Specifies whether this aggregation should be enabled',
}),
},
schema: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.ipRange.schema.help', {
defaultMessage: 'Schema to use for this aggregation',
}),
},
field: {
types: ['string'],
required: true,
help: i18n.translate('data.search.aggs.buckets.ipRange.field.help', {
defaultMessage: 'Field to use for this aggregation',
}),
},
ipRangeType: {
types: ['string'],
options: ['mask', 'fromTo'],
help: i18n.translate('data.search.aggs.buckets.ipRange.ipRangeType.help', {
defaultMessage:
'IP range type to use for this aggregation. Takes one of the following values: mask, fromTo.',
}),
},
ranges: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.ipRange.ranges.help', {
defaultMessage: 'Serialized ranges to use for this aggregation.',
}),
},
json: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.ipRange.json.help', {
defaultMessage: 'Advanced json to include when the agg is sent to Elasticsearch',
}),
},
customLabel: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.ipRange.customLabel.help', {
defaultMessage: 'Represents a custom label for this aggregation',
}),
},
},
fn: (input, args) => {
const { id, enabled, schema, ...rest } = args;
return {
type: 'agg_type',
value: {
id,
enabled,
schema,
type: BUCKET_TYPES.IP_RANGE,
params: {
...rest,
json: getParsedValue(args, 'json'),
ranges: getParsedValue(args, 'ranges'),
},
},
};
},
});

View file

@ -18,8 +18,8 @@
*/
export interface DateRangeKey {
from: number;
to: number;
from: number | string;
to: number | string;
}
export function convertDateRangeToString({ from, to }: DateRangeKey, format: (val: any) => string) {

View file

@ -0,0 +1,23 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export interface ExtendedBounds {
min: number;
max: number;
}

View file

@ -0,0 +1,86 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
type GeoPoint =
| {
lat: number;
lon: number;
}
| string
| [number, number];
interface GeoBox {
top: number;
left: number;
bottom: number;
right: number;
}
/** GeoBoundingBox Accepted Formats:
* Lat Lon As Properties:
* "top_left" : {
* "lat" : 40.73, "lon" : -74.1
* },
* "bottom_right" : {
* "lat" : 40.01, "lon" : -71.12
* }
*
* Lat Lon As Array:
* {
* "top_left" : [-74.1, 40.73],
* "bottom_right" : [-71.12, 40.01]
* }
*
* Lat Lon As String:
* {
* "top_left" : "40.73, -74.1",
* "bottom_right" : "40.01, -71.12"
* }
*
* Bounding Box as Well-Known Text (WKT):
* {
* "wkt" : "BBOX (-74.1, -71.12, 40.73, 40.01)"
* }
*
* Geohash:
* {
* "top_right" : "dr5r9ydj2y73",
* "bottom_left" : "drj7teegpus6"
* }
*
* Vertices:
* {
* "top" : 40.73,
* "left" : -74.1,
* "bottom" : 40.01,
* "right" : -71.12
* }
*
* **/
export type GeoBoundingBox =
| Partial<{
top_left: GeoPoint;
top_right: GeoPoint;
bottom_right: GeoPoint;
bottom_left: GeoPoint;
}>
| {
wkt: string;
}
| GeoBox;

View file

@ -17,9 +17,18 @@
* under the License.
*/
export type IpRangeKey =
| { type: 'mask'; mask: string }
| { type: 'range'; from: string; to: string };
export interface CidrMaskIpRangeAggKey {
type: 'mask';
mask: string;
}
export interface RangeIpRangeAggKey {
type: 'range';
from: string;
to: string;
}
export type IpRangeKey = CidrMaskIpRangeAggKey | RangeIpRangeAggKey;
export const convertIPRangeToString = (range: IpRangeKey, format: (val: any) => string) => {
if (range.type === 'mask') {

View file

@ -24,6 +24,7 @@ import { RangeKey } from './range_key';
import { createFilterRange } from './create_filter/range';
import { BUCKET_TYPES } from './bucket_agg_types';
import { GetInternalStartServicesFn } from '../../../types';
import { BaseAggParams } from '../types';
const keyCaches = new WeakMap();
const formats = new WeakMap();
@ -36,6 +37,14 @@ export interface RangeBucketAggDependencies {
getInternalStartServices: GetInternalStartServicesFn;
}
export interface AggParamsRange extends BaseAggParams {
field: string;
ranges?: Array<{
from: number;
to: number;
}>;
}
export const getRangeBucketAgg = ({ getInternalStartServices }: RangeBucketAggDependencies) =>
new BucketAggType(
{

View file

@ -0,0 +1,100 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { functionWrapper } from '../test_helpers';
import { aggRange } from './range_fn';
describe('agg_expression_functions', () => {
describe('aggRange', () => {
const fn = functionWrapper(aggRange());
test('fills in defaults when only required args are provided', () => {
const actual = fn({
field: 'number_field',
});
expect(actual).toMatchInlineSnapshot(`
Object {
"type": "agg_type",
"value": Object {
"enabled": true,
"id": undefined,
"params": Object {
"customLabel": undefined,
"field": "number_field",
"json": undefined,
"ranges": undefined,
},
"schema": undefined,
"type": "range",
},
}
`);
});
test('includes optional params when they are provided', () => {
const actual = fn({
field: 'number_field',
ranges: JSON.stringify([
{ from: 1, to: 2 },
{ from: 5, to: 100 },
]),
});
expect(actual.value).toMatchInlineSnapshot(`
Object {
"enabled": true,
"id": undefined,
"params": Object {
"customLabel": undefined,
"field": "number_field",
"json": undefined,
"ranges": Array [
Object {
"from": 1,
"to": 2,
},
Object {
"from": 5,
"to": 100,
},
],
},
"schema": undefined,
"type": "range",
}
`);
});
test('correctly parses json string argument', () => {
const actual = fn({
field: 'number_field',
json: '{ "foo": true }',
});
expect(actual.value.params.json).toEqual({ foo: true });
expect(() => {
fn({
field: 'number_field',
json: '/// intentionally malformed json ///',
});
}).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`);
});
});
});

View file

@ -0,0 +1,106 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { i18n } from '@kbn/i18n';
import { Assign } from '@kbn/utility-types';
import { ExpressionFunctionDefinition } from '../../../../../expressions/public';
import { AggExpressionType, AggExpressionFunctionArgs, BUCKET_TYPES } from '../';
import { getParsedValue } from '../utils/get_parsed_value';
const fnName = 'aggRange';
type Input = any;
type AggArgs = AggExpressionFunctionArgs<typeof BUCKET_TYPES.RANGE>;
type Arguments = Assign<AggArgs, { ranges?: string }>;
type Output = AggExpressionType;
type FunctionDefinition = ExpressionFunctionDefinition<typeof fnName, Input, Arguments, Output>;
export const aggRange = (): FunctionDefinition => ({
name: fnName,
help: i18n.translate('data.search.aggs.function.buckets.range.help', {
defaultMessage: 'Generates a serialized agg config for a Range agg',
}),
type: 'agg_type',
args: {
id: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.range.id.help', {
defaultMessage: 'ID for this aggregation',
}),
},
enabled: {
types: ['boolean'],
default: true,
help: i18n.translate('data.search.aggs.buckets.range.enabled.help', {
defaultMessage: 'Specifies whether this aggregation should be enabled',
}),
},
schema: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.range.schema.help', {
defaultMessage: 'Schema to use for this aggregation',
}),
},
field: {
types: ['string'],
required: true,
help: i18n.translate('data.search.aggs.buckets.range.field.help', {
defaultMessage: 'Field to use for this aggregation',
}),
},
ranges: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.range.ranges.help', {
defaultMessage: 'Serialized ranges to use for this aggregation.',
}),
},
json: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.range.json.help', {
defaultMessage: 'Advanced json to include when the agg is sent to Elasticsearch',
}),
},
customLabel: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.range.customLabel.help', {
defaultMessage: 'Represents a custom label for this aggregation',
}),
},
},
fn: (input, args) => {
const { id, enabled, schema, ...rest } = args;
return {
type: 'agg_type',
value: {
id,
enabled,
schema,
type: BUCKET_TYPES.RANGE,
params: {
...rest,
json: getParsedValue(args, 'json'),
ranges: getParsedValue(args, 'ranges'),
},
},
};
},
});

View file

@ -24,6 +24,7 @@ import { isStringType, migrateIncludeExcludeFormat } from './migrate_include_exc
import { BUCKET_TYPES } from './bucket_agg_types';
import { KBN_FIELD_TYPES } from '../../../../common';
import { GetInternalStartServicesFn } from '../../../types';
import { BaseAggParams } from '../types';
const significantTermsTitle = i18n.translate('data.search.aggs.buckets.significantTermsTitle', {
defaultMessage: 'Significant Terms',
@ -33,6 +34,13 @@ export interface SignificantTermsBucketAggDependencies {
getInternalStartServices: GetInternalStartServicesFn;
}
export interface AggParamsSignificantTerms extends BaseAggParams {
field: string;
size?: number;
exclude?: string;
include?: string;
}
export const getSignificantTermsBucketAgg = ({
getInternalStartServices,
}: SignificantTermsBucketAggDependencies) =>

View file

@ -0,0 +1,96 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { functionWrapper } from '../test_helpers';
import { aggSignificantTerms } from './significant_terms_fn';
describe('agg_expression_functions', () => {
describe('aggSignificantTerms', () => {
const fn = functionWrapper(aggSignificantTerms());
test('fills in defaults when only required args are provided', () => {
const actual = fn({
field: 'machine.os.keyword',
});
expect(actual).toMatchInlineSnapshot(`
Object {
"type": "agg_type",
"value": Object {
"enabled": true,
"id": undefined,
"params": Object {
"customLabel": undefined,
"exclude": undefined,
"field": "machine.os.keyword",
"include": undefined,
"json": undefined,
"size": undefined,
},
"schema": undefined,
"type": "significant_terms",
},
}
`);
});
test('includes optional params when they are provided', () => {
const actual = fn({
id: '1',
enabled: false,
schema: 'whatever',
field: 'machine.os.keyword',
size: 6,
include: 'win',
exclude: 'ios',
});
expect(actual.value).toMatchInlineSnapshot(`
Object {
"enabled": false,
"id": "1",
"params": Object {
"customLabel": undefined,
"exclude": "ios",
"field": "machine.os.keyword",
"include": "win",
"json": undefined,
"size": 6,
},
"schema": "whatever",
"type": "significant_terms",
}
`);
});
test('correctly parses json string argument', () => {
const actual = fn({
field: 'machine.os.keyword',
json: '{ "foo": true }',
});
expect(actual.value.params.json).toEqual({ foo: true });
expect(() => {
fn({
field: 'machine.os.keyword',
json: '/// intentionally malformed json ///',
});
}).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`);
});
});
});

View file

@ -0,0 +1,116 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { i18n } from '@kbn/i18n';
import { ExpressionFunctionDefinition } from '../../../../../expressions/public';
import { AggExpressionType, AggExpressionFunctionArgs, BUCKET_TYPES } from '../';
import { getParsedValue } from '../utils/get_parsed_value';
const fnName = 'aggSignificantTerms';
type Input = any;
type AggArgs = AggExpressionFunctionArgs<typeof BUCKET_TYPES.SIGNIFICANT_TERMS>;
type Arguments = AggArgs;
type Output = AggExpressionType;
type FunctionDefinition = ExpressionFunctionDefinition<typeof fnName, Input, Arguments, Output>;
export const aggSignificantTerms = (): FunctionDefinition => ({
name: fnName,
help: i18n.translate('data.search.aggs.function.buckets.significantTerms.help', {
defaultMessage: 'Generates a serialized agg config for a Significant Terms agg',
}),
type: 'agg_type',
args: {
id: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.significantTerms.id.help', {
defaultMessage: 'ID for this aggregation',
}),
},
enabled: {
types: ['boolean'],
default: true,
help: i18n.translate('data.search.aggs.buckets.significantTerms.enabled.help', {
defaultMessage: 'Specifies whether this aggregation should be enabled',
}),
},
schema: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.significantTerms.schema.help', {
defaultMessage: 'Schema to use for this aggregation',
}),
},
field: {
types: ['string'],
required: true,
help: i18n.translate('data.search.aggs.buckets.significantTerms.field.help', {
defaultMessage: 'Field to use for this aggregation',
}),
},
size: {
types: ['number'],
help: i18n.translate('data.search.aggs.buckets.significantTerms.size.help', {
defaultMessage: 'Max number of buckets to retrieve',
}),
},
exclude: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.significantTerms.exclude.help', {
defaultMessage: 'Specific bucket values to exclude from results',
}),
},
include: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.significantTerms.include.help', {
defaultMessage: 'Specific bucket values to include in results',
}),
},
json: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.significantTerms.json.help', {
defaultMessage: 'Advanced json to include when the agg is sent to Elasticsearch',
}),
},
customLabel: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.significantTerms.customLabel.help', {
defaultMessage: 'Represents a custom label for this aggregation',
}),
},
},
fn: (input, args) => {
const { id, enabled, schema, ...rest } = args;
return {
type: 'agg_type',
value: {
id,
enabled,
schema,
type: BUCKET_TYPES.SIGNIFICANT_TERMS,
params: {
...rest,
json: getParsedValue(args, 'json'),
},
},
};
},
});

View file

@ -26,7 +26,7 @@ import {
isStringOrNumberType,
migrateIncludeExcludeFormat,
} from './migrate_include_exclude_format';
import { AggConfigSerialized, IAggConfigs } from '../types';
import { AggConfigSerialized, BaseAggParams, IAggConfigs } from '../types';
import { Adapters } from '../../../../../inspector/public';
import { ISearchSource } from '../../search_source';
@ -63,11 +63,11 @@ export interface TermsBucketAggDependencies {
getInternalStartServices: GetInternalStartServicesFn;
}
export interface AggParamsTerms {
export interface AggParamsTerms extends BaseAggParams {
field: string;
order: 'asc' | 'desc';
orderBy: string;
orderAgg?: AggConfigSerialized;
order?: 'asc' | 'desc';
size?: number;
missingBucket?: boolean;
missingBucketLabel?: string;
@ -76,7 +76,6 @@ export interface AggParamsTerms {
// advanced
exclude?: string;
include?: string;
json?: string;
}
export const getTermsBucketAgg = ({ getInternalStartServices }: TermsBucketAggDependencies) =>

View file

@ -27,7 +27,6 @@ describe('agg_expression_functions', () => {
test('fills in defaults when only required args are provided', () => {
const actual = fn({
field: 'machine.os.keyword',
order: 'asc',
orderBy: '1',
});
expect(actual).toMatchInlineSnapshot(`
@ -37,18 +36,19 @@ describe('agg_expression_functions', () => {
"enabled": true,
"id": undefined,
"params": Object {
"customLabel": undefined,
"exclude": undefined,
"field": "machine.os.keyword",
"include": undefined,
"json": undefined,
"missingBucket": false,
"missingBucketLabel": "Missing",
"order": "asc",
"missingBucket": undefined,
"missingBucketLabel": undefined,
"order": undefined,
"orderAgg": undefined,
"orderBy": "1",
"otherBucket": false,
"otherBucketLabel": "Other",
"size": 5,
"otherBucket": undefined,
"otherBucketLabel": undefined,
"size": undefined,
},
"schema": undefined,
"type": "terms",
@ -70,6 +70,7 @@ describe('agg_expression_functions', () => {
missingBucketLabel: 'missing',
otherBucket: true,
otherBucketLabel: 'other',
include: 'win',
exclude: 'ios',
});
@ -78,9 +79,10 @@ describe('agg_expression_functions', () => {
"enabled": false,
"id": "1",
"params": Object {
"customLabel": undefined,
"exclude": "ios",
"field": "machine.os.keyword",
"include": undefined,
"include": "win",
"json": undefined,
"missingBucket": true,
"missingBucketLabel": "missing",
@ -107,37 +109,39 @@ describe('agg_expression_functions', () => {
expect(actual.value.params).toMatchInlineSnapshot(`
Object {
"customLabel": undefined,
"exclude": undefined,
"field": "machine.os.keyword",
"include": undefined,
"json": undefined,
"missingBucket": false,
"missingBucketLabel": "Missing",
"missingBucket": undefined,
"missingBucketLabel": undefined,
"order": "asc",
"orderAgg": Object {
"enabled": true,
"id": undefined,
"params": Object {
"customLabel": undefined,
"exclude": undefined,
"field": "name",
"include": undefined,
"json": undefined,
"missingBucket": false,
"missingBucketLabel": "Missing",
"missingBucket": undefined,
"missingBucketLabel": undefined,
"order": "asc",
"orderAgg": undefined,
"orderBy": "1",
"otherBucket": false,
"otherBucketLabel": "Other",
"size": 5,
"otherBucket": undefined,
"otherBucketLabel": undefined,
"size": undefined,
},
"schema": undefined,
"type": "terms",
},
"orderBy": "1",
"otherBucket": false,
"otherBucketLabel": "Other",
"size": 5,
"otherBucket": undefined,
"otherBucketLabel": undefined,
"size": undefined,
}
`);
});

View file

@ -20,27 +20,25 @@
import { i18n } from '@kbn/i18n';
import { Assign } from '@kbn/utility-types';
import { ExpressionFunctionDefinition } from '../../../../../expressions/public';
import { AggExpressionType, AggExpressionFunctionArgs } from '../';
import { AggExpressionType, AggExpressionFunctionArgs, BUCKET_TYPES } from '../';
import { getParsedValue } from '../utils/get_parsed_value';
const aggName = 'terms';
const fnName = 'aggTerms';
type Input = any;
type AggArgs = AggExpressionFunctionArgs<typeof aggName>;
type AggArgs = AggExpressionFunctionArgs<typeof BUCKET_TYPES.TERMS>;
// Since the orderAgg param is an agg nested in a subexpression, we need to
// overwrite the param type to expect a value of type AggExpressionType.
type Arguments = AggArgs &
Assign<
AggArgs,
{ orderAgg?: AggArgs['orderAgg'] extends undefined ? undefined : AggExpressionType }
>;
type Arguments = Assign<AggArgs, { orderAgg?: AggExpressionType }>;
type Output = AggExpressionType;
type FunctionDefinition = ExpressionFunctionDefinition<typeof fnName, Input, Arguments, Output>;
export const aggTerms = (): FunctionDefinition => ({
name: fnName,
help: i18n.translate('data.search.aggs.function.buckets.terms.help', {
defaultMessage: 'Generates a serialized agg config for a terms agg',
defaultMessage: 'Generates a serialized agg config for a Terms agg',
}),
type: 'agg_type',
args: {
@ -72,7 +70,7 @@ export const aggTerms = (): FunctionDefinition => ({
},
order: {
types: ['string'],
required: true,
options: ['asc', 'desc'],
help: i18n.translate('data.search.aggs.buckets.terms.order.help', {
defaultMessage: 'Order in which to return the results: asc or desc',
}),
@ -91,41 +89,30 @@ export const aggTerms = (): FunctionDefinition => ({
},
size: {
types: ['number'],
default: 5,
help: i18n.translate('data.search.aggs.buckets.terms.size.help', {
defaultMessage: 'Max number of buckets to retrieve',
}),
},
missingBucket: {
types: ['boolean'],
default: false,
help: i18n.translate('data.search.aggs.buckets.terms.missingBucket.help', {
defaultMessage: 'When set to true, groups together any buckets with missing fields',
}),
},
missingBucketLabel: {
types: ['string'],
default: i18n.translate('data.search.aggs.buckets.terms.missingBucketLabel', {
defaultMessage: 'Missing',
description: `Default label used in charts when documents are missing a field.
Visible when you create a chart with a terms aggregation and enable "Show missing values"`,
}),
help: i18n.translate('data.search.aggs.buckets.terms.missingBucketLabel.help', {
defaultMessage: 'Default label used in charts when documents are missing a field.',
}),
},
otherBucket: {
types: ['boolean'],
default: false,
help: i18n.translate('data.search.aggs.buckets.terms.otherBucket.help', {
defaultMessage: 'When set to true, groups together any buckets beyond the allowed size',
}),
},
otherBucketLabel: {
types: ['string'],
default: i18n.translate('data.search.aggs.buckets.terms.otherBucketLabel', {
defaultMessage: 'Other',
}),
help: i18n.translate('data.search.aggs.buckets.terms.otherBucketLabel.help', {
defaultMessage: 'Default label used in charts for documents in the Other bucket',
}),
@ -148,32 +135,27 @@ export const aggTerms = (): FunctionDefinition => ({
defaultMessage: 'Advanced json to include when the agg is sent to Elasticsearch',
}),
},
customLabel: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.terms.customLabel.help', {
defaultMessage: 'Represents a custom label for this aggregation',
}),
},
},
fn: (input, args) => {
const { id, enabled, schema, ...rest } = args;
let json;
try {
json = args.json ? JSON.parse(args.json) : undefined;
} catch (e) {
throw new Error('Unable to parse json argument string');
}
// Need to spread this object to work around TS bug:
// https://github.com/microsoft/TypeScript/issues/15300#issuecomment-436793742
const orderAgg = args.orderAgg?.value ? { ...args.orderAgg.value } : undefined;
return {
type: 'agg_type',
value: {
id,
enabled,
schema,
type: aggName,
type: BUCKET_TYPES.TERMS,
params: {
...rest,
orderAgg,
json,
orderAgg: args.orderAgg?.value,
json: getParsedValue(args, 'json'),
},
},
};

View file

@ -21,11 +21,22 @@ import { IndexPattern } from '../../index_patterns';
import {
AggConfigSerialized,
AggConfigs,
AggParamsRange,
AggParamsIpRange,
AggParamsDateRange,
AggParamsFilter,
AggParamsFilters,
AggParamsSignificantTerms,
AggParamsGeoTile,
AggParamsGeoHash,
AggParamsTerms,
AggParamsHistogram,
AggParamsDateHistogram,
AggTypesRegistrySetup,
AggTypesRegistryStart,
CreateAggConfigParams,
getCalculateAutoTimeExpression,
BUCKET_TYPES,
} from './';
export { IAggConfig, AggConfigSerialized } from './agg_config';
@ -55,6 +66,12 @@ export interface SearchAggsStart {
types: AggTypesRegistryStart;
}
/** @internal */
export interface BaseAggParams {
json?: string;
customLabel?: string;
}
/** @internal */
export interface AggExpressionType {
type: 'agg_type';
@ -74,5 +91,15 @@ export type AggExpressionFunctionArgs<
* @internal
*/
export interface AggParamsMapping {
terms: AggParamsTerms;
[BUCKET_TYPES.RANGE]: AggParamsRange;
[BUCKET_TYPES.IP_RANGE]: AggParamsIpRange;
[BUCKET_TYPES.DATE_RANGE]: AggParamsDateRange;
[BUCKET_TYPES.FILTER]: AggParamsFilter;
[BUCKET_TYPES.FILTERS]: AggParamsFilters;
[BUCKET_TYPES.SIGNIFICANT_TERMS]: AggParamsSignificantTerms;
[BUCKET_TYPES.GEOTILE_GRID]: AggParamsGeoTile;
[BUCKET_TYPES.GEOHASH_GRID]: AggParamsGeoHash;
[BUCKET_TYPES.HISTOGRAM]: AggParamsHistogram;
[BUCKET_TYPES.DATE_HISTOGRAM]: AggParamsDateHistogram;
[BUCKET_TYPES.TERMS]: AggParamsTerms;
}

View file

@ -0,0 +1,34 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* This method parses a JSON string and constructs the Object or object described by the string.
* If the given string is not valid JSON, you will get a syntax error.
* @param data { Object } - an object that contains the required for parsing field
* @param key { string} - name of the field to be parsed
*
* @internal
*/
export const getParsedValue = (data: any, key: string) => {
try {
return data[key] ? JSON.parse(data[key]) : undefined;
} catch (e) {
throw new Error(`Unable to parse ${key} argument string`);
}
};