[data.search.aggs]: Create agg types function for terms agg. (#63541) (#64342)

This commit is contained in:
Luke Elmers 2020-04-23 21:49:51 -06:00 committed by GitHub
parent 8bdee234c5
commit 0f96249483
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 810 additions and 152 deletions

View file

@ -1,11 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [AggConfigOptions](./kibana-plugin-plugins-data-public.aggconfigoptions.md) &gt; [enabled](./kibana-plugin-plugins-data-public.aggconfigoptions.enabled.md)
## AggConfigOptions.enabled property
<b>Signature:</b>
```typescript
enabled?: boolean;
```

View file

@ -1,11 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [AggConfigOptions](./kibana-plugin-plugins-data-public.aggconfigoptions.md) &gt; [id](./kibana-plugin-plugins-data-public.aggconfigoptions.id.md)
## AggConfigOptions.id property
<b>Signature:</b>
```typescript
id?: string;
```

View file

@ -2,21 +2,12 @@
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [AggConfigOptions](./kibana-plugin-plugins-data-public.aggconfigoptions.md)
## AggConfigOptions interface
## AggConfigOptions type
<b>Signature:</b>
```typescript
export interface AggConfigOptions
export declare type AggConfigOptions = Assign<AggConfigSerialized, {
type: IAggType;
}>;
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [enabled](./kibana-plugin-plugins-data-public.aggconfigoptions.enabled.md) | <code>boolean</code> | |
| [id](./kibana-plugin-plugins-data-public.aggconfigoptions.id.md) | <code>string</code> | |
| [params](./kibana-plugin-plugins-data-public.aggconfigoptions.params.md) | <code>Record&lt;string, any&gt;</code> | |
| [schema](./kibana-plugin-plugins-data-public.aggconfigoptions.schema.md) | <code>string</code> | |
| [type](./kibana-plugin-plugins-data-public.aggconfigoptions.type.md) | <code>IAggType</code> | |

View file

@ -1,11 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [AggConfigOptions](./kibana-plugin-plugins-data-public.aggconfigoptions.md) &gt; [params](./kibana-plugin-plugins-data-public.aggconfigoptions.params.md)
## AggConfigOptions.params property
<b>Signature:</b>
```typescript
params?: Record<string, any>;
```

View file

@ -1,11 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [AggConfigOptions](./kibana-plugin-plugins-data-public.aggconfigoptions.md) &gt; [schema](./kibana-plugin-plugins-data-public.aggconfigoptions.schema.md)
## AggConfigOptions.schema property
<b>Signature:</b>
```typescript
schema?: string;
```

View file

@ -1,11 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [AggConfigOptions](./kibana-plugin-plugins-data-public.aggconfigoptions.md) &gt; [type](./kibana-plugin-plugins-data-public.aggconfigoptions.type.md)
## AggConfigOptions.type property
<b>Signature:</b>
```typescript
type: IAggType;
```

View file

@ -7,5 +7,5 @@
<b>Signature:</b>
```typescript
makeAgg: (agg: TAggConfig, state?: any) => TAggConfig;
makeAgg: (agg: TAggConfig, state?: AggConfigSerialized) => TAggConfig;
```

View file

@ -21,5 +21,5 @@ export declare class AggParamType<TAggConfig extends IAggConfig = IAggConfig> ex
| Property | Modifiers | Type | Description |
| --- | --- | --- | --- |
| [allowedAggs](./kibana-plugin-plugins-data-public.aggparamtype.allowedaggs.md) | | <code>string[]</code> | |
| [makeAgg](./kibana-plugin-plugins-data-public.aggparamtype.makeagg.md) | | <code>(agg: TAggConfig, state?: any) =&gt; TAggConfig</code> | |
| [makeAgg](./kibana-plugin-plugins-data-public.aggparamtype.makeagg.md) | | <code>(agg: TAggConfig, state?: AggConfigSerialized) =&gt; TAggConfig</code> | |

View file

@ -49,7 +49,6 @@
| Interface | Description |
| --- | --- |
| [AggConfigOptions](./kibana-plugin-plugins-data-public.aggconfigoptions.md) | |
| [AggParamOption](./kibana-plugin-plugins-data-public.aggparamoption.md) | |
| [DataPublicPluginSetup](./kibana-plugin-plugins-data-public.datapublicpluginsetup.md) | |
| [DataPublicPluginStart](./kibana-plugin-plugins-data-public.datapublicpluginstart.md) | |
@ -118,6 +117,7 @@
| Type Alias | Description |
| --- | --- |
| [AggConfigOptions](./kibana-plugin-plugins-data-public.aggconfigoptions.md) | |
| [AggParam](./kibana-plugin-plugins-data-public.aggparam.md) | |
| [CustomFilter](./kibana-plugin-plugins-data-public.customfilter.md) | |
| [EsQuerySortValue](./kibana-plugin-plugins-data-public.esquerysortvalue.md) | |

View file

@ -139,6 +139,7 @@ export class DataPublicPlugin implements Plugin<DataPublicPluginSetup, DataPubli
return {
autocomplete: this.autocomplete.setup(core),
search: this.searchService.setup(core, {
expressions,
getInternalStartServices,
packageInfo: this.packageInfo,
query: queryService,

View file

@ -20,6 +20,7 @@ import { EuiConfirmModalProps } from '@elastic/eui';
import { EuiFieldText } from '@elastic/eui';
import { EuiGlobalToastListToast } from '@elastic/eui';
import { ExclusiveUnion } from '@elastic/eui';
import { ExpressionAstFunction } from 'src/plugins/expressions/public';
import { ExpressionsSetup } from 'src/plugins/expressions/public';
import { History } from 'history';
import { HttpSetup } from 'src/core/public';
@ -59,21 +60,13 @@ import { Unit } from '@elastic/datemath';
import { UnregisterCallback } from 'history';
import { UserProvidedValues } from 'src/core/server/types';
// Warning: (ae-forgotten-export) The symbol "AggConfigSerialized" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "AggConfigOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export interface AggConfigOptions {
// (undocumented)
enabled?: boolean;
// (undocumented)
id?: string;
// (undocumented)
params?: Record<string, any>;
// (undocumented)
schema?: string;
// (undocumented)
export type AggConfigOptions = Assign<AggConfigSerialized, {
type: IAggType;
}
}>;
// Warning: (ae-missing-release-tag) "AggGroupNames" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
@ -112,7 +105,7 @@ export class AggParamType<TAggConfig extends IAggConfig = IAggConfig> extends Ba
// (undocumented)
allowedAggs: string[];
// (undocumented)
makeAgg: (agg: TAggConfig, state?: any) => TAggConfig;
makeAgg: (agg: TAggConfig, state?: AggConfigSerialized) => TAggConfig;
}
// Warning: (ae-missing-release-tag) "AggTypeFieldFilters" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)

View file

@ -24,18 +24,21 @@ import { AggConfigs, CreateAggConfigParams } from './agg_configs';
import { AggType } from './agg_type';
import { AggTypesRegistryStart } from './agg_types_registry';
import { mockDataServices, mockAggTypesRegistry } from './test_helpers';
import { MetricAggType } from './metrics/metric_agg_type';
import { Field as IndexPatternField, IndexPattern } from '../../index_patterns';
import { stubIndexPatternWithFields } from '../../../public/stubs';
import { FieldFormatsStart } from '../../field_formats';
import { fieldFormatsServiceMock } from '../../field_formats/mocks';
describe('AggConfig', () => {
let indexPattern: IndexPattern;
let typesRegistry: AggTypesRegistryStart;
const fieldFormats = fieldFormatsServiceMock.createStartContract();
let fieldFormats: FieldFormatsStart;
beforeEach(() => {
jest.restoreAllMocks();
mockDataServices();
fieldFormats = fieldFormatsServiceMock.createStartContract();
indexPattern = stubIndexPatternWithFields as IndexPattern;
typesRegistry = mockAggTypesRegistry();
});
@ -325,7 +328,7 @@ describe('AggConfig', () => {
});
});
describe('#toJSON', () => {
describe('#serialize', () => {
it('includes the aggs id, params, type and schema', () => {
const ac = new AggConfigs(indexPattern, [], { typesRegistry, fieldFormats });
const configStates = {
@ -342,7 +345,7 @@ describe('AggConfig', () => {
expect(aggConfig.type).toHaveProperty('name', 'date_histogram');
expect(typeof aggConfig.schema).toBe('string');
const state = aggConfig.toJSON();
const state = aggConfig.serialize();
expect(state).toHaveProperty('id', '1');
expect(typeof state.params).toBe('object');
expect(state).toHaveProperty('type', 'date_histogram');
@ -367,6 +370,201 @@ describe('AggConfig', () => {
});
});
describe('#toExpressionAst', () => {
beforeEach(() => {
fieldFormats.getDefaultInstance = (() => ({
getConverterFor: (t?: string) => t || identity,
})) as any;
indexPattern.fields.getByName = name =>
({
format: {
getConverterFor: (t?: string) => t || identity,
},
} as IndexPatternField);
});
it('works with primitive param types', () => {
const ac = new AggConfigs(indexPattern, [], { typesRegistry, fieldFormats });
const configStates = {
enabled: true,
type: 'terms',
schema: 'segment',
params: {
field: 'machine.os.keyword',
order: 'asc',
},
};
const aggConfig = ac.createAggConfig(configStates);
expect(aggConfig.toExpressionAst()).toMatchInlineSnapshot(`
Object {
"arguments": Object {
"enabled": Array [
true,
],
"id": Array [
"1",
],
"missingBucket": Array [
false,
],
"missingBucketLabel": Array [
"Missing",
],
"order": Array [
"asc",
],
"otherBucket": Array [
false,
],
"otherBucketLabel": Array [
"Other",
],
"schema": Array [
"segment",
],
"size": Array [
5,
],
},
"function": "aggTerms",
"type": "function",
}
`);
});
it('creates a subexpression for params of type "agg"', () => {
const ac = new AggConfigs(indexPattern, [], { typesRegistry, fieldFormats });
const configStates = {
type: 'terms',
params: {
field: 'machine.os.keyword',
order: 'asc',
orderAgg: {
enabled: true,
type: 'terms',
params: {
field: 'bytes',
order: 'asc',
size: 5,
},
},
},
};
const aggConfig = ac.createAggConfig(configStates);
const aggArg = aggConfig.toExpressionAst()?.arguments.orderAgg;
expect(aggArg).toMatchInlineSnapshot(`
Array [
Object {
"chain": Array [
Object {
"arguments": Object {
"enabled": Array [
true,
],
"id": Array [
"1-orderAgg",
],
"missingBucket": Array [
false,
],
"missingBucketLabel": Array [
"Missing",
],
"order": Array [
"asc",
],
"otherBucket": Array [
false,
],
"otherBucketLabel": Array [
"Other",
],
"schema": Array [
"orderAgg",
],
"size": Array [
5,
],
},
"function": "aggTerms",
"type": "function",
},
],
"type": "expression",
},
]
`);
});
it('creates a subexpression for param types other than "agg" which have specified toExpressionAst', () => {
// Overwrite the `ranges` param in the `range` agg with a mock toExpressionAst function
const range: MetricAggType = typesRegistry.get('range');
range.expressionName = 'aggRange';
const rangesParam = range.params.find(p => p.name === 'ranges');
rangesParam!.toExpressionAst = (val: any) => ({
type: 'function',
function: 'aggRanges',
arguments: {
ranges: ['oh hi there!'],
},
});
const ac = new AggConfigs(indexPattern, [], { typesRegistry, fieldFormats });
const configStates = {
type: 'range',
params: {
field: 'bytes',
},
};
const aggConfig = ac.createAggConfig(configStates);
const ranges = aggConfig.toExpressionAst()!.arguments.ranges;
expect(ranges).toMatchInlineSnapshot(`
Array [
Object {
"chain": Array [
Object {
"arguments": Object {
"ranges": Array [
"oh hi there!",
],
},
"function": "aggRanges",
"type": "function",
},
],
"type": "expression",
},
]
`);
});
it('stringifies any other params which are an object', () => {
const ac = new AggConfigs(indexPattern, [], { typesRegistry, fieldFormats });
const configStates = {
type: 'terms',
params: {
field: 'machine.os.keyword',
order: 'asc',
json: { foo: 'bar' },
},
};
const aggConfig = ac.createAggConfig(configStates);
const json = aggConfig.toExpressionAst()?.arguments.json;
expect(json).toEqual([JSON.stringify(configStates.params.json)]);
});
it(`returns undefined if an expressionName doesn't exist on the agg type`, () => {
const ac = new AggConfigs(indexPattern, [], { typesRegistry, fieldFormats });
const configStates = {
type: 'unknown type',
params: {},
};
const aggConfig = ac.createAggConfig(configStates);
expect(aggConfig.toExpressionAst()).toBe(undefined);
});
});
describe('#makeLabel', () => {
let aggConfig: AggConfig;
@ -422,6 +620,9 @@ describe('AggConfig', () => {
let aggConfig: AggConfig;
beforeEach(() => {
fieldFormats.getDefaultInstance = (() => ({
getConverterFor: (t?: string) => t || identity,
})) as any;
indexPattern.fields.getByName = name =>
({
format: {
@ -434,11 +635,7 @@ describe('AggConfig', () => {
type: 'histogram',
schema: 'bucket',
params: {
field: {
format: {
getConverterFor: (t?: string) => t || identity,
},
},
field: 'bytes',
},
};
const ac = new AggConfigs(indexPattern, [configStates], { typesRegistry, fieldFormats });
@ -446,6 +643,11 @@ describe('AggConfig', () => {
});
it("returns the field's formatter", () => {
aggConfig.params.field = {
format: {
getConverterFor: (t?: string) => t || identity,
},
};
expect(aggConfig.fieldFormatter().toString()).toBe(
aggConfig
.getField()

View file

@ -19,6 +19,8 @@
import _ from 'lodash';
import { i18n } from '@kbn/i18n';
import { Assign } from '@kbn/utility-types';
import { ExpressionAstFunction, ExpressionAstArgument } from 'src/plugins/expressions/public';
import { IAggType } from './agg_type';
import { writeParams } from './agg_params';
import { IAggConfigs } from './agg_configs';
@ -27,11 +29,17 @@ import { ISearchSource } from '../search_source';
import { FieldFormatsContentType, KBN_FIELD_TYPES } from '../../../common';
import { FieldFormatsStart } from '../../field_formats';
export interface AggConfigOptions {
type: IAggType;
type State = string | number | boolean | null | undefined | SerializableState;
interface SerializableState {
[key: string]: State | State[];
}
export interface AggConfigSerialized {
type: string;
enabled?: boolean;
id?: string;
params?: Record<string, any>;
params?: SerializableState;
schema?: string;
}
@ -39,6 +47,8 @@ export interface AggConfigDependencies {
fieldFormats: FieldFormatsStart;
}
export type AggConfigOptions = Assign<AggConfigSerialized, { type: IAggType }>;
/**
* @name AggConfig
*
@ -257,7 +267,10 @@ export class AggConfig {
return configDsl;
}
toJSON() {
/**
* @returns Returns a serialized representation of an AggConfig.
*/
serialize(): AggConfigSerialized {
const params = this.params;
const outParams = _.transform(
@ -281,7 +294,64 @@ export class AggConfig {
enabled: this.enabled,
type: this.type && this.type.name,
schema: this.schema,
params: outParams,
params: outParams as SerializableState,
};
}
/**
* @deprecated - Use serialize() instead.
*/
toJSON(): AggConfigSerialized {
return this.serialize();
}
/**
* @returns Returns an ExpressionAst representing the function for this agg type.
*/
toExpressionAst(): ExpressionAstFunction | undefined {
const functionName = this.type && this.type.expressionName;
const { type, ...rest } = this.serialize();
if (!functionName || !rest.params) {
// Return undefined - there is no matching expression function for this agg
return;
}
// Go through each of the params and convert to an array of expression args.
const params = Object.entries(rest.params).reduce((acc, [key, value]) => {
const deserializedParam = this.getAggParams().find(p => p.name === key);
if (deserializedParam && deserializedParam.toExpressionAst) {
// If the param provides `toExpressionAst`, we call it with the value
const paramExpressionAst = deserializedParam.toExpressionAst(this.getParam(key));
if (paramExpressionAst) {
acc[key] = [
{
type: 'expression',
chain: [paramExpressionAst],
},
];
}
} else if (typeof value === 'object') {
// For object params which don't provide `toExpressionAst`, we stringify
acc[key] = [JSON.stringify(value)];
} else if (typeof value !== 'undefined') {
// Everything else just gets stored in an array if it is defined
acc[key] = [value];
}
return acc;
}, {} as Record<string, ExpressionAstArgument[]>);
return {
type: 'function',
function: functionName,
arguments: {
...params,
// Expression args which are provided to all functions
id: [this.id],
enabled: [this.enabled],
...(this.schema ? { schema: [this.schema] } : {}), // schema may be undefined
},
};
}

View file

@ -20,7 +20,7 @@
import _ from 'lodash';
import { Assign } from '@kbn/utility-types';
import { AggConfig, AggConfigOptions, IAggConfig } from './agg_config';
import { AggConfig, AggConfigSerialized, IAggConfig } from './agg_config';
import { IAggType } from './agg_type';
import { AggTypesRegistryStart } from './agg_types_registry';
import { AggGroupNames } from './agg_groups';
@ -51,7 +51,7 @@ export interface AggConfigsOptions {
fieldFormats: FieldFormatsStart;
}
export type CreateAggConfigParams = Assign<AggConfigOptions, { type: string | IAggType }>;
export type CreateAggConfigParams = Assign<AggConfigSerialized, { type: string | IAggType }>;
/**
* @name AggConfigs

View file

@ -39,6 +39,7 @@ export interface AggTypeConfig<
createFilter?: (aggConfig: TAggConfig, key: any, params?: any) => any;
type?: string;
dslName?: string;
expressionName?: string;
makeLabel?: ((aggConfig: TAggConfig) => string) | (() => string);
ordered?: any;
hasNoDsl?: boolean;
@ -88,6 +89,14 @@ export class AggType<
* @type {string}
*/
dslName: string;
/**
* the name of the expression function that this aggType represents.
* TODO: this should probably be a required field.
*
* @property name
* @type {string}
*/
expressionName?: string;
/**
* the user friendly name that will be shown in the ui for this aggType
*
@ -219,6 +228,7 @@ export class AggType<
this.name = config.name;
this.type = config.type || 'metrics';
this.dslName = config.dslName || config.name;
this.expressionName = config.expressionName;
this.title = config.title;
this.makeLabel = config.makeLabel || constant(this.name);
this.ordered = config.ordered;

View file

@ -37,6 +37,7 @@ import { getDerivativeMetricAgg } from './metrics/derivative';
import { getCumulativeSumMetricAgg } from './metrics/cumulative_sum';
import { getMovingAvgMetricAgg } from './metrics/moving_avg';
import { getSerialDiffMetricAgg } from './metrics/serial_diff';
import { getDateHistogramBucketAgg } from './buckets/date_histogram';
import { getHistogramBucketAgg } from './buckets/histogram';
import { getRangeBucketAgg } from './buckets/range';
@ -103,3 +104,7 @@ export const getAggTypes = ({
getGeoTitleBucketAgg({ getInternalStartServices }),
],
});
import { aggTerms } from './buckets/terms_fn';
export const getAggTypesFunctions = () => [aggTerms];

View file

@ -26,7 +26,7 @@ import {
isStringOrNumberType,
migrateIncludeExcludeFormat,
} from './migrate_include_exclude_format';
import { IAggConfigs } from '../agg_configs';
import { AggConfigSerialized, IAggConfigs } from '../types';
import { Adapters } from '../../../../../inspector/public';
import { ISearchSource } from '../../search_source';
@ -63,10 +63,27 @@ export interface TermsBucketAggDependencies {
getInternalStartServices: GetInternalStartServicesFn;
}
export interface AggParamsTerms {
field: string;
order: 'asc' | 'desc';
orderBy: string;
orderAgg?: AggConfigSerialized;
size?: number;
missingBucket?: boolean;
missingBucketLabel?: string;
otherBucket?: boolean;
otherBucketLabel?: string;
// advanced
exclude?: string;
include?: string;
json?: string;
}
export const getTermsBucketAgg = ({ getInternalStartServices }: TermsBucketAggDependencies) =>
new BucketAggType(
{
name: BUCKET_TYPES.TERMS,
expressionName: 'aggTerms',
title: termsTitle,
makeLabel(agg) {
const params = agg.params;
@ -154,8 +171,7 @@ export const getTermsBucketAgg = ({ getInternalStartServices }: TermsBucketAggDe
type: 'agg',
allowedAggs: termsAggFilter,
default: null,
makeAgg(termsAgg, state) {
state = state || {};
makeAgg(termsAgg, state = { type: 'count' }) {
state.schema = 'orderAgg';
const orderAgg = termsAgg.aggConfigs.createAggConfig<IBucketAggConfig>(state, {
addToAggConfigs: false,

View file

@ -0,0 +1,164 @@
/*
* 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 { aggTerms } from './terms_fn';
describe('agg_expression_functions', () => {
describe('aggTerms', () => {
const fn = functionWrapper(aggTerms());
test('fills in defaults when only required args are provided', () => {
const actual = fn({
field: 'machine.os.keyword',
order: 'asc',
orderBy: '1',
});
expect(actual).toMatchInlineSnapshot(`
Object {
"type": "agg_type",
"value": Object {
"enabled": true,
"id": undefined,
"params": Object {
"exclude": undefined,
"field": "machine.os.keyword",
"include": undefined,
"json": undefined,
"missingBucket": false,
"missingBucketLabel": "Missing",
"order": "asc",
"orderAgg": undefined,
"orderBy": "1",
"otherBucket": false,
"otherBucketLabel": "Other",
"size": 5,
},
"schema": undefined,
"type": "terms",
},
}
`);
});
test('includes optional params when they are provided', () => {
const actual = fn({
id: '1',
enabled: false,
schema: 'whatever',
field: 'machine.os.keyword',
order: 'desc',
orderBy: '2',
size: 6,
missingBucket: true,
missingBucketLabel: 'missing',
otherBucket: true,
otherBucketLabel: 'other',
exclude: 'ios',
});
expect(actual.value).toMatchInlineSnapshot(`
Object {
"enabled": false,
"id": "1",
"params": Object {
"exclude": "ios",
"field": "machine.os.keyword",
"include": undefined,
"json": undefined,
"missingBucket": true,
"missingBucketLabel": "missing",
"order": "desc",
"orderAgg": undefined,
"orderBy": "2",
"otherBucket": true,
"otherBucketLabel": "other",
"size": 6,
},
"schema": "whatever",
"type": "terms",
}
`);
});
test('handles orderAgg as a subexpression', () => {
const actual = fn({
field: 'machine.os.keyword',
order: 'asc',
orderBy: '1',
orderAgg: fn({ field: 'name', order: 'asc', orderBy: '1' }),
});
expect(actual.value.params).toMatchInlineSnapshot(`
Object {
"exclude": undefined,
"field": "machine.os.keyword",
"include": undefined,
"json": undefined,
"missingBucket": false,
"missingBucketLabel": "Missing",
"order": "asc",
"orderAgg": Object {
"enabled": true,
"id": undefined,
"params": Object {
"exclude": undefined,
"field": "name",
"include": undefined,
"json": undefined,
"missingBucket": false,
"missingBucketLabel": "Missing",
"order": "asc",
"orderAgg": undefined,
"orderBy": "1",
"otherBucket": false,
"otherBucketLabel": "Other",
"size": 5,
},
"schema": undefined,
"type": "terms",
},
"orderBy": "1",
"otherBucket": false,
"otherBucketLabel": "Other",
"size": 5,
}
`);
});
test('correctly parses json string argument', () => {
const actual = fn({
field: 'machine.os.keyword',
order: 'asc',
orderBy: '1',
json: '{ "foo": true }',
});
expect(actual.value.params.json).toEqual({ foo: true });
expect(() => {
fn({
field: 'machine.os.keyword',
order: 'asc',
orderBy: '1',
json: '/// intentionally malformed json ///',
});
}).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`);
});
});
});

View file

@ -0,0 +1,181 @@
/*
* 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 } from '../';
const aggName = 'terms';
const fnName = 'aggTerms';
type Input = any;
type AggArgs = AggExpressionFunctionArgs<typeof aggName>;
// 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 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',
}),
type: 'agg_type',
args: {
id: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.terms.id.help', {
defaultMessage: 'ID for this aggregation',
}),
},
enabled: {
types: ['boolean'],
default: true,
help: i18n.translate('data.search.aggs.buckets.terms.enabled.help', {
defaultMessage: 'Specifies whether this aggregation should be enabled',
}),
},
schema: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.terms.schema.help', {
defaultMessage: 'Schema to use for this aggregation',
}),
},
field: {
types: ['string'],
required: true,
help: i18n.translate('data.search.aggs.buckets.terms.field.help', {
defaultMessage: 'Field to use for this aggregation',
}),
},
order: {
types: ['string'],
required: true,
help: i18n.translate('data.search.aggs.buckets.terms.order.help', {
defaultMessage: 'Order in which to return the results: asc or desc',
}),
},
orderBy: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.terms.orderBy.help', {
defaultMessage: 'Field to order results by',
}),
},
orderAgg: {
types: ['agg_type'],
help: i18n.translate('data.search.aggs.buckets.terms.orderAgg.help', {
defaultMessage: 'Agg config to use for ordering results',
}),
},
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',
}),
},
exclude: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.terms.exclude.help', {
defaultMessage: 'Specific bucket values to exclude from results',
}),
},
include: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.terms.include.help', {
defaultMessage: 'Specific bucket values to include in results',
}),
},
json: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.terms.json.help', {
defaultMessage: 'Advanced json to include when the agg is sent to Elasticsearch',
}),
},
},
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,
params: {
...rest,
orderAgg,
json,
},
},
};
},
});

View file

@ -36,14 +36,14 @@ const metricAggFilter = [
'!geo_centroid',
];
const parentPipelineType = i18n.translate(
export const parentPipelineType = i18n.translate(
'data.search.aggs.metrics.parentPipelineAggregationsSubtypeTitle',
{
defaultMessage: 'Parent Pipeline Aggregations',
}
);
const parentPipelineAggHelper = {
export const parentPipelineAggHelper = {
subtype: parentPipelineType,
params() {
return [
@ -56,13 +56,9 @@ const parentPipelineAggHelper = {
name: 'customMetric',
type: 'agg',
allowedAggs: metricAggFilter,
makeAgg(termsAgg, state: any) {
state = state || { type: 'count' };
makeAgg(termsAgg, state = { type: 'count' }) {
const metricAgg = termsAgg.aggConfigs.createAggConfig(state, { addToAggConfigs: false });
metricAgg.id = termsAgg.id + '-metric';
return metricAgg;
},
modifyAggConfigOnSearchRequestStart: forwardModifyAggConfigOnSearchRequestStart(
@ -89,5 +85,3 @@ const parentPipelineAggHelper = {
return subAgg ? subAgg.type.getFormat(subAgg) : new (FieldFormat.from(identity))();
},
};
export { parentPipelineAggHelper, parentPipelineType };

View file

@ -43,14 +43,14 @@ const metricAggFilter: string[] = [
];
const bucketAggFilter: string[] = [];
const siblingPipelineType = i18n.translate(
export const siblingPipelineType = i18n.translate(
'data.search.aggs.metrics.siblingPipelineAggregationsSubtypeTitle',
{
defaultMessage: 'Sibling pipeline aggregations',
}
);
const siblingPipelineAggHelper = {
export const siblingPipelineAggHelper = {
subtype: siblingPipelineType,
params() {
return [
@ -59,11 +59,9 @@ const siblingPipelineAggHelper = {
type: 'agg',
allowedAggs: bucketAggFilter,
default: null,
makeAgg(agg: IMetricAggConfig, state: any) {
state = state || { type: 'date_histogram' };
makeAgg(agg: IMetricAggConfig, state = { type: 'date_histogram' }) {
const orderAgg = agg.aggConfigs.createAggConfig(state, { addToAggConfigs: false });
orderAgg.id = agg.id + '-bucket';
return orderAgg;
},
modifyAggConfigOnSearchRequestStart: forwardModifyAggConfigOnSearchRequestStart(
@ -76,11 +74,9 @@ const siblingPipelineAggHelper = {
type: 'agg',
allowedAggs: metricAggFilter,
default: null,
makeAgg(agg: IMetricAggConfig, state: any) {
state = state || { type: 'count' };
makeAgg(agg: IMetricAggConfig, state = { type: 'count' }) {
const orderAgg = agg.aggConfigs.createAggConfig(state, { addToAggConfigs: false });
orderAgg.id = agg.id + '-metric';
return orderAgg;
},
modifyAggConfigOnSearchRequestStart: forwardModifyAggConfigOnSearchRequestStart(
@ -98,5 +94,3 @@ const siblingPipelineAggHelper = {
: new (FieldFormat.from(identity))();
},
};
export { siblingPipelineAggHelper, siblingPipelineType };

View file

@ -25,21 +25,28 @@ import {
import { AggConfigs, IAggConfigs } from '../agg_configs';
import { mockAggTypesRegistry } from '../test_helpers';
import { METRIC_TYPES } from './metric_agg_types';
import { FieldFormatsStart } from '../../../field_formats';
import { fieldFormatsServiceMock } from '../../../field_formats/mocks';
import { notificationServiceMock } from '../../../../../../../src/core/public/mocks';
import { InternalStartServices } from '../../../types';
describe('AggTypesMetricsPercentileRanksProvider class', function() {
let aggConfigs: IAggConfigs;
const aggTypesDependencies: PercentileRanksMetricAggDependencies = {
getInternalStartServices: () =>
(({
fieldFormats: fieldFormatsServiceMock.createStartContract(),
notifications: notificationServiceMock.createStartContract(),
} as unknown) as InternalStartServices),
};
let fieldFormats: FieldFormatsStart;
let aggTypesDependencies: PercentileRanksMetricAggDependencies;
beforeEach(() => {
fieldFormats = fieldFormatsServiceMock.createStartContract();
fieldFormats.getDefaultInstance = (() => ({
convert: (t?: string) => t,
})) as any;
aggTypesDependencies = {
getInternalStartServices: () =>
(({
fieldFormats,
notifications: notificationServiceMock.createStartContract(),
} as unknown) as InternalStartServices),
};
const typesRegistry = mockAggTypesRegistry([getPercentileRanksMetricAgg(aggTypesDependencies)]);
const field = {
name: 'bytes',
@ -61,12 +68,7 @@ describe('AggTypesMetricsPercentileRanksProvider class', function() {
type: METRIC_TYPES.PERCENTILE_RANKS,
schema: 'metric',
params: {
field: {
displayName: 'bytes',
format: {
convert: jest.fn(x => x),
},
},
field: 'bytes',
customLabel: 'my custom field label',
values: [5000, 10000],
},

View file

@ -61,12 +61,7 @@ describe('AggTypesMetricsPercentilesProvider class', () => {
type: METRIC_TYPES.PERCENTILES,
schema: 'metric',
params: {
field: {
displayName: 'bytes',
format: {
convert: jest.fn(x => `${x}th`),
},
},
field: 'bytes',
customLabel: 'prince',
percents: [95],
},

View file

@ -17,13 +17,13 @@
* under the License.
*/
import { AggConfig, IAggConfig } from '../agg_config';
import { AggConfig, IAggConfig, AggConfigSerialized } from '../agg_config';
import { BaseParamType } from './base';
export class AggParamType<TAggConfig extends IAggConfig = IAggConfig> extends BaseParamType<
TAggConfig
> {
makeAgg: (agg: TAggConfig, state?: any) => TAggConfig;
makeAgg: (agg: TAggConfig, state?: AggConfigSerialized) => TAggConfig;
allowedAggs: string[] = [];
constructor(config: Record<string, any>) {
@ -42,17 +42,25 @@ export class AggParamType<TAggConfig extends IAggConfig = IAggConfig> extends Ba
}
if (!config.serialize) {
this.serialize = (agg: TAggConfig) => {
return agg.toJSON();
return agg.serialize();
};
}
if (!config.deserialize) {
this.deserialize = (state: unknown, agg?: TAggConfig): TAggConfig => {
this.deserialize = (state: AggConfigSerialized, agg?: TAggConfig): TAggConfig => {
if (!agg) {
throw new Error('aggConfig was not provided to AggParamType deserialize function');
}
return this.makeAgg(agg, state);
};
}
if (!config.toExpressionAst) {
this.toExpressionAst = (agg: TAggConfig) => {
if (!agg || !agg.toExpressionAst) {
throw new Error('aggConfig was not provided to AggParamType toExpressionAst function');
}
return agg.toExpressionAst();
};
}
this.makeAgg = config.makeAgg;
this.valueType = AggConfig;

View file

@ -17,6 +17,7 @@
* under the License.
*/
import { ExpressionAstFunction } from 'src/plugins/expressions/public';
import { IAggConfigs } from '../agg_configs';
import { IAggConfig } from '../agg_config';
import { FetchOptions } from '../../fetch';
@ -37,6 +38,7 @@ export class BaseParamType<TAggConfig extends IAggConfig = IAggConfig> {
) => void;
serialize: (value: any, aggConfig?: TAggConfig) => any;
deserialize: (value: any, aggConfig?: TAggConfig) => any;
toExpressionAst?: (value: any) => ExpressionAstFunction | undefined;
options: any[];
valueType?: any;
@ -77,6 +79,7 @@ export class BaseParamType<TAggConfig extends IAggConfig = IAggConfig> {
this.write = config.write || defaultWrite;
this.serialize = config.serialize;
this.deserialize = config.deserialize;
this.toExpressionAst = config.toExpressionAst;
this.options = config.options;
this.modifyAggConfigOnSearchRequestStart =
config.modifyAggConfigOnSearchRequestStart || function() {};

View file

@ -0,0 +1,49 @@
/*
* 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 { mapValues } from 'lodash';
import {
AnyExpressionFunctionDefinition,
ExpressionFunctionDefinition,
ExecutionContext,
} from '../../../../../../plugins/expressions/public';
/**
* Takes a function spec and passes in default args,
* overriding with any provided args.
*
* Similar to the test helper used in Expressions & Canvas,
* however in this case we are ignoring the input & execution
* context, as they are not applicable to the agg type
* expression functions.
*/
export const functionWrapper = <T extends AnyExpressionFunctionDefinition>(spec: T) => {
const defaultArgs = mapValues(spec.args, argSpec => argSpec.default);
return (
args: T extends ExpressionFunctionDefinition<
infer Name,
infer Input,
infer Arguments,
infer Output,
infer Context
>
? Arguments
: never
) => spec.fn(null, { ...defaultArgs, ...args }, {} as ExecutionContext);
};

View file

@ -17,5 +17,6 @@
* under the License.
*/
export { functionWrapper } from './function_wrapper';
export { mockAggTypesRegistry } from './mock_agg_types_registry';
export { mockDataServices } from './mock_data_services';

View file

@ -19,21 +19,23 @@
import { IndexPattern } from '../../index_patterns';
import {
AggConfig,
AggConfigSerialized,
AggConfigs,
AggParamsTerms,
AggType,
aggTypeFieldFilters,
AggTypesRegistrySetup,
AggTypesRegistryStart,
AggConfig,
AggConfigs,
CreateAggConfigParams,
FieldParamType,
getCalculateAutoTimeExpression,
MetricAggType,
aggTypeFieldFilters,
parentPipelineAggHelper,
siblingPipelineAggHelper,
} from './';
export { IAggConfig } from './agg_config';
export { IAggConfig, AggConfigSerialized } from './agg_config';
export { CreateAggConfigParams, IAggConfigs } from './agg_configs';
export { IAggType } from './agg_type';
export { AggParam, AggParamOption } from './agg_params';
@ -70,3 +72,25 @@ export interface SearchAggsStart {
) => InstanceType<typeof AggConfigs>;
types: AggTypesRegistryStart;
}
/** @internal */
export interface AggExpressionType {
type: 'agg_type';
value: AggConfigSerialized;
}
/** @internal */
export type AggExpressionFunctionArgs<
Name extends keyof AggParamsMapping
> = AggParamsMapping[Name] & Pick<AggConfigSerialized, 'id' | 'enabled' | 'schema'>;
/**
* A global list of the param interfaces for each agg type.
* For now this is internal, but eventually we will probably
* want to make it public.
*
* @internal
*/
export interface AggParamsMapping {
terms: AggParamsTerms;
}

View file

@ -27,7 +27,7 @@ export const serializeAggConfig = (aggConfig: IAggConfig): KibanaDatatableColumn
return {
type: aggConfig.type.name,
indexPatternId: aggConfig.getIndexPattern().id,
aggConfigParams: aggConfig.toJSON().params,
aggConfigParams: aggConfig.serialize().params,
};
};

View file

@ -18,9 +18,10 @@
*/
import { coreMock } from '../../../../core/public/mocks';
import { CoreSetup } from '../../../../core/public';
import { expressionsPluginMock } from '../../../../plugins/expressions/public/mocks';
import { SearchService } from './search_service';
import { CoreSetup } from '../../../../core/public';
describe('Search service', () => {
let searchService: SearchService;
@ -35,6 +36,7 @@ describe('Search service', () => {
it('exposes proper contract', async () => {
const setup = searchService.setup(mockCoreSetup, {
packageInfo: { version: '8' },
expressions: expressionsPluginMock.createSetupContract(),
} as any);
expect(setup).toHaveProperty('registerSearchStrategyProvider');
});

View file

@ -18,6 +18,7 @@
*/
import { Plugin, CoreSetup, CoreStart, PackageInfo } from '../../../../core/public';
import { ExpressionsSetup } from '../../../../plugins/expressions/public';
import { SYNC_SEARCH_STRATEGY, syncSearchStrategyProvider } from './sync_search_strategy';
import {
@ -37,6 +38,7 @@ import { GetInternalStartServicesFn } from '../types';
import { SearchInterceptor } from './search_interceptor';
import {
getAggTypes,
getAggTypesFunctions,
AggType,
AggTypesRegistry,
AggConfig,
@ -52,9 +54,10 @@ import { FieldFormatsStart } from '../field_formats';
import { ISearchGeneric } from './i_search';
interface SearchServiceSetupDependencies {
expressions: ExpressionsSetup;
getInternalStartServices: GetInternalStartServicesFn;
packageInfo: PackageInfo;
query: QuerySetup;
getInternalStartServices: GetInternalStartServicesFn;
}
interface SearchServiceStartDependencies {
@ -97,22 +100,27 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
public setup(
core: CoreSetup,
{ packageInfo, query, getInternalStartServices }: SearchServiceSetupDependencies
{ expressions, packageInfo, query, getInternalStartServices }: SearchServiceSetupDependencies
): ISearchSetup {
this.esClient = getEsClient(core.injectedMetadata, core.http, packageInfo);
this.registerSearchStrategyProvider(SYNC_SEARCH_STRATEGY, syncSearchStrategyProvider);
this.registerSearchStrategyProvider(ES_SEARCH_STRATEGY, esSearchStrategyProvider);
const aggTypesSetup = this.aggTypesRegistry.setup();
// register each agg type
const aggTypes = getAggTypes({
query,
uiSettings: core.uiSettings,
getInternalStartServices,
});
aggTypes.buckets.forEach(b => aggTypesSetup.registerBucket(b));
aggTypes.metrics.forEach(m => aggTypesSetup.registerMetric(m));
// register expression functions for each agg type
const aggFunctions = getAggTypesFunctions();
aggFunctions.forEach(fn => expressions.registerFunction(fn));
return {
aggs: {
calculateAutoTimeExpression: getCalculateAutoTimeExpression(core.uiSettings),