[data.search.aggs] Remove service getters from agg types (#61628)

* [data.search.aggs] Remove service getters from agg types

Part of #60333

* new portion of changes

* pass dependencies to MetricAgg Type through constructor

* update docs

* refactoring buckets

* remove unused mockDataServices

* Remove service getters from metrics

* Some fixes

* remove temporary code

* moved notifications to the getInternalStartServices

* fixed karma lock

* update docs

* Fixed tests

* fix broken CI

* fix PR comment

* fix typo

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
Co-authored-by: Uladzislau Lasitsa <Uladzislau_Lasitsa@epam.com>
This commit is contained in:
Alexey Antonov 2020-04-07 13:39:30 +03:00 committed by GitHub
parent fb0d0a5834
commit 008b0fda64
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
85 changed files with 2642 additions and 1908 deletions

View file

@ -19,7 +19,7 @@ search: {
intervalOptions: ({
display: string;
val: string;
enabled(agg: import("./search/aggs/buckets/_bucket_agg_type").IBucketAggConfig): boolean | "" | undefined;
enabled(agg: import("./search/aggs/buckets/bucket_agg_type").IBucketAggConfig): boolean | "" | undefined;
} | {
display: string;
val: string;

View file

@ -88,6 +88,9 @@ const mockCoreStart = {
get: sinon.fake.returns(''),
},
},
notifications: {
toasts: {},
},
i18n: {},
overlays: {},
savedObjects: {
@ -164,8 +167,11 @@ const mockAggTypesRegistry = () => {
const registrySetup = registry.setup();
const aggTypes = getAggTypes({
uiSettings: mockCoreSetup.uiSettings,
notifications: mockCoreStart.notifications,
query: querySetup,
getInternalStartServices: () => ({
fieldFormats: getFieldFormatsRegistry(mockCoreStart),
notifications: mockCoreStart.notifications,
}),
});
aggTypes.buckets.forEach(type => registrySetup.registerBucket(type));
aggTypes.metrics.forEach(type => registrySetup.registerMetric(type));

View file

@ -17,23 +17,14 @@
* under the License.
*/
import { FieldFormat, IFieldFormatsRegistry } from '.';
const fieldFormatMock = ({
convert: jest.fn(),
getConverterFor: jest.fn(),
getParamDefaults: jest.fn(),
param: jest.fn(),
params: jest.fn(),
toJSON: jest.fn(),
type: jest.fn(),
setupContentType: jest.fn(),
} as unknown) as FieldFormat;
import { IFieldFormatsRegistry } from '.';
export const fieldFormatsMock: IFieldFormatsRegistry = {
getByFieldType: jest.fn(),
getDefaultConfig: jest.fn(),
getDefaultInstance: jest.fn().mockImplementation(() => fieldFormatMock) as any,
getDefaultInstance: jest.fn().mockImplementation(() => ({
getConverterFor: jest.fn().mockImplementation(() => (t: string) => t),
})) as any,
getDefaultInstanceCacheResolver: jest.fn(),
getDefaultInstancePlain: jest.fn(),
getDefaultType: jest.fn(),

View file

@ -0,0 +1,41 @@
/*
* 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 { FieldFormatsStart, FieldFormatsSetup, FieldFormatsService } from '.';
import { fieldFormatsMock } from '../../common/field_formats/mocks';
type FieldFormatsServiceClientContract = PublicMethodsOf<FieldFormatsService>;
const createSetupContractMock = () => fieldFormatsMock as FieldFormatsSetup;
const createStartContractMock = () => fieldFormatsMock as FieldFormatsStart;
const createMock = () => {
const mocked: jest.Mocked<FieldFormatsServiceClientContract> = {
setup: jest.fn().mockReturnValue(createSetupContractMock()),
start: jest.fn().mockReturnValue(createStartContractMock()),
};
return mocked;
};
export const fieldFormatsServiceMock = {
create: createMock,
createSetupContract: createSetupContractMock,
createStartContract: createStartContractMock,
};

View file

@ -17,8 +17,8 @@
* under the License.
*/
import { Plugin, DataPublicPluginSetup, DataPublicPluginStart, IndexPatternsContract } from '.';
import { fieldFormatsMock } from '../common/field_formats/mocks';
import { Plugin, IndexPatternsContract } from '.';
import { fieldFormatsServiceMock } from './field_formats/mocks';
import { searchSetupMock, searchStartMock } from './search/mocks';
import { queryServiceMock } from './query/mocks';
@ -36,7 +36,7 @@ const createSetupContract = (): Setup => {
return {
autocomplete: autocompleteMock,
search: searchSetupMock,
fieldFormats: fieldFormatsMock as DataPublicPluginSetup['fieldFormats'],
fieldFormats: fieldFormatsServiceMock.createSetupContract(),
query: querySetupMock,
};
};
@ -49,7 +49,7 @@ const createStartContract = (): Start => {
},
autocomplete: autocompleteMock,
search: searchStartMock,
fieldFormats: fieldFormatsMock as DataPublicPluginStart['fieldFormats'],
fieldFormats: fieldFormatsServiceMock.createStartContract(),
query: queryStartMock,
ui: {
IndexPatternSelect: jest.fn(),

View file

@ -30,6 +30,7 @@ import {
DataPublicPluginStart,
DataSetupDependencies,
DataStartDependencies,
GetInternalStartServicesFn,
} from './types';
import { AutocompleteService } from './autocomplete';
import { SearchService } from './search/search_service';
@ -47,6 +48,8 @@ import {
setQueryService,
setSearchService,
setUiSettings,
getFieldFormats,
getNotifications,
} from './services';
import { createSearchBar } from './ui/search_bar/create_search_bar';
import { esaggs } from './search/expressions';
@ -100,6 +103,11 @@ export class DataPublicPlugin implements Plugin<DataPublicPluginSetup, DataPubli
expressions.registerFunction(esaggs);
const getInternalStartServices: GetInternalStartServicesFn = () => ({
fieldFormats: getFieldFormats(),
notifications: getNotifications(),
});
const queryService = this.queryService.setup({
uiSettings: core.uiSettings,
storage: this.storage,
@ -122,6 +130,7 @@ export class DataPublicPlugin implements Plugin<DataPublicPluginSetup, DataPubli
return {
autocomplete: this.autocomplete.setup(core),
search: this.searchService.setup(core, {
getInternalStartServices,
packageInfo: this.packageInfo,
query: queryService,
}),

View file

@ -12,8 +12,8 @@ import { Assign } from '@kbn/utility-types';
import { Breadcrumb } from '@elastic/eui';
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 { CoreStart } from 'src/core/public';
import { CoreStart as CoreStart_2 } from 'kibana/public';
import { EuiButtonEmptyProps } from '@elastic/eui';
import { EuiComboBoxProps } from '@elastic/eui';
import { EuiConfirmModalProps } from '@elastic/eui';
@ -632,21 +632,21 @@ export type IAggType = AggType;
// Warning: (ae-missing-release-tag) "IDataPluginServices" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export interface IDataPluginServices extends Partial<CoreStart_2> {
export interface IDataPluginServices extends Partial<CoreStart> {
// (undocumented)
appName: string;
// (undocumented)
data: DataPublicPluginStart;
// (undocumented)
http: CoreStart_2['http'];
http: CoreStart['http'];
// (undocumented)
notifications: CoreStart_2['notifications'];
notifications: CoreStart['notifications'];
// (undocumented)
savedObjects: CoreStart_2['savedObjects'];
savedObjects: CoreStart['savedObjects'];
// (undocumented)
storage: IStorageWrapper;
// (undocumented)
uiSettings: CoreStart_2['uiSettings'];
uiSettings: CoreStart['uiSettings'];
}
// Warning: (ae-missing-release-tag) "IEsSearchRequest" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@ -1094,7 +1094,7 @@ export type ISearch<T extends TStrategyTypes = typeof DEFAULT_SEARCH_STRATEGY> =
// @public (undocumented)
export interface ISearchContext {
// (undocumented)
core: CoreStart;
core: CoreStart_2;
// (undocumented)
getSearchStrategy: <T extends TStrategyTypes>(name: T) => TSearchStrategyProvider<T>;
}
@ -1307,7 +1307,7 @@ export class Plugin implements Plugin_2<DataPublicPluginSetup, DataPublicPluginS
// Warning: (ae-forgotten-export) The symbol "DataStartDependencies" needs to be exported by the entry point index.d.ts
//
// (undocumented)
start(core: CoreStart_2, { uiActions }: DataStartDependencies): DataPublicPluginStart;
start(core: CoreStart, { uiActions }: DataStartDependencies): DataPublicPluginStart;
// (undocumented)
stop(): void;
}
@ -1533,7 +1533,7 @@ export const search: {
intervalOptions: ({
display: string;
val: string;
enabled(agg: import("./search/aggs/buckets/_bucket_agg_type").IBucketAggConfig): boolean | "" | undefined;
enabled(agg: import("./search/aggs/buckets/bucket_agg_type").IBucketAggConfig): boolean | "" | undefined;
} | {
display: string;
val: string;

View file

@ -26,8 +26,6 @@ import { AggTypesRegistryStart } from './agg_types_registry';
import { mockDataServices, mockAggTypesRegistry } from './test_helpers';
import { Field as IndexPatternField, IndexPattern } from '../../index_patterns';
import { stubIndexPatternWithFields } from '../../../public/stubs';
import { dataPluginMock } from '../../../public/mocks';
import { setFieldFormats } from '../../../public/services';
describe('AggConfig', () => {
let indexPattern: IndexPattern;
@ -400,13 +398,6 @@ describe('AggConfig', () => {
describe('#fieldFormatter - custom getFormat handler', () => {
it('returns formatter from getFormat handler', () => {
setFieldFormats({
...dataPluginMock.createStartContract().fieldFormats,
getDefaultInstance: jest.fn().mockImplementation(() => ({
getConverterFor: jest.fn().mockImplementation(() => (t: string) => t),
})) as any,
});
const ac = new AggConfigs(indexPattern, [], { typesRegistry });
const configStates = {
enabled: true,
@ -429,12 +420,6 @@ describe('AggConfig', () => {
let aggConfig: AggConfig;
beforeEach(() => {
setFieldFormats({
...dataPluginMock.createStartContract().fieldFormats,
getDefaultInstance: jest.fn().mockImplementation(() => ({
getConverterFor: (t?: string) => t || identity,
})) as any,
});
indexPattern.fields.getByName = name =>
({
format: {

View file

@ -22,12 +22,22 @@ import { BaseParamType } from './param_types/base';
import { FieldParamType } from './param_types/field';
import { OptionedParamType } from './param_types/optioned';
import { AggParamType } from '../aggs/param_types/agg';
import { fieldFormatsServiceMock } from '../../field_formats/mocks';
import { notificationServiceMock } from '../../../../../../src/core/public/mocks';
import { AggTypeDependencies } from './agg_type';
describe('AggParams class', () => {
const aggTypesDependencies: AggTypeDependencies = {
getInternalStartServices: () => ({
fieldFormats: fieldFormatsServiceMock.createStartContract(),
notifications: notificationServiceMock.createStartContract(),
}),
};
describe('constructor args', () => {
it('accepts an array of param defs', () => {
const params = [{ name: 'one' }, { name: 'two' }] as AggParamType[];
const aggParams = initParams(params);
const aggParams = initParams(params, aggTypesDependencies);
expect(aggParams).toHaveLength(params.length);
expect(Array.isArray(aggParams)).toBeTruthy();
@ -37,7 +47,7 @@ describe('AggParams class', () => {
describe('AggParam creation', () => {
it('Uses the FieldParamType class for params with the name "field"', () => {
const params = [{ name: 'field', type: 'field' }] as AggParamType[];
const aggParams = initParams(params);
const aggParams = initParams(params, aggTypesDependencies);
expect(aggParams).toHaveLength(params.length);
expect(aggParams[0] instanceof FieldParamType).toBeTruthy();
@ -50,7 +60,7 @@ describe('AggParams class', () => {
type: 'optioned',
},
] as AggParamType[];
const aggParams = initParams(params);
const aggParams = initParams(params, aggTypesDependencies);
expect(aggParams).toHaveLength(params.length);
expect(aggParams[0] instanceof OptionedParamType).toBeTruthy();
@ -72,7 +82,7 @@ describe('AggParams class', () => {
},
] as AggParamType[];
const aggParams = initParams(params);
const aggParams = initParams(params, aggTypesDependencies);
expect(aggParams).toHaveLength(params.length);

View file

@ -26,6 +26,7 @@ import { BaseParamType } from './param_types/base';
import { AggConfig } from './agg_config';
import { IAggConfigs } from './agg_configs';
import { AggTypeDependencies } from './agg_type';
const paramTypeMap = {
field: FieldParamType,
@ -45,12 +46,13 @@ export interface AggParamOption {
}
export const initParams = <TAggParam extends AggParamType = AggParamType>(
params: TAggParam[]
params: TAggParam[],
{ getInternalStartServices }: AggTypeDependencies
): TAggParam[] =>
params.map((config: TAggParam) => {
const Class = paramTypeMap[config.type] || paramTypeMap._default;
return new Class(config);
return new Class(config, { getInternalStartServices });
}) as TAggParam[];
/**

View file

@ -17,40 +17,49 @@
* under the License.
*/
import { AggType, AggTypeConfig } from './agg_type';
import { AggType, AggTypeConfig, AggTypeDependencies } from './agg_type';
import { IAggConfig } from './agg_config';
import { mockDataServices } from './test_helpers';
import { dataPluginMock } from '../../../public/mocks';
import { setFieldFormats } from '../../../public/services';
import { fieldFormatsServiceMock } from '../../field_formats/mocks';
import { notificationServiceMock } from '../../../../../../src/core/public/mocks';
describe('AggType Class', () => {
let dependencies: AggTypeDependencies;
beforeEach(() => {
mockDataServices();
dependencies = {
getInternalStartServices: () => ({
fieldFormats: {
...fieldFormatsServiceMock.createStartContract(),
getDefaultInstance: jest.fn(() => 'default') as any,
},
notifications: notificationServiceMock.createStartContract(),
}),
};
});
describe('constructor', () => {
it("requires a valid config object as it's first param", () => {
test("requires a valid config object as it's first param", () => {
expect(() => {
const aggConfig: AggTypeConfig = (undefined as unknown) as AggTypeConfig;
new AggType(aggConfig);
new AggType(aggConfig, dependencies);
}).toThrowError();
});
describe('application of config properties', () => {
it('assigns the config value to itself', () => {
test('assigns the config value to itself', () => {
const config: AggTypeConfig = {
name: 'name',
title: 'title',
};
const aggType = new AggType(config);
const aggType = new AggType(config, dependencies);
expect(aggType.name).toBe('name');
expect(aggType.title).toBe('title');
});
describe('makeLabel', () => {
it('makes a function when the makeLabel config is not specified', () => {
test('makes a function when the makeLabel config is not specified', () => {
const makeLabel = () => 'label';
const aggConfig = {} as IAggConfig;
const config: AggTypeConfig = {
@ -59,7 +68,7 @@ describe('AggType Class', () => {
makeLabel,
};
const aggType = new AggType(config);
const aggType = new AggType(config, dependencies);
expect(aggType.makeLabel).toBe(makeLabel);
expect(aggType.makeLabel(aggConfig)).toBe('label');
@ -67,26 +76,32 @@ describe('AggType Class', () => {
});
describe('getResponseAggs/getRequestAggs', () => {
it('copies the value', () => {
test('copies the value', () => {
const testConfig = (aggConfig: IAggConfig) => [aggConfig];
const aggType = new AggType({
name: 'name',
title: 'title',
getResponseAggs: testConfig,
getRequestAggs: testConfig,
});
const aggType = new AggType(
{
name: 'name',
title: 'title',
getResponseAggs: testConfig,
getRequestAggs: testConfig,
},
dependencies
);
expect(aggType.getResponseAggs).toBe(testConfig);
expect(aggType.getResponseAggs).toBe(testConfig);
});
it('defaults to noop', () => {
test('defaults to noop', () => {
const aggConfig = {} as IAggConfig;
const aggType = new AggType({
name: 'name',
title: 'title',
});
const aggType = new AggType(
{
name: 'name',
title: 'title',
},
dependencies
);
const responseAggs = aggType.getRequestAggs(aggConfig);
expect(responseAggs).toBe(undefined);
@ -94,11 +109,14 @@ describe('AggType Class', () => {
});
describe('params', () => {
it('defaults to AggParams object with JSON param', () => {
const aggType = new AggType({
name: 'smart agg',
title: 'title',
});
test('defaults to AggParams object with JSON param', () => {
const aggType = new AggType(
{
name: 'smart agg',
title: 'title',
},
dependencies
);
expect(Array.isArray(aggType.params)).toBeTruthy();
expect(aggType.params.length).toBe(2);
@ -106,26 +124,32 @@ describe('AggType Class', () => {
expect(aggType.params[1].name).toBe('customLabel');
});
it('can disable customLabel', () => {
const aggType = new AggType({
name: 'smart agg',
title: 'title',
customLabels: false,
});
test('can disable customLabel', () => {
const aggType = new AggType(
{
name: 'smart agg',
title: 'title',
customLabels: false,
},
dependencies
);
expect(aggType.params.length).toBe(1);
expect(aggType.params[0].name).toBe('json');
});
it('passes the params arg directly to the AggParams constructor', () => {
test('passes the params arg directly to the AggParams constructor', () => {
const params = [{ name: 'one' }, { name: 'two' }];
const paramLength = params.length + 2; // json and custom label are always appended
const aggType = new AggType({
name: 'bucketeer',
title: 'title',
params,
});
const aggType = new AggType(
{
name: 'bucketeer',
title: 'title',
params,
},
dependencies
);
expect(Array.isArray(aggType.params)).toBeTruthy();
expect(aggType.params.length).toBe(paramLength);
@ -143,11 +167,14 @@ describe('AggType Class', () => {
} as unknown) as IAggConfig;
});
it('returns the formatter for the aggConfig', () => {
const aggType = new AggType({
name: 'name',
title: 'title',
});
test('returns the formatter for the aggConfig', () => {
const aggType = new AggType(
{
name: 'name',
title: 'title',
},
dependencies
);
field = {
format: 'format',
@ -156,16 +183,14 @@ describe('AggType Class', () => {
expect(aggType.getFormat(aggConfig)).toBe('format');
});
it('returns default formatter', () => {
setFieldFormats({
...dataPluginMock.createStartContract().fieldFormats,
getDefaultInstance: jest.fn(() => 'default') as any,
});
const aggType = new AggType({
name: 'name',
title: 'title',
});
test('returns default formatter', () => {
const aggType = new AggType(
{
name: 'name',
title: 'title',
},
dependencies
);
field = undefined;

View file

@ -28,7 +28,7 @@ import { BaseParamType } from './param_types/base';
import { AggParamType } from './param_types/agg';
import { KBN_FIELD_TYPES, IFieldFormat } from '../../../common';
import { ISearchSource } from '../search_source';
import { getFieldFormats } from '../../../public/services';
import { GetInternalStartServicesFn } from '../../types';
export interface AggTypeConfig<
TAggConfig extends AggConfig = AggConfig,
@ -60,16 +60,13 @@ export interface AggTypeConfig<
getKey?: (bucket: any, key: any, agg: TAggConfig) => any;
}
const getFormat = (agg: AggConfig) => {
const field = agg.getField();
const fieldFormatsService = getFieldFormats();
return field ? field.format : fieldFormatsService.getDefaultInstance(KBN_FIELD_TYPES.STRING);
};
// TODO need to make a more explicit interface for this
export type IAggType = AggType;
export interface AggTypeDependencies {
getInternalStartServices: GetInternalStartServicesFn;
}
export class AggType<
TAggConfig extends AggConfig = AggConfig,
TParam extends AggParamType<TAggConfig> = AggParamType<TAggConfig>
@ -215,7 +212,10 @@ export class AggType<
* @private
* @param {object} config - used to set the properties of the AggType
*/
constructor(config: AggTypeConfig<TAggConfig>) {
constructor(
config: AggTypeConfig<TAggConfig>,
{ getInternalStartServices }: AggTypeDependencies
) {
this.name = config.name;
this.type = config.type || 'metrics';
this.dslName = config.dslName || config.name;
@ -251,14 +251,22 @@ export class AggType<
});
}
this.params = initParams(params);
this.params = initParams(params, { getInternalStartServices });
}
this.getRequestAggs = config.getRequestAggs || noop;
this.getResponseAggs = config.getResponseAggs || (() => {});
this.decorateAggConfig = config.decorateAggConfig || (() => ({}));
this.postFlightRequest = config.postFlightRequest || identity;
this.getFormat = config.getFormat || getFormat;
this.getFormat =
config.getFormat ||
((agg: TAggConfig) => {
const field = agg.getField();
const { fieldFormats } = getInternalStartServices();
return field ? field.format : fieldFormats.getDefaultInstance(KBN_FIELD_TYPES.STRING);
});
this.getValue = config.getValue || ((agg: TAggConfig, bucket: any) => {});
}
}

View file

@ -17,83 +17,89 @@
* under the License.
*/
import { IUiSettingsClient, NotificationsSetup } from 'src/core/public';
import { IUiSettingsClient } from 'src/core/public';
import { QuerySetup } from '../../query/query_service';
import { countMetricAgg } from './metrics/count';
import { avgMetricAgg } from './metrics/avg';
import { sumMetricAgg } from './metrics/sum';
import { medianMetricAgg } from './metrics/median';
import { minMetricAgg } from './metrics/min';
import { maxMetricAgg } from './metrics/max';
import { topHitMetricAgg } from './metrics/top_hit';
import { stdDeviationMetricAgg } from './metrics/std_deviation';
import { cardinalityMetricAgg } from './metrics/cardinality';
import { percentilesMetricAgg } from './metrics/percentiles';
import { geoBoundsMetricAgg } from './metrics/geo_bounds';
import { geoCentroidMetricAgg } from './metrics/geo_centroid';
import { percentileRanksMetricAgg } from './metrics/percentile_ranks';
import { derivativeMetricAgg } from './metrics/derivative';
import { cumulativeSumMetricAgg } from './metrics/cumulative_sum';
import { movingAvgMetricAgg } from './metrics/moving_avg';
import { serialDiffMetricAgg } from './metrics/serial_diff';
import { getCountMetricAgg } from './metrics/count';
import { getAvgMetricAgg } from './metrics/avg';
import { getSumMetricAgg } from './metrics/sum';
import { getMedianMetricAgg } from './metrics/median';
import { getMinMetricAgg } from './metrics/min';
import { getMaxMetricAgg } from './metrics/max';
import { getTopHitMetricAgg } from './metrics/top_hit';
import { getStdDeviationMetricAgg } from './metrics/std_deviation';
import { getCardinalityMetricAgg } from './metrics/cardinality';
import { getPercentilesMetricAgg } from './metrics/percentiles';
import { getGeoBoundsMetricAgg } from './metrics/geo_bounds';
import { getGeoCentroidMetricAgg } from './metrics/geo_centroid';
import { getPercentileRanksMetricAgg } from './metrics/percentile_ranks';
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 { rangeBucketAgg } from './buckets/range';
import { getRangeBucketAgg } from './buckets/range';
import { getDateRangeBucketAgg } from './buckets/date_range';
import { ipRangeBucketAgg } from './buckets/ip_range';
import { termsBucketAgg } from './buckets/terms';
import { filterBucketAgg } from './buckets/filter';
import { getIpRangeBucketAgg } from './buckets/ip_range';
import { getTermsBucketAgg } from './buckets/terms';
import { getFilterBucketAgg } from './buckets/filter';
import { getFiltersBucketAgg } from './buckets/filters';
import { significantTermsBucketAgg } from './buckets/significant_terms';
import { geoHashBucketAgg } from './buckets/geo_hash';
import { geoTileBucketAgg } from './buckets/geo_tile';
import { bucketSumMetricAgg } from './metrics/bucket_sum';
import { bucketAvgMetricAgg } from './metrics/bucket_avg';
import { bucketMinMetricAgg } from './metrics/bucket_min';
import { bucketMaxMetricAgg } from './metrics/bucket_max';
import { getSignificantTermsBucketAgg } from './buckets/significant_terms';
import { getGeoHashBucketAgg } from './buckets/geo_hash';
import { getGeoTitleBucketAgg } from './buckets/geo_tile';
import { getBucketSumMetricAgg } from './metrics/bucket_sum';
import { getBucketAvgMetricAgg } from './metrics/bucket_avg';
import { getBucketMinMetricAgg } from './metrics/bucket_min';
import { getBucketMaxMetricAgg } from './metrics/bucket_max';
import { GetInternalStartServicesFn } from '../../types';
export interface AggTypesDependencies {
notifications: NotificationsSetup;
uiSettings: IUiSettingsClient;
query: QuerySetup;
getInternalStartServices: GetInternalStartServicesFn;
}
export const getAggTypes = ({ notifications, uiSettings, query }: AggTypesDependencies) => ({
export const getAggTypes = ({
uiSettings,
query,
getInternalStartServices,
}: AggTypesDependencies) => ({
metrics: [
countMetricAgg,
avgMetricAgg,
sumMetricAgg,
medianMetricAgg,
minMetricAgg,
maxMetricAgg,
stdDeviationMetricAgg,
cardinalityMetricAgg,
percentilesMetricAgg,
percentileRanksMetricAgg,
topHitMetricAgg,
derivativeMetricAgg,
cumulativeSumMetricAgg,
movingAvgMetricAgg,
serialDiffMetricAgg,
bucketAvgMetricAgg,
bucketSumMetricAgg,
bucketMinMetricAgg,
bucketMaxMetricAgg,
geoBoundsMetricAgg,
geoCentroidMetricAgg,
getCountMetricAgg({ getInternalStartServices }),
getAvgMetricAgg({ getInternalStartServices }),
getSumMetricAgg({ getInternalStartServices }),
getMedianMetricAgg({ getInternalStartServices }),
getMinMetricAgg({ getInternalStartServices }),
getMaxMetricAgg({ getInternalStartServices }),
getStdDeviationMetricAgg({ getInternalStartServices }),
getCardinalityMetricAgg({ getInternalStartServices }),
getPercentilesMetricAgg({ getInternalStartServices }),
getPercentileRanksMetricAgg({ getInternalStartServices }),
getTopHitMetricAgg({ getInternalStartServices }),
getDerivativeMetricAgg({ getInternalStartServices }),
getCumulativeSumMetricAgg({ getInternalStartServices }),
getMovingAvgMetricAgg({ getInternalStartServices }),
getSerialDiffMetricAgg({ getInternalStartServices }),
getBucketAvgMetricAgg({ getInternalStartServices }),
getBucketSumMetricAgg({ getInternalStartServices }),
getBucketMinMetricAgg({ getInternalStartServices }),
getBucketMaxMetricAgg({ getInternalStartServices }),
getGeoBoundsMetricAgg({ getInternalStartServices }),
getGeoCentroidMetricAgg({ getInternalStartServices }),
],
buckets: [
getDateHistogramBucketAgg({ uiSettings, query }),
getHistogramBucketAgg({ uiSettings, notifications }),
rangeBucketAgg,
getDateRangeBucketAgg({ uiSettings }),
ipRangeBucketAgg,
termsBucketAgg,
filterBucketAgg,
getFiltersBucketAgg({ uiSettings }),
significantTermsBucketAgg,
geoHashBucketAgg,
geoTileBucketAgg,
getDateHistogramBucketAgg({ uiSettings, query, getInternalStartServices }),
getHistogramBucketAgg({ uiSettings, getInternalStartServices }),
getRangeBucketAgg({ getInternalStartServices }),
getDateRangeBucketAgg({ uiSettings, getInternalStartServices }),
getIpRangeBucketAgg({ getInternalStartServices }),
getTermsBucketAgg({ getInternalStartServices }),
getFilterBucketAgg({ getInternalStartServices }),
getFiltersBucketAgg({ uiSettings, getInternalStartServices }),
getSignificantTermsBucketAgg({ getInternalStartServices }),
getGeoHashBucketAgg({ getInternalStartServices }),
getGeoTitleBucketAgg({ getInternalStartServices }),
],
});

View file

@ -22,7 +22,7 @@ import {
AggTypesRegistrySetup,
AggTypesRegistryStart,
} from './agg_types_registry';
import { BucketAggType } from './buckets/_bucket_agg_type';
import { BucketAggType } from './buckets/bucket_agg_type';
import { MetricAggType } from './metrics/metric_agg_type';
const bucketType = { name: 'terms', type: 'bucket' } as BucketAggType<any>;

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { BucketAggType } from './buckets/_bucket_agg_type';
import { BucketAggType } from './buckets/bucket_agg_type';
import { MetricAggType } from './metrics/metric_agg_type';
export type AggTypesRegistrySetup = ReturnType<AggTypesRegistry['setup']>;

View file

@ -18,7 +18,7 @@
*/
import { i18n } from '@kbn/i18n';
import { IBucketAggConfig } from './_bucket_agg_type';
import { IBucketAggConfig } from './bucket_agg_type';
export const intervalOptions = [
{

View file

@ -24,8 +24,8 @@ import {
} from './_terms_other_bucket_helper';
import { AggConfigs, CreateAggConfigParams } from '../agg_configs';
import { BUCKET_TYPES } from './bucket_agg_types';
import { IBucketAggConfig } from './_bucket_agg_type';
import { mockDataServices, mockAggTypesRegistry } from '../test_helpers';
import { IBucketAggConfig } from './bucket_agg_type';
import { mockAggTypesRegistry } from '../test_helpers';
const indexPattern = {
id: '1234',
@ -223,10 +223,6 @@ describe('Terms Agg Other bucket helper', () => {
return new AggConfigs(indexPattern, [...aggs], { typesRegistry });
};
beforeEach(() => {
mockDataServices();
});
describe('buildOtherBucketAgg', () => {
test('returns a function', () => {
const aggConfigs = getAggConfigs(singleTerm.aggs);

View file

@ -21,7 +21,7 @@ import { isNumber, keys, values, find, each, cloneDeep, flatten } from 'lodash';
import { buildExistsFilter, buildPhrasesFilter, buildQueryFromFilters } from '../../../../common';
import { AggGroupNames } from '../agg_groups';
import { IAggConfigs } from '../agg_configs';
import { IBucketAggConfig } from './_bucket_agg_type';
import { IBucketAggConfig } from './bucket_agg_type';
/**
* walks the aggregation DSL and returns DSL starting at aggregation with id of startFromAggId

View file

@ -21,6 +21,7 @@ import { IAggConfig } from '../agg_config';
import { KBN_FIELD_TYPES } from '../../../../common';
import { AggType, AggTypeConfig } from '../agg_type';
import { AggParamType } from '../param_types/agg';
import { GetInternalStartServicesFn } from '../../../types';
export interface IBucketAggConfig extends IAggConfig {
type: InstanceType<typeof BucketAggType>;
@ -39,6 +40,10 @@ interface BucketAggTypeConfig<TBucketAggConfig extends IAggConfig>
getKey?: (bucket: any, key: any, agg: IAggConfig) => any;
}
interface BucketAggTypeDependencies {
getInternalStartServices: GetInternalStartServicesFn;
}
export class BucketAggType<TBucketAggConfig extends IAggConfig = IBucketAggConfig> extends AggType<
TBucketAggConfig,
BucketAggParam<TBucketAggConfig>
@ -46,8 +51,11 @@ export class BucketAggType<TBucketAggConfig extends IAggConfig = IBucketAggConfi
getKey: (bucket: any, key: any, agg: TBucketAggConfig) => any;
type = bucketType;
constructor(config: BucketAggTypeConfig<TBucketAggConfig>) {
super(config);
constructor(
config: BucketAggTypeConfig<TBucketAggConfig>,
dependencies: BucketAggTypeDependencies
) {
super(config, dependencies);
this.getKey =
config.getKey ||

View file

@ -29,8 +29,9 @@ import {
} from '../date_histogram';
import { BUCKET_TYPES } from '../bucket_agg_types';
import { RangeFilter } from '../../../../../common';
import { coreMock } from '../../../../../../../core/public/mocks';
import { coreMock, notificationServiceMock } from '../../../../../../../core/public/mocks';
import { queryServiceMock } from '../../../../query/mocks';
import { fieldFormatsServiceMock } from '../../../../field_formats/mocks';
describe('AggConfig Filters', () => {
describe('date_histogram', () => {
@ -46,6 +47,10 @@ describe('AggConfig Filters', () => {
aggTypesDependencies = {
uiSettings,
query: queryServiceMock.createSetupContract(),
getInternalStartServices: () => ({
fieldFormats: fieldFormatsServiceMock.createStartContract(),
notifications: notificationServiceMock.createStartContract(),
}),
};
mockDataServices();
@ -90,7 +95,7 @@ describe('AggConfig Filters', () => {
filter = createFilterDateHistogram(agg, bucketKey);
};
it('creates a valid range filter', () => {
test('creates a valid range filter', () => {
init();
expect(filter).toHaveProperty('range');
@ -110,7 +115,7 @@ describe('AggConfig Filters', () => {
expect(filter.meta).toHaveProperty('index', '1234');
});
it('extends the filter edge to 1ms before the next bucket for all interval options', () => {
test('extends the filter edge to 1ms before the next bucket for all interval options', () => {
intervalOptions.forEach(option => {
let duration;
if (option.val !== 'custom' && moment(1, option.val).isValid()) {

View file

@ -25,8 +25,9 @@ import { DateFormat } from '../../../../field_formats';
import { AggConfigs } from '../../agg_configs';
import { mockAggTypesRegistry } from '../../test_helpers';
import { BUCKET_TYPES } from '../bucket_agg_types';
import { IBucketAggConfig } from '../_bucket_agg_type';
import { coreMock } from '../../../../../../../core/public/mocks';
import { IBucketAggConfig } from '../bucket_agg_type';
import { coreMock, notificationServiceMock } from '../../../../../../../core/public/mocks';
import { fieldFormatsServiceMock } from '../../../../field_formats/mocks';
describe('AggConfig Filters', () => {
describe('Date range', () => {
@ -37,6 +38,10 @@ describe('AggConfig Filters', () => {
aggTypesDependencies = {
uiSettings,
getInternalStartServices: () => ({
fieldFormats: fieldFormatsServiceMock.createStartContract(),
notifications: notificationServiceMock.createStartContract(),
}),
};
});
@ -71,7 +76,7 @@ describe('AggConfig Filters', () => {
);
};
it('should return a range filter for date_range agg', () => {
test('should return a range filter for date_range agg', () => {
const aggConfigs = getAggConfigs();
const from = new Date('1 Feb 2015');
const to = new Date('7 Feb 2015');

View file

@ -18,7 +18,7 @@
*/
import moment from 'moment';
import { IBucketAggConfig } from '../_bucket_agg_type';
import { IBucketAggConfig } from '../bucket_agg_type';
import { DateRangeKey } from '../lib/date_range';
import { buildRangeFilter, RangeFilterParams } from '../../../../../common';

View file

@ -21,8 +21,9 @@ import { getFiltersBucketAgg, FiltersBucketAggDependencies } from '../filters';
import { createFilterFilters } from './filters';
import { AggConfigs } from '../../agg_configs';
import { mockAggTypesRegistry } from '../../test_helpers';
import { IBucketAggConfig } from '../_bucket_agg_type';
import { coreMock } from '../../../../../../../core/public/mocks';
import { IBucketAggConfig } from '../bucket_agg_type';
import { coreMock, notificationServiceMock } from '../../../../../../../core/public/mocks';
import { fieldFormatsServiceMock } from '../../../../field_formats/mocks';
describe('AggConfig Filters', () => {
describe('filters', () => {
@ -33,6 +34,10 @@ describe('AggConfig Filters', () => {
aggTypesDependencies = {
uiSettings,
getInternalStartServices: () => ({
fieldFormats: fieldFormatsServiceMock.createStartContract(),
notifications: notificationServiceMock.createStartContract(),
}),
};
});
@ -67,7 +72,8 @@ describe('AggConfig Filters', () => {
{ typesRegistry: mockAggTypesRegistry([getFiltersBucketAgg(aggTypesDependencies)]) }
);
};
it('should return a filters filter', () => {
test('should return a filters filter', () => {
const aggConfigs = getAggConfigs();
const filter = createFilterFilters(aggConfigs.aggs[0] as IBucketAggConfig, 'type:nginx');

View file

@ -18,7 +18,7 @@
*/
import { get } from 'lodash';
import { IBucketAggConfig } from '../_bucket_agg_type';
import { IBucketAggConfig } from '../bucket_agg_type';
import { buildQueryFilter } from '../../../../../common';
export const createFilterFilters = (aggConfig: IBucketAggConfig, key: string) => {

View file

@ -19,19 +19,13 @@
import { createFilterHistogram } from './histogram';
import { AggConfigs } from '../../agg_configs';
import { mockDataServices, mockAggTypesRegistry } from '../../test_helpers';
import { mockAggTypesRegistry } from '../../test_helpers';
import { BUCKET_TYPES } from '../bucket_agg_types';
import { IBucketAggConfig } from '../_bucket_agg_type';
import { IBucketAggConfig } from '../bucket_agg_type';
import { BytesFormat, FieldFormatsGetConfigFn } from '../../../../../common';
describe('AggConfig Filters', () => {
describe('histogram', () => {
beforeEach(() => {
mockDataServices();
});
const typesRegistry = mockAggTypesRegistry();
const getConfig = (() => {}) as FieldFormatsGetConfigFn;
const getAggConfigs = () => {
const field = {
@ -61,11 +55,11 @@ describe('AggConfig Filters', () => {
},
},
],
{ typesRegistry }
{ typesRegistry: mockAggTypesRegistry() }
);
};
it('should return an range filter for histogram', () => {
test('should return an range filter for histogram', () => {
const aggConfigs = getAggConfigs();
const filter = createFilterHistogram(aggConfigs.aggs[0] as IBucketAggConfig, '2048');

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { IBucketAggConfig } from '../_bucket_agg_type';
import { IBucketAggConfig } from '../bucket_agg_type';
import { buildRangeFilter, RangeFilterParams } from '../../../../../common';
export const createFilterHistogram = (aggConfig: IBucketAggConfig, key: string) => {

View file

@ -17,17 +17,26 @@
* under the License.
*/
import { ipRangeBucketAgg } from '../ip_range';
import { getIpRangeBucketAgg } from '../ip_range';
import { createFilterIpRange } from './ip_range';
import { AggConfigs, CreateAggConfigParams } from '../../agg_configs';
import { mockAggTypesRegistry } from '../../test_helpers';
import { IpFormat } from '../../../../../common';
import { BUCKET_TYPES } from '../bucket_agg_types';
import { IBucketAggConfig } from '../_bucket_agg_type';
import { IBucketAggConfig } from '../bucket_agg_type';
import { fieldFormatsServiceMock } from '../../../../field_formats/mocks';
import { notificationServiceMock } from '../../../../../../../core/public/mocks';
describe('AggConfig Filters', () => {
describe('IP range', () => {
const typesRegistry = mockAggTypesRegistry([ipRangeBucketAgg]);
const typesRegistry = mockAggTypesRegistry([
getIpRangeBucketAgg({
getInternalStartServices: () => ({
fieldFormats: fieldFormatsServiceMock.createStartContract(),
notifications: notificationServiceMock.createStartContract(),
}),
}),
]);
const getAggConfigs = (aggs: CreateAggConfigParams[]) => {
const field = {
name: 'ip',
@ -46,7 +55,7 @@ describe('AggConfig Filters', () => {
return new AggConfigs(indexPattern, aggs, { typesRegistry });
};
it('should return a range filter for ip_range agg', () => {
test('should return a range filter for ip_range agg', () => {
const aggConfigs = getAggConfigs([
{
type: BUCKET_TYPES.IP_RANGE,
@ -75,7 +84,7 @@ describe('AggConfig Filters', () => {
expect(filter.range.ip).toHaveProperty('lte', '1.1.1.1');
});
it('should return a range filter for ip_range agg using a CIDR mask', () => {
test('should return a range filter for ip_range agg using a CIDR mask', () => {
const aggConfigs = getAggConfigs([
{
type: BUCKET_TYPES.IP_RANGE,

View file

@ -18,7 +18,7 @@
*/
import { CidrMask } from '../lib/cidr_mask';
import { IBucketAggConfig } from '../_bucket_agg_type';
import { IBucketAggConfig } from '../bucket_agg_type';
import { IpRangeKey } from '../lib/ip_range';
import { buildRangeFilter, RangeFilterParams } from '../../../../../common';

View file

@ -17,22 +17,31 @@
* under the License.
*/
import { rangeBucketAgg } from '../range';
import { getRangeBucketAgg, RangeBucketAggDependencies } from '../range';
import { createFilterRange } from './range';
import { BytesFormat, FieldFormatsGetConfigFn } from '../../../../../common';
import { AggConfigs } from '../../agg_configs';
import { mockDataServices, mockAggTypesRegistry } from '../../test_helpers';
import { BUCKET_TYPES } from '../bucket_agg_types';
import { IBucketAggConfig } from '../_bucket_agg_type';
import { IBucketAggConfig } from '../bucket_agg_type';
import { fieldFormatsServiceMock } from '../../../../field_formats/mocks';
import { notificationServiceMock } from '../../../../../../../core/public/mocks';
describe('AggConfig Filters', () => {
describe('range', () => {
let aggTypesDependencies: RangeBucketAggDependencies;
beforeEach(() => {
aggTypesDependencies = {
getInternalStartServices: () => ({
fieldFormats: fieldFormatsServiceMock.createStartContract(),
notifications: notificationServiceMock.createStartContract(),
}),
};
mockDataServices();
});
const typesRegistry = mockAggTypesRegistry([rangeBucketAgg]);
const getConfig = (() => {}) as FieldFormatsGetConfigFn;
const getAggConfigs = () => {
const field = {
@ -62,11 +71,11 @@ describe('AggConfig Filters', () => {
},
},
],
{ typesRegistry }
{ typesRegistry: mockAggTypesRegistry([getRangeBucketAgg(aggTypesDependencies)]) }
);
};
it('should return a range filter for range agg', () => {
test('should return a range filter for range agg', () => {
const aggConfigs = getAggConfigs();
const filter = createFilterRange(aggConfigs.aggs[0] as IBucketAggConfig, {
gte: 1024,

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { IBucketAggConfig } from '../_bucket_agg_type';
import { IBucketAggConfig } from '../bucket_agg_type';
import { buildRangeFilter } from '../../../../../common';
export const createFilterRange = (aggConfig: IBucketAggConfig, params: any) => {

View file

@ -17,17 +17,30 @@
* under the License.
*/
import { termsBucketAgg } from '../terms';
import { getTermsBucketAgg } from '../terms';
import { createFilterTerms } from './terms';
import { AggConfigs, CreateAggConfigParams } from '../../agg_configs';
import { mockAggTypesRegistry } from '../../test_helpers';
import { BUCKET_TYPES } from '../bucket_agg_types';
import { IBucketAggConfig } from '../_bucket_agg_type';
import { IBucketAggConfig } from '../bucket_agg_type';
import { Filter, ExistsFilter } from '../../../../../common';
import { RangeBucketAggDependencies } from '../range';
import { fieldFormatsServiceMock } from '../../../../field_formats/mocks';
import { notificationServiceMock } from '../../../../../../../core/public/mocks';
describe('AggConfig Filters', () => {
describe('terms', () => {
const typesRegistry = mockAggTypesRegistry([termsBucketAgg]);
let aggTypesDependencies: RangeBucketAggDependencies;
beforeEach(() => {
aggTypesDependencies = {
getInternalStartServices: () => ({
fieldFormats: fieldFormatsServiceMock.createStartContract(),
notifications: notificationServiceMock.createStartContract(),
}),
};
});
const getAggConfigs = (aggs: CreateAggConfigParams[]) => {
const indexPattern = {
id: '1234',
@ -43,10 +56,12 @@ describe('AggConfig Filters', () => {
indexPattern,
};
return new AggConfigs(indexPattern, aggs, { typesRegistry });
return new AggConfigs(indexPattern, aggs, {
typesRegistry: mockAggTypesRegistry([getTermsBucketAgg(aggTypesDependencies)]),
});
};
it('should return a match_phrase filter for terms', () => {
test('should return a match_phrase filter for terms', () => {
const aggConfigs = getAggConfigs([
{ type: BUCKET_TYPES.TERMS, schema: 'segment', params: { field: 'field' } },
]);
@ -65,7 +80,7 @@ describe('AggConfig Filters', () => {
expect(filter.meta).toHaveProperty('index', '1234');
});
it('should set query to true or false for boolean filter', () => {
test('should set query to true or false for boolean filter', () => {
const aggConfigs = getAggConfigs([
{ type: BUCKET_TYPES.TERMS, schema: 'segment', params: { field: 'field' } },
]);
@ -93,7 +108,7 @@ describe('AggConfig Filters', () => {
expect(filterTrue.query.match_phrase.field).toBeTruthy();
});
it('should generate correct __missing__ filter', () => {
test('should generate correct __missing__ filter', () => {
const aggConfigs = getAggConfigs([
{ type: BUCKET_TYPES.TERMS, schema: 'segment', params: { field: 'field' } },
]);
@ -110,7 +125,7 @@ describe('AggConfig Filters', () => {
expect(filter.meta).toHaveProperty('negate', true);
});
it('should generate correct __other__ filter', () => {
test('should generate correct __other__ filter', () => {
const aggConfigs = getAggConfigs([
{ type: BUCKET_TYPES.TERMS, schema: 'segment', params: { field: 'field' } },
]);

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { IBucketAggConfig } from '../_bucket_agg_type';
import { IBucketAggConfig } from '../bucket_agg_type';
import {
buildPhrasesFilter,
buildExistsFilter,

View file

@ -23,7 +23,7 @@ import { i18n } from '@kbn/i18n';
import { IUiSettingsClient } from 'src/core/public';
import { TimeBuckets } from './lib/time_buckets';
import { BucketAggType, IBucketAggConfig } from './_bucket_agg_type';
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';
@ -33,8 +33,8 @@ import { isMetricAggType } from '../metrics/metric_agg_type';
import { FIELD_FORMAT_IDS, KBN_FIELD_TYPES } from '../../../../common';
import { TimefilterContract } from '../../../query';
import { getFieldFormats } from '../../../../public/services';
import { QuerySetup } from '../../../query/query_service';
import { GetInternalStartServicesFn } from '../../../types';
const detectedTimezone = moment.tz.guess();
const tzOffset = moment().format('Z');
@ -61,6 +61,7 @@ interface ITimeBuckets {
export interface DateHistogramBucketAggDependencies {
uiSettings: IUiSettingsClient;
query: QuerySetup;
getInternalStartServices: GetInternalStartServicesFn;
}
export interface IBucketDateHistogramAggConfig extends IBucketAggConfig {
@ -74,211 +75,218 @@ export function isDateHistogramBucketAggConfig(agg: any): agg is IBucketDateHist
export const getDateHistogramBucketAgg = ({
uiSettings,
query,
getInternalStartServices,
}: DateHistogramBucketAggDependencies) =>
new BucketAggType<IBucketDateHistogramAggConfig>({
name: BUCKET_TYPES.DATE_HISTOGRAM,
title: i18n.translate('data.search.aggs.buckets.dateHistogramTitle', {
defaultMessage: 'Date Histogram',
}),
ordered: {
date: true,
},
makeLabel(agg) {
let output: Record<string, any> = {};
new BucketAggType<IBucketDateHistogramAggConfig>(
{
name: BUCKET_TYPES.DATE_HISTOGRAM,
title: i18n.translate('data.search.aggs.buckets.dateHistogramTitle', {
defaultMessage: 'Date Histogram',
}),
ordered: {
date: true,
},
makeLabel(agg) {
let output: Record<string, any> = {};
if (this.params) {
output = writeParams(this.params, agg);
}
if (this.params) {
output = writeParams(this.params, agg);
}
const field = agg.getFieldDisplayName();
return i18n.translate('data.search.aggs.buckets.dateHistogramLabel', {
defaultMessage: '{fieldName} per {intervalDescription}',
values: {
fieldName: field,
intervalDescription: output.metricScaleText || output.bucketInterval.description,
},
});
},
createFilter: createFilterDateHistogram,
decorateAggConfig() {
let buckets: any;
return {
buckets: {
configurable: true,
get() {
if (buckets) return buckets;
const { timefilter } = query.timefilter;
buckets = new TimeBuckets({ uiSettings });
updateTimeBuckets(this, timefilter, buckets);
return buckets;
const field = agg.getFieldDisplayName();
return i18n.translate('data.search.aggs.buckets.dateHistogramLabel', {
defaultMessage: '{fieldName} per {intervalDescription}',
values: {
fieldName: field,
intervalDescription: output.metricScaleText || output.bucketInterval.description,
},
} as any,
};
},
getFormat(agg) {
const DateFieldFormat = getFieldFormats().getType(FIELD_FORMAT_IDS.DATE);
});
},
createFilter: createFilterDateHistogram,
decorateAggConfig() {
let buckets: any;
if (!DateFieldFormat) {
throw new Error('Unable to retrieve Date Field Format');
}
return {
buckets: {
configurable: true,
get() {
if (buckets) return buckets;
return new DateFieldFormat(
const { timefilter } = query.timefilter;
buckets = new TimeBuckets({ uiSettings });
updateTimeBuckets(this, timefilter, buckets);
return buckets;
},
} as any,
};
},
getFormat(agg) {
const { fieldFormats } = getInternalStartServices();
const DateFieldFormat = fieldFormats.getType(FIELD_FORMAT_IDS.DATE);
if (!DateFieldFormat) {
throw new Error('Unable to retrieve Date Field Format');
}
return new DateFieldFormat(
{
pattern: agg.buckets.getScaledDateFormat(),
},
(key: string) => uiSettings.get(key)
);
},
params: [
{
pattern: agg.buckets.getScaledDateFormat(),
},
(key: string) => uiSettings.get(key)
);
},
params: [
{
name: 'field',
type: 'field',
filterFieldTypes: KBN_FIELD_TYPES.DATE,
default(agg: IBucketDateHistogramAggConfig) {
return agg.getIndexPattern().timeFieldName;
},
onChange(agg: IBucketDateHistogramAggConfig) {
if (get(agg, 'params.interval') === 'auto' && !agg.fieldIsTimeField()) {
delete agg.params.interval;
}
},
},
{
name: 'timeRange',
default: null,
write: noop,
},
{
name: 'useNormalizedEsInterval',
default: true,
write: noop,
},
{
name: 'scaleMetricValues',
default: false,
write: noop,
advanced: true,
},
{
name: 'interval',
deserialize(state: any, agg) {
// For upgrading from 7.0.x to 7.1.x - intervals are now stored as key of options or custom value
if (state === 'custom') {
return get(agg, 'params.customInterval');
}
const interval = find(intervalOptions, { val: state });
// For upgrading from 4.0.x to 4.1.x - intervals are now stored as 'y' instead of 'year',
// but this maps the old values to the new values
if (!interval && state === 'year') {
return 'y';
}
return state;
},
default: 'auto',
options: intervalOptions,
write(agg, output, aggs) {
const { timefilter } = query.timefilter;
updateTimeBuckets(agg, timefilter);
const { useNormalizedEsInterval, scaleMetricValues } = agg.params;
const interval = agg.buckets.getInterval(useNormalizedEsInterval);
output.bucketInterval = interval;
if (interval.expression === '0ms') {
// We are hitting this code a couple of times while configuring in editor
// with an interval of 0ms because the overall time range has not yet been
// set. Since 0ms is not a valid ES interval, we cannot pass it through dateHistogramInterval
// below, since it would throw an exception. So in the cases we still have an interval of 0ms
// here we simply skip the rest of the method and never write an interval into the DSL, since
// this DSL will anyway not be used before we're passing this code with an actual interval.
return;
}
output.params = {
...output.params,
...dateHistogramInterval(interval.expression),
};
const scaleMetrics = scaleMetricValues && interval.scaled && interval.scale < 1;
if (scaleMetrics && aggs) {
const metrics = aggs.aggs.filter(a => isMetricAggType(a.type));
const all = every(metrics, (a: IBucketAggConfig) => {
const { type } = a;
if (isMetricAggType(type)) {
return type.isScalable();
}
});
if (all) {
output.metricScale = interval.scale;
output.metricScaleText = interval.preScaled.description;
name: 'field',
type: 'field',
filterFieldTypes: KBN_FIELD_TYPES.DATE,
default(agg: IBucketDateHistogramAggConfig) {
return agg.getIndexPattern().timeFieldName;
},
onChange(agg: IBucketDateHistogramAggConfig) {
if (get(agg, 'params.interval') === 'auto' && !agg.fieldIsTimeField()) {
delete agg.params.interval;
}
}
},
},
},
{
name: 'time_zone',
default: undefined,
// We don't ever want this parameter to be serialized out (when saving or to URLs)
// since we do all the logic handling it "on the fly" in the `write` method, to prevent
// time_zones being persisted into saved_objects
serialize: noop,
write(agg, output) {
// If a time_zone has been set explicitly always prefer this.
let tz = agg.params.time_zone;
if (!tz && agg.params.field) {
// If a field has been configured check the index pattern's typeMeta if a date_histogram on that
// field requires a specific time_zone
tz = get(agg.getIndexPattern(), [
'typeMeta',
'aggs',
'date_histogram',
agg.params.field.name,
'time_zone',
]);
}
if (!tz) {
// If the index pattern typeMeta data, didn't had a time zone assigned for the selected field use the configured tz
const isDefaultTimezone = uiSettings.isDefault('dateFormat:tz');
tz = isDefaultTimezone ? detectedTimezone || tzOffset : uiSettings.get('dateFormat:tz');
}
output.params.time_zone = tz;
{
name: 'timeRange',
default: null,
write: noop,
},
},
{
name: 'drop_partials',
default: false,
write: noop,
shouldShow: agg => {
const field = agg.params.field;
return field && field.name && field.name === agg.getIndexPattern().timeFieldName;
{
name: 'useNormalizedEsInterval',
default: true,
write: noop,
},
},
{
name: 'format',
},
{
name: 'min_doc_count',
default: 1,
},
{
name: 'extended_bounds',
default: {},
write(agg, output) {
const val = agg.params.extended_bounds;
{
name: 'scaleMetricValues',
default: false,
write: noop,
advanced: true,
},
{
name: 'interval',
deserialize(state: any, agg) {
// For upgrading from 7.0.x to 7.1.x - intervals are now stored as key of options or custom value
if (state === 'custom') {
return get(agg, 'params.customInterval');
}
if (val.min != null || val.max != null) {
output.params.extended_bounds = {
min: moment(val.min).valueOf(),
max: moment(val.max).valueOf(),
const interval = find(intervalOptions, { val: state });
// For upgrading from 4.0.x to 4.1.x - intervals are now stored as 'y' instead of 'year',
// but this maps the old values to the new values
if (!interval && state === 'year') {
return 'y';
}
return state;
},
default: 'auto',
options: intervalOptions,
write(agg, output, aggs) {
const { timefilter } = query.timefilter;
updateTimeBuckets(agg, timefilter);
const { useNormalizedEsInterval, scaleMetricValues } = agg.params;
const interval = agg.buckets.getInterval(useNormalizedEsInterval);
output.bucketInterval = interval;
if (interval.expression === '0ms') {
// We are hitting this code a couple of times while configuring in editor
// with an interval of 0ms because the overall time range has not yet been
// set. Since 0ms is not a valid ES interval, we cannot pass it through dateHistogramInterval
// below, since it would throw an exception. So in the cases we still have an interval of 0ms
// here we simply skip the rest of the method and never write an interval into the DSL, since
// this DSL will anyway not be used before we're passing this code with an actual interval.
return;
}
output.params = {
...output.params,
...dateHistogramInterval(interval.expression),
};
return;
}
const scaleMetrics = scaleMetricValues && interval.scaled && interval.scale < 1;
if (scaleMetrics && aggs) {
const metrics = aggs.aggs.filter(a => isMetricAggType(a.type));
const all = every(metrics, (a: IBucketAggConfig) => {
const { type } = a;
if (isMetricAggType(type)) {
return type.isScalable();
}
});
if (all) {
output.metricScale = interval.scale;
output.metricScaleText = interval.preScaled.description;
}
}
},
},
},
],
});
{
name: 'time_zone',
default: undefined,
// We don't ever want this parameter to be serialized out (when saving or to URLs)
// since we do all the logic handling it "on the fly" in the `write` method, to prevent
// time_zones being persisted into saved_objects
serialize: noop,
write(agg, output) {
// If a time_zone has been set explicitly always prefer this.
let tz = agg.params.time_zone;
if (!tz && agg.params.field) {
// If a field has been configured check the index pattern's typeMeta if a date_histogram on that
// field requires a specific time_zone
tz = get(agg.getIndexPattern(), [
'typeMeta',
'aggs',
'date_histogram',
agg.params.field.name,
'time_zone',
]);
}
if (!tz) {
// If the index pattern typeMeta data, didn't had a time zone assigned for the selected field use the configured tz
const isDefaultTimezone = uiSettings.isDefault('dateFormat:tz');
tz = isDefaultTimezone
? detectedTimezone || tzOffset
: uiSettings.get('dateFormat:tz');
}
output.params.time_zone = tz;
},
},
{
name: 'drop_partials',
default: false,
write: noop,
shouldShow: agg => {
const field = agg.params.field;
return field && field.name && field.name === agg.getIndexPattern().timeFieldName;
},
},
{
name: 'format',
},
{
name: 'min_doc_count',
default: 1,
},
{
name: 'extended_bounds',
default: {},
write(agg, output) {
const val = agg.params.extended_bounds;
if (val.min != null || val.max != null) {
output.params.extended_bounds = {
min: moment(val.min).valueOf(),
max: moment(val.max).valueOf(),
};
return;
}
},
},
],
},
{ getInternalStartServices }
);

View file

@ -17,11 +17,12 @@
* under the License.
*/
import { coreMock } from '../../../../../../../src/core/public/mocks';
import { coreMock, notificationServiceMock } from '../../../../../../../src/core/public/mocks';
import { getDateRangeBucketAgg, DateRangeBucketAggDependencies } from './date_range';
import { AggConfigs } from '../agg_configs';
import { mockAggTypesRegistry } from '../test_helpers';
import { BUCKET_TYPES } from './bucket_agg_types';
import { fieldFormatsServiceMock } from '../../../field_formats/mocks';
describe('date_range params', () => {
let aggTypesDependencies: DateRangeBucketAggDependencies;
@ -31,6 +32,10 @@ describe('date_range params', () => {
aggTypesDependencies = {
uiSettings,
getInternalStartServices: () => ({
fieldFormats: fieldFormatsServiceMock.createStartContract(),
notifications: notificationServiceMock.createStartContract(),
}),
};
});

View file

@ -23,12 +23,12 @@ import { i18n } from '@kbn/i18n';
import { IUiSettingsClient } from 'src/core/public';
import { BUCKET_TYPES } from './bucket_agg_types';
import { BucketAggType, IBucketAggConfig } from './_bucket_agg_type';
import { BucketAggType, IBucketAggConfig } from './bucket_agg_type';
import { createFilterDateRange } from './create_filter/date_range';
import { convertDateRangeToString, DateRangeKey } from './lib/date_range';
import { KBN_FIELD_TYPES, FieldFormat, TEXT_CONTEXT_TYPE } from '../../../../common';
import { getFieldFormats } from '../../../../public/services';
import { GetInternalStartServicesFn } from '../../../types';
const dateRangeTitle = i18n.translate('data.search.aggs.buckets.dateRangeTitle', {
defaultMessage: 'Date Range',
@ -36,76 +36,85 @@ const dateRangeTitle = i18n.translate('data.search.aggs.buckets.dateRangeTitle',
export interface DateRangeBucketAggDependencies {
uiSettings: IUiSettingsClient;
getInternalStartServices: GetInternalStartServicesFn;
}
export const getDateRangeBucketAgg = ({ uiSettings }: DateRangeBucketAggDependencies) =>
new BucketAggType({
name: BUCKET_TYPES.DATE_RANGE,
title: dateRangeTitle,
createFilter: createFilterDateRange,
getKey({ from, to }): DateRangeKey {
return { from, to };
},
getFormat(agg) {
const fieldFormatsService = getFieldFormats();
const formatter = agg.fieldOwnFormatter(
TEXT_CONTEXT_TYPE,
fieldFormatsService.getDefaultInstance(KBN_FIELD_TYPES.DATE)
);
const DateRangeFormat = FieldFormat.from(function(range: DateRangeKey) {
return convertDateRangeToString(range, formatter);
});
return new DateRangeFormat();
},
makeLabel(aggConfig) {
return aggConfig.getFieldDisplayName() + ' date ranges';
},
params: [
{
name: 'field',
type: 'field',
filterFieldTypes: KBN_FIELD_TYPES.DATE,
default(agg: IBucketAggConfig) {
return agg.getIndexPattern().timeFieldName;
},
export const getDateRangeBucketAgg = ({
uiSettings,
getInternalStartServices,
}: DateRangeBucketAggDependencies) =>
new BucketAggType(
{
name: BUCKET_TYPES.DATE_RANGE,
title: dateRangeTitle,
createFilter: createFilterDateRange,
getKey({ from, to }): DateRangeKey {
return { from, to };
},
{
name: 'ranges',
default: [
{
from: 'now-1w/w',
to: 'now',
getFormat(agg) {
const { fieldFormats } = getInternalStartServices();
const formatter = agg.fieldOwnFormatter(
TEXT_CONTEXT_TYPE,
fieldFormats.getDefaultInstance(KBN_FIELD_TYPES.DATE)
);
const DateRangeFormat = FieldFormat.from(function(range: DateRangeKey) {
return convertDateRangeToString(range, formatter);
});
return new DateRangeFormat();
},
makeLabel(aggConfig) {
return aggConfig.getFieldDisplayName() + ' date ranges';
},
params: [
{
name: 'field',
type: 'field',
filterFieldTypes: KBN_FIELD_TYPES.DATE,
default(agg: IBucketAggConfig) {
return agg.getIndexPattern().timeFieldName;
},
],
},
{
name: 'time_zone',
default: undefined,
// Implimentation method is the same as that of date_histogram
serialize: () => undefined,
write: (agg, output) => {
const field = agg.getParam('field');
let tz = agg.getParam('time_zone');
if (!tz && field) {
tz = get(agg.getIndexPattern(), [
'typeMeta',
'aggs',
'date_range',
field.name,
'time_zone',
]);
}
if (!tz) {
const detectedTimezone = moment.tz.guess();
const tzOffset = moment().format('Z');
const isDefaultTimezone = uiSettings.isDefault('dateFormat:tz');
tz = isDefaultTimezone ? detectedTimezone || tzOffset : uiSettings.get('dateFormat:tz');
}
output.params.time_zone = tz;
},
},
],
});
{
name: 'ranges',
default: [
{
from: 'now-1w/w',
to: 'now',
},
],
},
{
name: 'time_zone',
default: undefined,
// Implimentation method is the same as that of date_histogram
serialize: () => undefined,
write: (agg, output) => {
const field = agg.getParam('field');
let tz = agg.getParam('time_zone');
if (!tz && field) {
tz = get(agg.getIndexPattern(), [
'typeMeta',
'aggs',
'date_range',
field.name,
'time_zone',
]);
}
if (!tz) {
const detectedTimezone = moment.tz.guess();
const tzOffset = moment().format('Z');
const isDefaultTimezone = uiSettings.isDefault('dateFormat:tz');
tz = isDefaultTimezone
? detectedTimezone || tzOffset
: uiSettings.get('dateFormat:tz');
}
output.params.time_zone = tz;
},
},
],
},
{ getInternalStartServices }
);

View file

@ -18,19 +18,28 @@
*/
import { i18n } from '@kbn/i18n';
import { BucketAggType } from './_bucket_agg_type';
import { BucketAggType } from './bucket_agg_type';
import { BUCKET_TYPES } from './bucket_agg_types';
import { GetInternalStartServicesFn } from '../../../types';
const filterTitle = i18n.translate('data.search.aggs.buckets.filterTitle', {
defaultMessage: 'Filter',
});
export const filterBucketAgg = new BucketAggType({
name: BUCKET_TYPES.FILTER,
title: filterTitle,
params: [
export interface FilterBucketAggDependencies {
getInternalStartServices: GetInternalStartServicesFn;
}
export const getFilterBucketAgg = ({ getInternalStartServices }: FilterBucketAggDependencies) =>
new BucketAggType(
{
name: 'geo_bounding_box',
name: BUCKET_TYPES.FILTER,
title: filterTitle,
params: [
{
name: 'geo_bounding_box',
},
],
},
],
});
{ getInternalStartServices }
);

View file

@ -23,11 +23,12 @@ import { IUiSettingsClient } from 'src/core/public';
import { createFilterFilters } from './create_filter/filters';
import { toAngularJSON } from '../utils';
import { BucketAggType } from './_bucket_agg_type';
import { BucketAggType } from './bucket_agg_type';
import { BUCKET_TYPES } from './bucket_agg_types';
import { Storage } from '../../../../../../plugins/kibana_utils/public';
import { getEsQueryConfig, buildEsQuery, Query } from '../../../../common';
import { getQueryLog } from '../../../query';
import { GetInternalStartServicesFn } from '../../../types';
const filtersTitle = i18n.translate('data.search.aggs.buckets.filtersTitle', {
defaultMessage: 'Filters',
@ -43,69 +44,81 @@ interface FilterValue {
export interface FiltersBucketAggDependencies {
uiSettings: IUiSettingsClient;
getInternalStartServices: GetInternalStartServicesFn;
}
export const getFiltersBucketAgg = ({ uiSettings }: FiltersBucketAggDependencies) =>
new BucketAggType({
name: BUCKET_TYPES.FILTERS,
title: filtersTitle,
createFilter: createFilterFilters,
customLabels: false,
params: [
{
name: 'filters',
default: [
{ input: { query: '', language: uiSettings.get('search:queryLanguage') }, label: '' },
],
write(aggConfig, output) {
const inFilters: FilterValue[] = aggConfig.params.filters;
if (!size(inFilters)) return;
export const getFiltersBucketAgg = ({
uiSettings,
getInternalStartServices,
}: FiltersBucketAggDependencies) =>
new BucketAggType(
{
name: BUCKET_TYPES.FILTERS,
title: filtersTitle,
createFilter: createFilterFilters,
customLabels: false,
params: [
{
name: 'filters',
default: [
{ input: { query: '', language: uiSettings.get('search:queryLanguage') }, label: '' },
],
write(aggConfig, output) {
const inFilters: FilterValue[] = aggConfig.params.filters;
if (!size(inFilters)) return;
inFilters.forEach(filter => {
const persistedLog = getQueryLog(
uiSettings,
new Storage(window.localStorage),
'vis_default_editor',
filter.input.language
inFilters.forEach(filter => {
const persistedLog = getQueryLog(
uiSettings,
new Storage(window.localStorage),
'vis_default_editor',
filter.input.language
);
persistedLog.add(filter.input.query);
});
const outFilters = transform(
inFilters,
function(filters, filter) {
const input = cloneDeep(filter.input);
if (!input) {
console.log('malformed filter agg params, missing "input" query'); // eslint-disable-line no-console
return;
}
const esQueryConfigs = getEsQueryConfig(uiSettings);
const query = buildEsQuery(
aggConfig.getIndexPattern(),
[input],
[],
esQueryConfigs
);
if (!query) {
console.log('malformed filter agg params, missing "query" on input'); // eslint-disable-line no-console
return;
}
const matchAllLabel = filter.input.query === '' ? '*' : '';
const label =
filter.label ||
matchAllLabel ||
(typeof filter.input.query === 'string'
? filter.input.query
: toAngularJSON(filter.input.query));
filters[label] = { query };
},
{}
);
persistedLog.add(filter.input.query);
});
const outFilters = transform(
inFilters,
function(filters, filter) {
const input = cloneDeep(filter.input);
if (!size(outFilters)) return;
if (!input) {
console.log('malformed filter agg params, missing "input" query'); // eslint-disable-line no-console
return;
}
const esQueryConfigs = getEsQueryConfig(uiSettings);
const query = buildEsQuery(aggConfig.getIndexPattern(), [input], [], esQueryConfigs);
if (!query) {
console.log('malformed filter agg params, missing "query" on input'); // eslint-disable-line no-console
return;
}
const matchAllLabel = filter.input.query === '' ? '*' : '';
const label =
filter.label ||
matchAllLabel ||
(typeof filter.input.query === 'string'
? filter.input.query
: toAngularJSON(filter.input.query));
filters[label] = { query };
},
{}
);
if (!size(outFilters)) return;
const params = output.params || (output.params = {});
params.filters = outFilters;
const params = output.params || (output.params = {});
params.filters = outFilters;
},
},
},
],
});
],
},
{ getInternalStartServices }
);

View file

@ -17,13 +17,29 @@
* under the License.
*/
import { geoHashBucketAgg } from './geo_hash';
import { getGeoHashBucketAgg, GeoHashBucketAggDependencies } from './geo_hash';
import { AggConfigs, IAggConfigs } from '../agg_configs';
import { mockAggTypesRegistry } from '../test_helpers';
import { BUCKET_TYPES } from './bucket_agg_types';
import { IBucketAggConfig } from './_bucket_agg_type';
import { notificationServiceMock } from '../../../../../../../src/core/public/mocks';
import { fieldFormatsServiceMock } from '../../../field_formats/mocks';
import { BucketAggType, IBucketAggConfig } from './bucket_agg_type';
describe('Geohash Agg', () => {
let aggTypesDependencies: GeoHashBucketAggDependencies;
let geoHashBucketAgg: BucketAggType;
beforeEach(() => {
aggTypesDependencies = {
getInternalStartServices: () => ({
fieldFormats: fieldFormatsServiceMock.createStartContract(),
notifications: notificationServiceMock.createStartContract(),
}),
};
geoHashBucketAgg = getGeoHashBucketAgg(aggTypesDependencies);
});
const getAggConfigs = (params?: Record<string, any>) => {
const indexPattern = {
id: '1234',
@ -74,7 +90,7 @@ describe('Geohash Agg', () => {
precisionParam = geoHashBucketAgg.params[PRECISION_PARAM_INDEX];
});
it('should select precision parameter', () => {
test('should select precision parameter', () => {
expect(precisionParam.name).toEqual('precision');
});
});
@ -89,7 +105,7 @@ describe('Geohash Agg', () => {
geoHashGridAgg = aggConfigs.aggs[0] as IBucketAggConfig;
});
it('should create filter, geohash_grid, and geo_centroid aggregations', () => {
test('should create filter, geohash_grid, and geo_centroid aggregations', () => {
const requestAggs = geoHashBucketAgg.getRequestAggs(geoHashGridAgg) as IBucketAggConfig[];
expect(requestAggs.length).toEqual(3);
@ -101,7 +117,7 @@ describe('Geohash Agg', () => {
});
describe('aggregation options', () => {
it('should only create geohash_grid and geo_centroid aggregations when isFilteredByCollar is false', () => {
test('should only create geohash_grid and geo_centroid aggregations when isFilteredByCollar is false', () => {
const aggConfigs = getAggConfigs({ isFilteredByCollar: false });
const requestAggs = geoHashBucketAgg.getRequestAggs(
aggConfigs.aggs[0] as IBucketAggConfig
@ -112,7 +128,7 @@ describe('Geohash Agg', () => {
expect(requestAggs[1].type.name).toEqual('geo_centroid');
});
it('should only create filter and geohash_grid aggregations when useGeocentroid is false', () => {
test('should only create filter and geohash_grid aggregations when useGeocentroid is false', () => {
const aggConfigs = getAggConfigs({ useGeocentroid: false });
const requestAggs = geoHashBucketAgg.getRequestAggs(
aggConfigs.aggs[0] as IBucketAggConfig
@ -138,7 +154,7 @@ describe('Geohash Agg', () => {
) as IBucketAggConfig[];
});
it('should change geo_bounding_box filter aggregation and vis session state when map movement is outside map collar', () => {
test('should change geo_bounding_box filter aggregation and vis session state when map movement is outside map collar', () => {
const [, geoBoxingBox] = geoHashBucketAgg.getRequestAggs(
getAggConfigs({
boundingBox: {
@ -151,7 +167,7 @@ describe('Geohash Agg', () => {
expect(originalRequestAggs[1].params).not.toEqual(geoBoxingBox.params);
});
it('should not change geo_bounding_box filter aggregation and vis session state when map movement is within map collar', () => {
test('should not change geo_bounding_box filter aggregation and vis session state when map movement is within map collar', () => {
const [, geoBoxingBox] = geoHashBucketAgg.getRequestAggs(
getAggConfigs({
boundingBox: {

View file

@ -18,9 +18,10 @@
*/
import { i18n } from '@kbn/i18n';
import { BucketAggType, IBucketAggConfig } from './_bucket_agg_type';
import { BucketAggType, IBucketAggConfig } from './bucket_agg_type';
import { KBN_FIELD_TYPES } from '../../../../common';
import { BUCKET_TYPES } from './bucket_agg_types';
import { GetInternalStartServicesFn } from '../../../types';
const defaultBoundingBox = {
top_left: { lat: 1, lon: 1 },
@ -33,83 +34,91 @@ const geohashGridTitle = i18n.translate('data.search.aggs.buckets.geohashGridTit
defaultMessage: 'Geohash',
});
export const geoHashBucketAgg = new BucketAggType<IBucketAggConfig>({
name: BUCKET_TYPES.GEOHASH_GRID,
title: geohashGridTitle,
params: [
export interface GeoHashBucketAggDependencies {
getInternalStartServices: GetInternalStartServicesFn;
}
export const getGeoHashBucketAgg = ({ getInternalStartServices }: GeoHashBucketAggDependencies) =>
new BucketAggType<IBucketAggConfig>(
{
name: 'field',
type: 'field',
filterFieldTypes: KBN_FIELD_TYPES.GEO_POINT,
},
{
name: 'autoPrecision',
default: true,
write: () => {},
},
{
name: 'precision',
default: defaultPrecision,
write(aggConfig, output) {
output.params.precision = aggConfig.params.precision;
name: BUCKET_TYPES.GEOHASH_GRID,
title: geohashGridTitle,
params: [
{
name: 'field',
type: 'field',
filterFieldTypes: KBN_FIELD_TYPES.GEO_POINT,
},
{
name: 'autoPrecision',
default: true,
write: () => {},
},
{
name: 'precision',
default: defaultPrecision,
write(aggConfig, output) {
output.params.precision = aggConfig.params.precision;
},
},
{
name: 'useGeocentroid',
default: true,
write: () => {},
},
{
name: 'isFilteredByCollar',
default: true,
write: () => {},
},
{
name: 'boundingBox',
default: null,
write: () => {},
},
],
getRequestAggs(agg) {
const aggs = [];
const params = agg.params;
if (params.isFilteredByCollar && agg.getField()) {
aggs.push(
agg.aggConfigs.createAggConfig(
{
type: 'filter',
id: 'filter_agg',
enabled: true,
params: {
geo_bounding_box: {
ignore_unmapped: true,
[agg.getField().name]: params.boundingBox || defaultBoundingBox,
},
},
} as any,
{ addToAggConfigs: false }
)
);
}
aggs.push(agg);
if (params.useGeocentroid) {
aggs.push(
agg.aggConfigs.createAggConfig(
{
type: 'geo_centroid',
enabled: true,
params: {
field: agg.getField(),
},
},
{ addToAggConfigs: false }
)
);
}
return aggs;
},
},
{
name: 'useGeocentroid',
default: true,
write: () => {},
},
{
name: 'isFilteredByCollar',
default: true,
write: () => {},
},
{
name: 'boundingBox',
default: null,
write: () => {},
},
],
getRequestAggs(agg) {
const aggs = [];
const params = agg.params;
if (params.isFilteredByCollar && agg.getField()) {
aggs.push(
agg.aggConfigs.createAggConfig(
{
type: 'filter',
id: 'filter_agg',
enabled: true,
params: {
geo_bounding_box: {
ignore_unmapped: true,
[agg.getField().name]: params.boundingBox || defaultBoundingBox,
},
},
} as any,
{ addToAggConfigs: false }
)
);
}
aggs.push(agg);
if (params.useGeocentroid) {
aggs.push(
agg.aggConfigs.createAggConfig(
{
type: 'geo_centroid',
enabled: true,
params: {
field: agg.getField(),
},
},
{ addToAggConfigs: false }
)
);
}
return aggs;
},
});
{ getInternalStartServices }
);

View file

@ -20,53 +20,61 @@
import { i18n } from '@kbn/i18n';
import { noop } from 'lodash';
import { BucketAggType } from './_bucket_agg_type';
import { BucketAggType, IBucketAggConfig } from './bucket_agg_type';
import { BUCKET_TYPES } from './bucket_agg_types';
import { KBN_FIELD_TYPES } from '../../../../common';
import { IBucketAggConfig } from './_bucket_agg_type';
import { METRIC_TYPES } from '../metrics/metric_agg_types';
import { GetInternalStartServicesFn } from '../../../types';
export interface GeoTitleBucketAggDependencies {
getInternalStartServices: GetInternalStartServicesFn;
}
const geotileGridTitle = i18n.translate('data.search.aggs.buckets.geotileGridTitle', {
defaultMessage: 'Geotile',
});
export const geoTileBucketAgg = new BucketAggType({
name: BUCKET_TYPES.GEOTILE_GRID,
title: geotileGridTitle,
params: [
export const getGeoTitleBucketAgg = ({ getInternalStartServices }: GeoTitleBucketAggDependencies) =>
new BucketAggType(
{
name: 'field',
type: 'field',
filterFieldTypes: KBN_FIELD_TYPES.GEO_POINT,
},
{
name: 'useGeocentroid',
default: true,
write: noop,
},
{
name: 'precision',
default: 0,
},
],
getRequestAggs(agg) {
const aggs = [];
const useGeocentroid = agg.getParam('useGeocentroid');
aggs.push(agg);
if (useGeocentroid) {
const aggConfig = {
type: METRIC_TYPES.GEO_CENTROID,
enabled: true,
params: {
field: agg.getField(),
name: BUCKET_TYPES.GEOTILE_GRID,
title: geotileGridTitle,
params: [
{
name: 'field',
type: 'field',
filterFieldTypes: KBN_FIELD_TYPES.GEO_POINT,
},
};
{
name: 'useGeocentroid',
default: true,
write: noop,
},
{
name: 'precision',
default: 0,
},
],
getRequestAggs(agg) {
const aggs = [];
const useGeocentroid = agg.getParam('useGeocentroid');
aggs.push(agg.aggConfigs.createAggConfig(aggConfig, { addToAggConfigs: false }));
}
aggs.push(agg);
return aggs as IBucketAggConfig[];
},
});
if (useGeocentroid) {
const aggConfig = {
type: METRIC_TYPES.GEO_CENTROID,
enabled: true,
params: {
field: agg.getField(),
},
};
aggs.push(agg.aggConfigs.createAggConfig(aggConfig, { addToAggConfigs: false }));
}
return aggs as IBucketAggConfig[];
},
},
{ getInternalStartServices }
);

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { coreMock } from '../../../../../../../src/core/public/mocks';
import { coreMock, notificationServiceMock } from '../../../../../../../src/core/public/mocks';
import { AggConfigs } from '../agg_configs';
import { mockAggTypesRegistry } from '../test_helpers';
import { BUCKET_TYPES } from './bucket_agg_types';
@ -27,17 +27,21 @@ import {
AutoBounds,
HistogramBucketAggDependencies,
} from './histogram';
import { BucketAggType } from './_bucket_agg_type';
import { BucketAggType } from './bucket_agg_type';
import { fieldFormatsServiceMock } from '../../../field_formats/mocks';
describe('Histogram Agg', () => {
let aggTypesDependencies: HistogramBucketAggDependencies;
beforeEach(() => {
const { uiSettings, notifications } = coreMock.createSetup();
const { uiSettings } = coreMock.createSetup();
aggTypesDependencies = {
uiSettings,
notifications,
getInternalStartServices: () => ({
fieldFormats: fieldFormatsServiceMock.createStartContract(),
notifications: notificationServiceMock.createStartContract(),
}),
};
});
@ -87,18 +91,18 @@ describe('Histogram Agg', () => {
histogramType = getHistogramBucketAgg(aggTypesDependencies);
});
it('is ordered', () => {
test('is ordered', () => {
expect(histogramType.ordered).toBeDefined();
});
it('is not ordered by date', () => {
test('is not ordered by date', () => {
expect(histogramType.ordered).not.toHaveProperty('date');
});
});
describe('params', () => {
describe('intervalBase', () => {
it('should not be written to the DSL', () => {
test('should not be written to the DSL', () => {
const aggConfigs = getAggConfigs({
intervalBase: 100,
field: {
@ -112,7 +116,7 @@ describe('Histogram Agg', () => {
});
describe('interval', () => {
it('accepts a whole number', () => {
test('accepts a whole number', () => {
const params = getParams({
interval: 100,
});
@ -120,7 +124,7 @@ describe('Histogram Agg', () => {
expect(params).toHaveProperty('interval', 100);
});
it('accepts a decimal number', function() {
test('accepts a decimal number', () => {
const params = getParams({
interval: 0.1,
});
@ -128,7 +132,7 @@ describe('Histogram Agg', () => {
expect(params).toHaveProperty('interval', 0.1);
});
it('accepts a decimal number string', function() {
test('accepts a decimal number string', () => {
const params = getParams({
interval: '0.1',
});
@ -136,7 +140,7 @@ describe('Histogram Agg', () => {
expect(params).toHaveProperty('interval', 0.1);
});
it('accepts a whole number string', function() {
test('accepts a whole number string', () => {
const params = getParams({
interval: '10',
});
@ -144,7 +148,7 @@ describe('Histogram Agg', () => {
expect(params).toHaveProperty('interval', 10);
});
it('fails on non-numeric values', function() {
test('fails on non-numeric values', () => {
const params = getParams({
interval: [],
});
@ -181,7 +185,7 @@ describe('Histogram Agg', () => {
return aggConfig.write(aggConfigs).params;
};
it('will respect the histogram:maxBars setting', () => {
test('will respect the histogram:maxBars setting', () => {
const params = getInterval(
5,
{ interval: 5 },
@ -194,19 +198,19 @@ describe('Histogram Agg', () => {
expect(params).toHaveProperty('interval', 2000);
});
it('will return specified interval, if bars are below histogram:maxBars config', () => {
test('will return specified interval, if bars are below histogram:maxBars config', () => {
const params = getInterval(100, { interval: 5 });
expect(params).toHaveProperty('interval', 5);
});
it('will set to intervalBase if interval is below base', () => {
test('will set to intervalBase if interval is below base', () => {
const params = getInterval(1000, { interval: 3, intervalBase: 8 });
expect(params).toHaveProperty('interval', 8);
});
it('will round to nearest intervalBase multiple if interval is above base', () => {
test('will round to nearest intervalBase multiple if interval is above base', () => {
const roundUp = getInterval(1000, { interval: 46, intervalBase: 10 });
expect(roundUp).toHaveProperty('interval', 50);
@ -214,13 +218,13 @@ describe('Histogram Agg', () => {
expect(roundDown).toHaveProperty('interval', 40);
});
it('will not change interval if it is a multiple of base', () => {
test('will not change interval if it is a multiple of base', () => {
const output = getInterval(1000, { interval: 35, intervalBase: 5 });
expect(output).toHaveProperty('interval', 35);
});
it('will round to intervalBase after scaling histogram:maxBars', () => {
test('will round to intervalBase after scaling histogram:maxBars', () => {
const output = getInterval(100, { interval: 5, intervalBase: 6 }, { min: 0, max: 1000 });
// 100 buckets in 0 to 1000 would result in an interval of 10, so we should
@ -232,7 +236,7 @@ describe('Histogram Agg', () => {
describe('min_doc_count', () => {
let output: Record<string, any>;
it('casts true values to 0', () => {
test('casts true values to 0', () => {
output = getParams({ min_doc_count: true });
expect(output).toHaveProperty('min_doc_count', 0);
@ -246,7 +250,7 @@ describe('Histogram Agg', () => {
expect(output).toHaveProperty('min_doc_count', 0);
});
it('writes 1 for falsy values', () => {
test('writes 1 for falsy values', () => {
output = getParams({ min_doc_count: '' });
expect(output).toHaveProperty('min_doc_count', 1);
@ -258,8 +262,8 @@ describe('Histogram Agg', () => {
});
});
describe('extended_bounds', function() {
it('does not write when only eb.min is set', function() {
describe('extended_bounds', () => {
test('does not write when only eb.min is set', () => {
const output = getParams({
has_extended_bounds: true,
extended_bounds: { min: 0 },
@ -267,7 +271,7 @@ describe('Histogram Agg', () => {
expect(output).not.toHaveProperty('extended_bounds');
});
it('does not write when only eb.max is set', function() {
test('does not write when only eb.max is set', () => {
const output = getParams({
has_extended_bounds: true,
extended_bounds: { max: 0 },
@ -276,7 +280,7 @@ describe('Histogram Agg', () => {
expect(output).not.toHaveProperty('extended_bounds');
});
it('writes when both eb.min and eb.max are set', function() {
test('writes when both eb.min and eb.max are set', () => {
const output = getParams({
has_extended_bounds: true,
extended_bounds: { min: 99, max: 100 },
@ -286,7 +290,7 @@ describe('Histogram Agg', () => {
expect(output.extended_bounds).toHaveProperty('max', 100);
});
it('does not write when nothing is set', function() {
test('does not write when nothing is set', () => {
const output = getParams({
has_extended_bounds: true,
extended_bounds: {},
@ -295,7 +299,7 @@ describe('Histogram Agg', () => {
expect(output).not.toHaveProperty('extended_bounds');
});
it('does not write when has_extended_bounds is false', function() {
test('does not write when has_extended_bounds is false', () => {
const output = getParams({
has_extended_bounds: false,
extended_bounds: { min: 99, max: 100 },

View file

@ -19,12 +19,13 @@
import { get } from 'lodash';
import { i18n } from '@kbn/i18n';
import { IUiSettingsClient, NotificationsSetup } from 'src/core/public';
import { IUiSettingsClient } from 'src/core/public';
import { BucketAggType, IBucketAggConfig } from './_bucket_agg_type';
import { BucketAggType, IBucketAggConfig } from './bucket_agg_type';
import { createFilterHistogram } from './create_filter/histogram';
import { BUCKET_TYPES } from './bucket_agg_types';
import { KBN_FIELD_TYPES } from '../../../../common';
import { GetInternalStartServicesFn } from '../../../types';
export interface AutoBounds {
min: number;
@ -33,7 +34,7 @@ export interface AutoBounds {
export interface HistogramBucketAggDependencies {
uiSettings: IUiSettingsClient;
notifications: NotificationsSetup;
getInternalStartServices: GetInternalStartServicesFn;
}
export interface IBucketHistogramAggConfig extends IBucketAggConfig {
@ -43,164 +44,167 @@ export interface IBucketHistogramAggConfig extends IBucketAggConfig {
export const getHistogramBucketAgg = ({
uiSettings,
notifications,
getInternalStartServices,
}: HistogramBucketAggDependencies) =>
new BucketAggType<IBucketHistogramAggConfig>({
name: BUCKET_TYPES.HISTOGRAM,
title: i18n.translate('data.search.aggs.buckets.histogramTitle', {
defaultMessage: 'Histogram',
}),
ordered: {},
makeLabel(aggConfig) {
return aggConfig.getFieldDisplayName();
},
createFilter: createFilterHistogram,
decorateAggConfig() {
let autoBounds: AutoBounds;
return {
setAutoBounds: {
configurable: true,
value(newValue: AutoBounds) {
autoBounds = newValue;
},
},
getAutoBounds: {
configurable: true,
value() {
return autoBounds;
},
},
};
},
params: [
{
name: 'field',
type: 'field',
filterFieldTypes: KBN_FIELD_TYPES.NUMBER,
new BucketAggType<IBucketHistogramAggConfig>(
{
name: BUCKET_TYPES.HISTOGRAM,
title: i18n.translate('data.search.aggs.buckets.histogramTitle', {
defaultMessage: 'Histogram',
}),
ordered: {},
makeLabel(aggConfig) {
return aggConfig.getFieldDisplayName();
},
{
/*
* This parameter can be set if you want the auto scaled interval to always
* be a multiple of a specific base.
*/
name: 'intervalBase',
default: null,
write: () => {},
createFilter: createFilterHistogram,
decorateAggConfig() {
let autoBounds: AutoBounds;
return {
setAutoBounds: {
configurable: true,
value(newValue: AutoBounds) {
autoBounds = newValue;
},
},
getAutoBounds: {
configurable: true,
value() {
return autoBounds;
},
},
};
},
{
name: 'interval',
modifyAggConfigOnSearchRequestStart(
aggConfig: IBucketHistogramAggConfig,
searchSource: any,
options: any
) {
const field = aggConfig.getField();
const aggBody = field.scripted
? { script: { source: field.script, lang: field.lang } }
: { field: field.name };
params: [
{
name: 'field',
type: 'field',
filterFieldTypes: KBN_FIELD_TYPES.NUMBER,
},
{
/*
* This parameter can be set if you want the auto scaled interval to always
* be a multiple of a specific base.
*/
name: 'intervalBase',
default: null,
write: () => {},
},
{
name: 'interval',
modifyAggConfigOnSearchRequestStart(
aggConfig: IBucketHistogramAggConfig,
searchSource: any,
options: any
) {
const field = aggConfig.getField();
const aggBody = field.scripted
? { script: { source: field.script, lang: field.lang } }
: { field: field.name };
const childSearchSource = searchSource
.createChild()
.setField('size', 0)
.setField('aggs', {
maxAgg: {
max: aggBody,
},
minAgg: {
min: aggBody,
},
});
return childSearchSource
.fetch(options)
.then((resp: any) => {
aggConfig.setAutoBounds({
min: get(resp, 'aggregations.minAgg.value'),
max: get(resp, 'aggregations.maxAgg.value'),
const childSearchSource = searchSource
.createChild()
.setField('size', 0)
.setField('aggs', {
maxAgg: {
max: aggBody,
},
minAgg: {
min: aggBody,
},
});
})
.catch((e: Error) => {
if (e.name === 'AbortError') return;
notifications.toasts.addWarning(
i18n.translate('data.search.aggs.histogram.missingMaxMinValuesWarning', {
defaultMessage:
'Unable to retrieve max and min values to auto-scale histogram buckets. This may lead to poor visualization performance.',
})
);
});
},
write(aggConfig, output) {
let interval = parseFloat(aggConfig.params.interval);
if (interval <= 0) {
interval = 1;
}
const autoBounds = aggConfig.getAutoBounds();
// ensure interval does not create too many buckets and crash browser
if (autoBounds) {
const range = autoBounds.max - autoBounds.min;
const bars = range / interval;
return childSearchSource
.fetch(options)
.then((resp: any) => {
aggConfig.setAutoBounds({
min: get(resp, 'aggregations.minAgg.value'),
max: get(resp, 'aggregations.maxAgg.value'),
});
})
.catch((e: Error) => {
if (e.name === 'AbortError') return;
getInternalStartServices().notifications.toasts.addWarning(
i18n.translate('data.search.aggs.histogram.missingMaxMinValuesWarning', {
defaultMessage:
'Unable to retrieve max and min values to auto-scale histogram buckets. This may lead to poor visualization performance.',
})
);
});
},
write(aggConfig, output) {
let interval = parseFloat(aggConfig.params.interval);
if (interval <= 0) {
interval = 1;
}
const autoBounds = aggConfig.getAutoBounds();
if (bars > uiSettings.get('histogram:maxBars')) {
const minInterval = range / uiSettings.get('histogram:maxBars');
// ensure interval does not create too many buckets and crash browser
if (autoBounds) {
const range = autoBounds.max - autoBounds.min;
const bars = range / interval;
// Round interval by order of magnitude to provide clean intervals
// Always round interval up so there will always be less buckets than histogram:maxBars
const orderOfMagnitude = Math.pow(10, Math.floor(Math.log10(minInterval)));
let roundInterval = orderOfMagnitude;
if (bars > uiSettings.get('histogram:maxBars')) {
const minInterval = range / uiSettings.get('histogram:maxBars');
while (roundInterval < minInterval) {
roundInterval += orderOfMagnitude;
// Round interval by order of magnitude to provide clean intervals
// Always round interval up so there will always be less buckets than histogram:maxBars
const orderOfMagnitude = Math.pow(10, Math.floor(Math.log10(minInterval)));
let roundInterval = orderOfMagnitude;
while (roundInterval < minInterval) {
roundInterval += orderOfMagnitude;
}
interval = roundInterval;
}
interval = roundInterval;
}
}
const base = aggConfig.params.intervalBase;
const base = aggConfig.params.intervalBase;
if (base) {
if (interval < base) {
// In case the specified interval is below the base, just increase it to it's base
interval = base;
} else if (interval % base !== 0) {
// In case the interval is not a multiple of the base round it to the next base
interval = Math.round(interval / base) * base;
if (base) {
if (interval < base) {
// In case the specified interval is below the base, just increase it to it's base
interval = base;
} else if (interval % base !== 0) {
// In case the interval is not a multiple of the base round it to the next base
interval = Math.round(interval / base) * base;
}
}
}
output.params.interval = interval;
output.params.interval = interval;
},
},
},
{
name: 'min_doc_count',
default: false,
write(aggConfig, output) {
if (aggConfig.params.min_doc_count) {
output.params.min_doc_count = 0;
} else {
output.params.min_doc_count = 1;
}
{
name: 'min_doc_count',
default: false,
write(aggConfig, output) {
if (aggConfig.params.min_doc_count) {
output.params.min_doc_count = 0;
} else {
output.params.min_doc_count = 1;
}
},
},
},
{
name: 'has_extended_bounds',
default: false,
write: () => {},
},
{
name: 'extended_bounds',
default: {
min: '',
max: '',
{
name: 'has_extended_bounds',
default: false,
write: () => {},
},
write(aggConfig, output) {
const { min, max } = aggConfig.params.extended_bounds;
{
name: 'extended_bounds',
default: {
min: '',
max: '',
},
write(aggConfig, output) {
const { min, max } = aggConfig.params.extended_bounds;
if (aggConfig.params.has_extended_bounds && (min || min === 0) && (max || max === 0)) {
output.params.extended_bounds = { min, max };
}
if (aggConfig.params.has_extended_bounds && (min || min === 0) && (max || max === 0)) {
output.params.extended_bounds = { min, max };
}
},
shouldShow: (aggConfig: IBucketAggConfig) => aggConfig.params.has_extended_bounds,
},
shouldShow: (aggConfig: IBucketAggConfig) => aggConfig.params.has_extended_bounds,
},
],
});
],
},
{ getInternalStartServices }
);

View file

@ -19,77 +19,85 @@
import { noop, map, omit, isNull } from 'lodash';
import { i18n } from '@kbn/i18n';
import { BucketAggType } from './_bucket_agg_type';
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 { KBN_FIELD_TYPES, FieldFormat, TEXT_CONTEXT_TYPE } from '../../../../common';
import { getFieldFormats } from '../../../../public/services';
import { GetInternalStartServicesFn } from '../../../types';
const ipRangeTitle = i18n.translate('data.search.aggs.buckets.ipRangeTitle', {
defaultMessage: 'IPv4 Range',
});
export const ipRangeBucketAgg = new BucketAggType({
name: BUCKET_TYPES.IP_RANGE,
title: ipRangeTitle,
createFilter: createFilterIpRange,
getKey(bucket, key, agg): IpRangeKey {
if (agg.params.ipRangeType === 'mask') {
return { type: 'mask', mask: key };
}
return { type: 'range', from: bucket.from, to: bucket.to };
},
getFormat(agg) {
const fieldFormatsService = getFieldFormats();
const formatter = agg.fieldOwnFormatter(
TEXT_CONTEXT_TYPE,
fieldFormatsService.getDefaultInstance(KBN_FIELD_TYPES.IP)
);
const IpRangeFormat = FieldFormat.from(function(range: IpRangeKey) {
return convertIPRangeToString(range, formatter);
});
return new IpRangeFormat();
},
makeLabel(aggConfig) {
return i18n.translate('data.search.aggs.buckets.ipRangeLabel', {
defaultMessage: '{fieldName} IP ranges',
values: {
fieldName: aggConfig.getFieldDisplayName(),
},
});
},
params: [
{
name: 'field',
type: 'field',
filterFieldTypes: KBN_FIELD_TYPES.IP,
},
{
name: 'ipRangeType',
default: 'fromTo',
write: noop,
},
{
name: 'ranges',
default: {
fromTo: [
{ from: '0.0.0.0', to: '127.255.255.255' },
{ from: '128.0.0.0', to: '191.255.255.255' },
],
mask: [{ mask: '0.0.0.0/1' }, { mask: '128.0.0.0/2' }],
},
write(aggConfig, output) {
const ipRangeType = aggConfig.params.ipRangeType;
let ranges = aggConfig.params.ranges[ipRangeType];
export interface IpRangeBucketAggDependencies {
getInternalStartServices: GetInternalStartServicesFn;
}
if (ipRangeType === 'fromTo') {
ranges = map(ranges, (range: any) => omit(range, isNull));
export const getIpRangeBucketAgg = ({ getInternalStartServices }: IpRangeBucketAggDependencies) =>
new BucketAggType(
{
name: BUCKET_TYPES.IP_RANGE,
title: ipRangeTitle,
createFilter: createFilterIpRange,
getKey(bucket, key, agg): IpRangeKey {
if (agg.params.ipRangeType === 'mask') {
return { type: 'mask', mask: key };
}
output.params.ranges = ranges;
return { type: 'range', from: bucket.from, to: bucket.to };
},
getFormat(agg) {
const { fieldFormats } = getInternalStartServices();
const formatter = agg.fieldOwnFormatter(
TEXT_CONTEXT_TYPE,
fieldFormats.getDefaultInstance(KBN_FIELD_TYPES.IP)
);
const IpRangeFormat = FieldFormat.from(function(range: IpRangeKey) {
return convertIPRangeToString(range, formatter);
});
return new IpRangeFormat();
},
makeLabel(aggConfig) {
return i18n.translate('data.search.aggs.buckets.ipRangeLabel', {
defaultMessage: '{fieldName} IP ranges',
values: {
fieldName: aggConfig.getFieldDisplayName(),
},
});
},
params: [
{
name: 'field',
type: 'field',
filterFieldTypes: KBN_FIELD_TYPES.IP,
},
{
name: 'ipRangeType',
default: 'fromTo',
write: noop,
},
{
name: 'ranges',
default: {
fromTo: [
{ from: '0.0.0.0', to: '127.255.255.255' },
{ from: '128.0.0.0', to: '191.255.255.255' },
],
mask: [{ mask: '0.0.0.0/1' }, { mask: '128.0.0.0/2' }],
},
write(aggConfig, output) {
const ipRangeType = aggConfig.params.ipRangeType;
let ranges = aggConfig.params.ranges[ipRangeType];
if (ipRangeType === 'fromTo') {
ranges = map(ranges, (range: any) => omit(range, isNull));
}
output.params.ranges = ranges;
},
},
],
},
],
});
{ getInternalStartServices }
);

View file

@ -18,7 +18,7 @@
*/
import { isString, isObject } from 'lodash';
import { IBucketAggConfig, BucketAggType, BucketAggParam } from './_bucket_agg_type';
import { IBucketAggConfig, BucketAggType, BucketAggParam } from './bucket_agg_type';
import { IAggConfig } from '../agg_config';
export const isType = (type: string) => {

View file

@ -17,11 +17,13 @@
* under the License.
*/
import { rangeBucketAgg } from './range';
import { getRangeBucketAgg, RangeBucketAggDependencies } from './range';
import { AggConfigs } from '../agg_configs';
import { mockDataServices, mockAggTypesRegistry } from '../test_helpers';
import { BUCKET_TYPES } from './bucket_agg_types';
import { FieldFormatsGetConfigFn, NumberFormat } from '../../../../common';
import { fieldFormatsServiceMock } from '../../../field_formats/mocks';
import { notificationServiceMock } from '../../../../../../../src/core/public/mocks';
const buckets = [
{
@ -44,7 +46,16 @@ const buckets = [
];
describe('Range Agg', () => {
let aggTypesDependencies: RangeBucketAggDependencies;
beforeEach(() => {
aggTypesDependencies = {
getInternalStartServices: () => ({
fieldFormats: fieldFormatsServiceMock.createStartContract(),
notifications: notificationServiceMock.createStartContract(),
}),
};
mockDataServices();
});
@ -84,15 +95,14 @@ describe('Range Agg', () => {
},
},
],
{ typesRegistry: mockAggTypesRegistry([rangeBucketAgg]) }
{ typesRegistry: mockAggTypesRegistry([getRangeBucketAgg(aggTypesDependencies)]) }
);
};
describe('formating', () => {
it('formats bucket keys properly', () => {
test('formats bucket keys properly', () => {
const aggConfigs = getAggConfigs();
const agg = aggConfigs.aggs[0];
const format = (val: any) => agg.fieldFormatter()(agg.getKey(val));
expect(format(buckets[0])).toBe('≥ -∞ and < 1 KB');

View file

@ -18,11 +18,12 @@
*/
import { i18n } from '@kbn/i18n';
import { BucketAggType } from './_bucket_agg_type';
import { BucketAggType } from './bucket_agg_type';
import { FieldFormat, KBN_FIELD_TYPES } from '../../../../common';
import { RangeKey } from './range_key';
import { createFilterRange } from './create_filter/range';
import { BUCKET_TYPES } from './bucket_agg_types';
import { GetInternalStartServicesFn } from '../../../types';
const keyCaches = new WeakMap();
const formats = new WeakMap();
@ -31,76 +32,84 @@ const rangeTitle = i18n.translate('data.search.aggs.buckets.rangeTitle', {
defaultMessage: 'Range',
});
export const rangeBucketAgg = new BucketAggType({
name: BUCKET_TYPES.RANGE,
title: rangeTitle,
createFilter: createFilterRange,
makeLabel(aggConfig) {
return i18n.translate('data.search.aggs.aggTypesLabel', {
defaultMessage: '{fieldName} ranges',
values: {
fieldName: aggConfig.getFieldDisplayName(),
export interface RangeBucketAggDependencies {
getInternalStartServices: GetInternalStartServicesFn;
}
export const getRangeBucketAgg = ({ getInternalStartServices }: RangeBucketAggDependencies) =>
new BucketAggType(
{
name: BUCKET_TYPES.RANGE,
title: rangeTitle,
createFilter: createFilterRange,
makeLabel(aggConfig) {
return i18n.translate('data.search.aggs.aggTypesLabel', {
defaultMessage: '{fieldName} ranges',
values: {
fieldName: aggConfig.getFieldDisplayName(),
},
});
},
});
},
getKey(bucket, key, agg) {
let keys = keyCaches.get(agg);
getKey(bucket, key, agg) {
let keys = keyCaches.get(agg);
if (!keys) {
keys = new Map();
keyCaches.set(agg, keys);
}
if (!keys) {
keys = new Map();
keyCaches.set(agg, keys);
}
const id = RangeKey.idBucket(bucket);
const id = RangeKey.idBucket(bucket);
key = keys.get(id);
if (!key) {
key = new RangeKey(bucket);
keys.set(id, key);
}
key = keys.get(id);
if (!key) {
key = new RangeKey(bucket);
keys.set(id, key);
}
return key;
},
getFormat(agg) {
let aggFormat = formats.get(agg);
if (aggFormat) return aggFormat;
return key;
},
getFormat(agg) {
let aggFormat = formats.get(agg);
if (aggFormat) return aggFormat;
const RangeFormat = FieldFormat.from((range: any) => {
const format = agg.fieldOwnFormatter();
const gte = '\u2265';
const lt = '\u003c';
return i18n.translate('data.search.aggs.aggTypes.rangesFormatMessage', {
defaultMessage: '{gte} {from} and {lt} {to}',
values: {
gte,
from: format(range.gte),
lt,
to: format(range.lt),
const RangeFormat = FieldFormat.from((range: any) => {
const format = agg.fieldOwnFormatter();
const gte = '\u2265';
const lt = '\u003c';
return i18n.translate('data.search.aggs.aggTypes.rangesFormatMessage', {
defaultMessage: '{gte} {from} and {lt} {to}',
values: {
gte,
from: format(range.gte),
lt,
to: format(range.lt),
},
});
});
aggFormat = new RangeFormat();
formats.set(agg, aggFormat);
return aggFormat;
},
params: [
{
name: 'field',
type: 'field',
filterFieldTypes: [KBN_FIELD_TYPES.NUMBER],
},
{
name: 'ranges',
default: [
{ from: 0, to: 1000 },
{ from: 1000, to: 2000 },
],
write(aggConfig, output) {
output.params.ranges = aggConfig.params.ranges;
output.params.keyed = true;
},
},
});
});
aggFormat = new RangeFormat();
formats.set(agg, aggFormat);
return aggFormat;
},
params: [
{
name: 'field',
type: 'field',
filterFieldTypes: [KBN_FIELD_TYPES.NUMBER],
},
{
name: 'ranges',
default: [
{ from: 0, to: 1000 },
{ from: 1000, to: 2000 },
],
write(aggConfig, output) {
output.params.ranges = aggConfig.params.ranges;
output.params.keyed = true;
},
},
],
});
{ getInternalStartServices }
);

View file

@ -20,12 +20,27 @@
import { AggConfigs, IAggConfigs } from '../agg_configs';
import { mockAggTypesRegistry } from '../test_helpers';
import { BUCKET_TYPES } from './bucket_agg_types';
import { significantTermsBucketAgg } from './significant_terms';
import { IBucketAggConfig } from './_bucket_agg_type';
import {
getSignificantTermsBucketAgg,
SignificantTermsBucketAggDependencies,
} from './significant_terms';
import { fieldFormatsServiceMock } from '../../../field_formats/mocks';
import { notificationServiceMock } from '../../../../../../../src/core/public/mocks';
describe('Significant Terms Agg', () => {
describe('order agg editor UI', () => {
describe('convert include/exclude from old format', () => {
let aggTypesDependencies: SignificantTermsBucketAggDependencies;
beforeEach(() => {
aggTypesDependencies = {
getInternalStartServices: () => ({
fieldFormats: fieldFormatsServiceMock.createStartContract(),
notifications: notificationServiceMock.createStartContract(),
}),
};
});
const getAggConfigs = (params: Record<string, any> = {}) => {
const indexPattern = {
id: '1234',
@ -51,7 +66,11 @@ describe('Significant Terms Agg', () => {
params,
},
],
{ typesRegistry: mockAggTypesRegistry([significantTermsBucketAgg]) }
{
typesRegistry: mockAggTypesRegistry([
getSignificantTermsBucketAgg(aggTypesDependencies),
]),
}
);
};
@ -64,19 +83,19 @@ describe('Significant Terms Agg', () => {
expect(params.exclude).toBe('400');
};
it('should generate correct label', () => {
test('should generate correct label', () => {
const aggConfigs = getAggConfigs({
size: 'SIZE',
field: {
name: 'FIELD',
},
});
const label = significantTermsBucketAgg.makeLabel(aggConfigs.aggs[0] as IBucketAggConfig);
const label = aggConfigs.aggs[0].makeLabel();
expect(label).toBe('Top SIZE unusual terms in FIELD');
});
it('should doesnt do anything with string type', () => {
test('should doesnt do anything with string type', () => {
const aggConfigs = getAggConfigs({
include: '404',
exclude: '400',
@ -89,7 +108,7 @@ describe('Significant Terms Agg', () => {
testSerializeAndWrite(aggConfigs);
});
it('should converts object to string type', () => {
test('should converts object to string type', () => {
const aggConfigs = getAggConfigs({
include: {
pattern: '404',

View file

@ -18,59 +18,72 @@
*/
import { i18n } from '@kbn/i18n';
import { BucketAggType } from './_bucket_agg_type';
import { BucketAggType } from './bucket_agg_type';
import { createFilterTerms } from './create_filter/terms';
import { isStringType, migrateIncludeExcludeFormat } from './migrate_include_exclude_format';
import { BUCKET_TYPES } from './bucket_agg_types';
import { KBN_FIELD_TYPES } from '../../../../common';
import { GetInternalStartServicesFn } from '../../../types';
const significantTermsTitle = i18n.translate('data.search.aggs.buckets.significantTermsTitle', {
defaultMessage: 'Significant Terms',
});
export const significantTermsBucketAgg = new BucketAggType({
name: BUCKET_TYPES.SIGNIFICANT_TERMS,
title: significantTermsTitle,
makeLabel(aggConfig) {
return i18n.translate('data.search.aggs.buckets.significantTermsLabel', {
defaultMessage: 'Top {size} unusual terms in {fieldName}',
values: {
size: aggConfig.params.size,
fieldName: aggConfig.getFieldDisplayName(),
export interface SignificantTermsBucketAggDependencies {
getInternalStartServices: GetInternalStartServicesFn;
}
export const getSignificantTermsBucketAgg = ({
getInternalStartServices,
}: SignificantTermsBucketAggDependencies) =>
new BucketAggType(
{
name: BUCKET_TYPES.SIGNIFICANT_TERMS,
title: significantTermsTitle,
makeLabel(aggConfig) {
return i18n.translate('data.search.aggs.buckets.significantTermsLabel', {
defaultMessage: 'Top {size} unusual terms in {fieldName}',
values: {
size: aggConfig.params.size,
fieldName: aggConfig.getFieldDisplayName(),
},
});
},
});
},
createFilter: createFilterTerms,
params: [
{
name: 'field',
type: 'field',
scriptable: false,
filterFieldTypes: KBN_FIELD_TYPES.STRING,
createFilter: createFilterTerms,
params: [
{
name: 'field',
type: 'field',
scriptable: false,
filterFieldTypes: KBN_FIELD_TYPES.STRING,
},
{
name: 'size',
default: '',
},
{
name: 'exclude',
displayName: i18n.translate('data.search.aggs.buckets.significantTerms.excludeLabel', {
defaultMessage: 'Exclude',
}),
type: 'string',
advanced: true,
shouldShow: isStringType,
...migrateIncludeExcludeFormat,
},
{
name: 'include',
displayName: i18n.translate('data.search.aggs.buckets.significantTerms.includeLabel', {
defaultMessage: 'Include',
}),
type: 'string',
advanced: true,
shouldShow: isStringType,
...migrateIncludeExcludeFormat,
},
],
},
{
name: 'size',
default: '',
},
{
name: 'exclude',
displayName: i18n.translate('data.search.aggs.buckets.significantTerms.excludeLabel', {
defaultMessage: 'Exclude',
}),
type: 'string',
advanced: true,
shouldShow: isStringType,
...migrateIncludeExcludeFormat,
},
{
name: 'include',
displayName: i18n.translate('data.search.aggs.buckets.significantTerms.includeLabel', {
defaultMessage: 'Include',
}),
type: 'string',
advanced: true,
shouldShow: isStringType,
...migrateIncludeExcludeFormat,
},
],
});
getInternalStartServices,
}
);

View file

@ -51,7 +51,7 @@ describe('Terms Agg', () => {
);
};
it('converts object to string type', function() {
test('converts object to string type', () => {
const aggConfigs = getAggConfigs({
include: {
pattern: '404',

View file

@ -19,9 +19,8 @@
import { noop } from 'lodash';
import { i18n } from '@kbn/i18n';
import { BucketAggType } from './_bucket_agg_type';
import { BucketAggType, IBucketAggConfig } from './bucket_agg_type';
import { BUCKET_TYPES } from './bucket_agg_types';
import { IBucketAggConfig } from './_bucket_agg_type';
import { createFilterTerms } from './create_filter/terms';
import { isStringType, migrateIncludeExcludeFormat } from './migrate_include_exclude_format';
import { IAggConfigs } from '../agg_configs';
@ -36,6 +35,7 @@ import {
mergeOtherBucketAggResponse,
updateMissingBucket,
} from './_terms_other_bucket_helper';
import { GetInternalStartServicesFn } from '../../../types';
export const termsAggFilter = [
'!top_hits',
@ -56,220 +56,230 @@ const termsTitle = i18n.translate('data.search.aggs.buckets.termsTitle', {
defaultMessage: 'Terms',
});
export const termsBucketAgg = new BucketAggType({
name: BUCKET_TYPES.TERMS,
title: termsTitle,
makeLabel(agg) {
const params = agg.params;
return agg.getFieldDisplayName() + ': ' + params.order.text;
},
getFormat(bucket): IFieldFormat {
return {
getConverterFor: (type: FieldFormatsContentType) => {
return (val: any) => {
if (val === '__other__') {
return bucket.params.otherBucketLabel;
}
if (val === '__missing__') {
return bucket.params.missingBucketLabel;
}
export interface TermsBucketAggDependencies {
getInternalStartServices: GetInternalStartServicesFn;
}
return bucket.params.field.format.convert(val, type);
};
export const getTermsBucketAgg = ({ getInternalStartServices }: TermsBucketAggDependencies) =>
new BucketAggType(
{
name: BUCKET_TYPES.TERMS,
title: termsTitle,
makeLabel(agg) {
const params = agg.params;
return agg.getFieldDisplayName() + ': ' + params.order.text;
},
} as IFieldFormat;
},
createFilter: createFilterTerms,
postFlightRequest: async (
resp: any,
aggConfigs: IAggConfigs,
aggConfig: IBucketAggConfig,
searchSource: ISearchSource,
inspectorAdapters: Adapters,
abortSignal?: AbortSignal
) => {
if (!resp.aggregations) return resp;
const nestedSearchSource = searchSource.createChild();
if (aggConfig.params.otherBucket) {
const filterAgg = buildOtherBucketAgg(aggConfigs, aggConfig, resp);
if (!filterAgg) return resp;
getFormat(bucket): IFieldFormat {
return {
getConverterFor: (type: FieldFormatsContentType) => {
return (val: any) => {
if (val === '__other__') {
return bucket.params.otherBucketLabel;
}
if (val === '__missing__') {
return bucket.params.missingBucketLabel;
}
nestedSearchSource.setField('aggs', filterAgg);
return bucket.params.field.format.convert(val, type);
};
},
} as IFieldFormat;
},
createFilter: createFilterTerms,
postFlightRequest: async (
resp: any,
aggConfigs: IAggConfigs,
aggConfig: IBucketAggConfig,
searchSource: ISearchSource,
inspectorAdapters: Adapters,
abortSignal?: AbortSignal
) => {
if (!resp.aggregations) return resp;
const nestedSearchSource = searchSource.createChild();
if (aggConfig.params.otherBucket) {
const filterAgg = buildOtherBucketAgg(aggConfigs, aggConfig, resp);
if (!filterAgg) return resp;
const request = inspectorAdapters.requests.start(
i18n.translate('data.search.aggs.buckets.terms.otherBucketTitle', {
defaultMessage: 'Other bucket',
}),
nestedSearchSource.setField('aggs', filterAgg);
const request = inspectorAdapters.requests.start(
i18n.translate('data.search.aggs.buckets.terms.otherBucketTitle', {
defaultMessage: 'Other bucket',
}),
{
description: i18n.translate('data.search.aggs.buckets.terms.otherBucketDescription', {
defaultMessage:
'This request counts the number of documents that fall ' +
'outside the criterion of the data buckets.',
}),
}
);
nestedSearchSource.getSearchRequestBody().then((body: string) => {
request.json(body);
});
request.stats(getRequestInspectorStats(nestedSearchSource));
const response = await nestedSearchSource.fetch({ abortSignal });
request
.stats(getResponseInspectorStats(nestedSearchSource, response))
.ok({ json: response });
resp = mergeOtherBucketAggResponse(aggConfigs, resp, response, aggConfig, filterAgg());
}
if (aggConfig.params.missingBucket) {
resp = updateMissingBucket(resp, aggConfigs, aggConfig);
}
return resp;
},
params: [
{
description: i18n.translate('data.search.aggs.buckets.terms.otherBucketDescription', {
defaultMessage:
'This request counts the number of documents that fall ' +
'outside the criterion of the data buckets.',
}),
}
);
nestedSearchSource.getSearchRequestBody().then((body: string) => {
request.json(body);
});
request.stats(getRequestInspectorStats(nestedSearchSource));
const response = await nestedSearchSource.fetch({ abortSignal });
request.stats(getResponseInspectorStats(nestedSearchSource, response)).ok({ json: response });
resp = mergeOtherBucketAggResponse(aggConfigs, resp, response, aggConfig, filterAgg());
}
if (aggConfig.params.missingBucket) {
resp = updateMissingBucket(resp, aggConfigs, aggConfig);
}
return resp;
},
params: [
{
name: 'field',
type: 'field',
filterFieldTypes: [
KBN_FIELD_TYPES.NUMBER,
KBN_FIELD_TYPES.BOOLEAN,
KBN_FIELD_TYPES.DATE,
KBN_FIELD_TYPES.IP,
KBN_FIELD_TYPES.STRING,
],
},
{
name: 'orderBy',
write: noop, // prevent default write, it's handled by orderAgg
},
{
name: 'orderAgg',
type: 'agg',
allowedAggs: termsAggFilter,
default: null,
makeAgg(termsAgg, state) {
state = state || {};
state.schema = 'orderAgg';
const orderAgg = termsAgg.aggConfigs.createAggConfig<IBucketAggConfig>(state, {
addToAggConfigs: false,
});
orderAgg.id = termsAgg.id + '-orderAgg';
return orderAgg;
},
write(agg, output, aggs) {
const dir = agg.params.order.value;
const order: Record<string, any> = (output.params.order = {});
let orderAgg = agg.params.orderAgg || aggs!.getResponseAggById(agg.params.orderBy);
// TODO: This works around an Elasticsearch bug the always casts terms agg scripts to strings
// thus causing issues with filtering. This probably causes other issues since float might not
// be able to contain the number on the elasticsearch side
if (output.params.script) {
output.params.value_type =
agg.getField().type === 'number' ? 'float' : agg.getField().type;
}
if (agg.params.missingBucket && agg.params.field.type === 'string') {
output.params.missing = '__missing__';
}
if (!orderAgg) {
order[agg.params.orderBy || '_count'] = dir;
return;
}
if (orderAgg.type.name === 'count') {
order._count = dir;
return;
}
const orderAggId = orderAgg.id;
if (orderAgg.parentId && aggs) {
orderAgg = aggs.byId(orderAgg.parentId);
}
output.subAggs = (output.subAggs || []).concat(orderAgg);
order[orderAggId] = dir;
},
},
{
name: 'order',
type: 'optioned',
default: 'desc',
options: [
{
text: i18n.translate('data.search.aggs.buckets.terms.orderDescendingTitle', {
defaultMessage: 'Descending',
}),
value: 'desc',
name: 'field',
type: 'field',
filterFieldTypes: [
KBN_FIELD_TYPES.NUMBER,
KBN_FIELD_TYPES.BOOLEAN,
KBN_FIELD_TYPES.DATE,
KBN_FIELD_TYPES.IP,
KBN_FIELD_TYPES.STRING,
],
},
{
text: i18n.translate('data.search.aggs.buckets.terms.orderAscendingTitle', {
defaultMessage: 'Ascending',
}),
value: 'asc',
name: 'orderBy',
write: noop, // prevent default write, it's handled by orderAgg
},
],
write: noop, // prevent default write, it's handled by orderAgg
},
{
name: 'size',
default: 5,
},
{
name: 'otherBucket',
default: false,
write: noop,
},
{
name: 'otherBucketLabel',
type: 'string',
default: i18n.translate('data.search.aggs.buckets.terms.otherBucketLabel', {
defaultMessage: 'Other',
}),
displayName: i18n.translate('data.search.aggs.otherBucket.labelForOtherBucketLabel', {
defaultMessage: 'Label for other bucket',
}),
shouldShow: agg => agg.getParam('otherBucket'),
write: noop,
},
{
name: 'missingBucket',
default: false,
write: noop,
},
{
name: 'missingBucketLabel',
default: i18n.translate('data.search.aggs.buckets.terms.missingBucketLabel', {
defaultMessage: 'Missing',
description: `Default label used in charts when documents are missing a field.
{
name: 'orderAgg',
type: 'agg',
allowedAggs: termsAggFilter,
default: null,
makeAgg(termsAgg, state) {
state = state || {};
state.schema = 'orderAgg';
const orderAgg = termsAgg.aggConfigs.createAggConfig<IBucketAggConfig>(state, {
addToAggConfigs: false,
});
orderAgg.id = termsAgg.id + '-orderAgg';
return orderAgg;
},
write(agg, output, aggs) {
const dir = agg.params.order.value;
const order: Record<string, any> = (output.params.order = {});
let orderAgg = agg.params.orderAgg || aggs!.getResponseAggById(agg.params.orderBy);
// TODO: This works around an Elasticsearch bug the always casts terms agg scripts to strings
// thus causing issues with filtering. This probably causes other issues since float might not
// be able to contain the number on the elasticsearch side
if (output.params.script) {
output.params.value_type =
agg.getField().type === 'number' ? 'float' : agg.getField().type;
}
if (agg.params.missingBucket && agg.params.field.type === 'string') {
output.params.missing = '__missing__';
}
if (!orderAgg) {
order[agg.params.orderBy || '_count'] = dir;
return;
}
if (orderAgg.type.name === 'count') {
order._count = dir;
return;
}
const orderAggId = orderAgg.id;
if (orderAgg.parentId && aggs) {
orderAgg = aggs.byId(orderAgg.parentId);
}
output.subAggs = (output.subAggs || []).concat(orderAgg);
order[orderAggId] = dir;
},
},
{
name: 'order',
type: 'optioned',
default: 'desc',
options: [
{
text: i18n.translate('data.search.aggs.buckets.terms.orderDescendingTitle', {
defaultMessage: 'Descending',
}),
value: 'desc',
},
{
text: i18n.translate('data.search.aggs.buckets.terms.orderAscendingTitle', {
defaultMessage: 'Ascending',
}),
value: 'asc',
},
],
write: noop, // prevent default write, it's handled by orderAgg
},
{
name: 'size',
default: 5,
},
{
name: 'otherBucket',
default: false,
write: noop,
},
{
name: 'otherBucketLabel',
type: 'string',
default: i18n.translate('data.search.aggs.buckets.terms.otherBucketLabel', {
defaultMessage: 'Other',
}),
displayName: i18n.translate('data.search.aggs.otherBucket.labelForOtherBucketLabel', {
defaultMessage: 'Label for other bucket',
}),
shouldShow: agg => agg.getParam('otherBucket'),
write: noop,
},
{
name: 'missingBucket',
default: false,
write: noop,
},
{
name: 'missingBucketLabel',
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"`,
}),
type: 'string',
displayName: i18n.translate('data.search.aggs.otherBucket.labelForMissingValuesLabel', {
defaultMessage: 'Label for missing values',
}),
shouldShow: agg => agg.getParam('missingBucket'),
write: noop,
}),
type: 'string',
displayName: i18n.translate('data.search.aggs.otherBucket.labelForMissingValuesLabel', {
defaultMessage: 'Label for missing values',
}),
shouldShow: agg => agg.getParam('missingBucket'),
write: noop,
},
{
name: 'exclude',
displayName: i18n.translate('data.search.aggs.buckets.terms.excludeLabel', {
defaultMessage: 'Exclude',
}),
type: 'string',
advanced: true,
shouldShow: isStringType,
...migrateIncludeExcludeFormat,
},
{
name: 'include',
displayName: i18n.translate('data.search.aggs.buckets.terms.includeLabel', {
defaultMessage: 'Include',
}),
type: 'string',
advanced: true,
shouldShow: isStringType,
...migrateIncludeExcludeFormat,
},
],
},
{
name: 'exclude',
displayName: i18n.translate('data.search.aggs.buckets.terms.excludeLabel', {
defaultMessage: 'Exclude',
}),
type: 'string',
advanced: true,
shouldShow: isStringType,
...migrateIncludeExcludeFormat,
},
{
name: 'include',
displayName: i18n.translate('data.search.aggs.buckets.terms.includeLabel', {
defaultMessage: 'Include',
}),
type: 'string',
advanced: true,
shouldShow: isStringType,
...migrateIncludeExcludeFormat,
},
],
});
{ getInternalStartServices }
);

View file

@ -20,16 +20,22 @@
import { coreMock } from '../../../../../../src/core/public/mocks';
import { getAggTypes } from './index';
import { isBucketAggType } from './buckets/_bucket_agg_type';
import { isBucketAggType } from './buckets/bucket_agg_type';
import { isMetricAggType } from './metrics/metric_agg_type';
import { QueryStart } from '../../query';
import { FieldFormatsStart } from '../../field_formats';
describe('AggTypesComponent', () => {
const core = coreMock.createSetup();
const coreSetup = coreMock.createSetup();
const coreStart = coreMock.createSetup();
const aggTypes = getAggTypes({
uiSettings: core.uiSettings,
notifications: core.notifications,
uiSettings: coreSetup.uiSettings,
query: {} as QueryStart,
getInternalStartServices: () => ({
notifications: coreStart.notifications,
fieldFormats: {} as FieldFormatsStart,
}),
});
const { buckets, metrics } = aggTypes;

View file

@ -21,25 +21,37 @@ import { i18n } from '@kbn/i18n';
import { MetricAggType } from './metric_agg_type';
import { METRIC_TYPES } from './metric_agg_types';
import { KBN_FIELD_TYPES } from '../../../../common';
import { GetInternalStartServicesFn } from '../../../types';
const averageTitle = i18n.translate('data.search.aggs.metrics.averageTitle', {
defaultMessage: 'Average',
});
export const avgMetricAgg = new MetricAggType({
name: METRIC_TYPES.AVG,
title: averageTitle,
makeLabel: aggConfig => {
return i18n.translate('data.search.aggs.metrics.averageLabel', {
defaultMessage: 'Average {field}',
values: { field: aggConfig.getFieldDisplayName() },
});
},
params: [
export interface AvgMetricAggDependencies {
getInternalStartServices: GetInternalStartServicesFn;
}
export const getAvgMetricAgg = ({ getInternalStartServices }: AvgMetricAggDependencies) => {
return new MetricAggType(
{
name: 'field',
type: 'field',
filterFieldTypes: KBN_FIELD_TYPES.NUMBER,
name: METRIC_TYPES.AVG,
title: averageTitle,
makeLabel: aggConfig => {
return i18n.translate('data.search.aggs.metrics.averageLabel', {
defaultMessage: 'Average {field}',
values: { field: aggConfig.getFieldDisplayName() },
});
},
params: [
{
name: 'field',
type: 'field',
filterFieldTypes: KBN_FIELD_TYPES.NUMBER,
},
],
},
],
});
{
getInternalStartServices,
}
);
};

View file

@ -23,6 +23,11 @@ import { MetricAggType } from './metric_agg_type';
import { makeNestedLabel } from './lib/make_nested_label';
import { siblingPipelineAggHelper } from './lib/sibling_pipeline_agg_helper';
import { METRIC_TYPES } from './metric_agg_types';
import { GetInternalStartServicesFn } from '../../../types';
export interface BucketAvgMetricAggDependencies {
getInternalStartServices: GetInternalStartServicesFn;
}
const overallAverageLabel = i18n.translate('data.search.aggs.metrics.overallAverageLabel', {
defaultMessage: 'overall average',
@ -32,25 +37,34 @@ const averageBucketTitle = i18n.translate('data.search.aggs.metrics.averageBucke
defaultMessage: 'Average Bucket',
});
export const bucketAvgMetricAgg = new MetricAggType({
name: METRIC_TYPES.AVG_BUCKET,
title: averageBucketTitle,
makeLabel: agg => makeNestedLabel(agg, overallAverageLabel),
subtype: siblingPipelineAggHelper.subtype,
params: [...siblingPipelineAggHelper.params()],
getFormat: siblingPipelineAggHelper.getFormat,
getValue(agg, bucket) {
const customMetric = agg.getParam('customMetric');
const customBucket = agg.getParam('customBucket');
const scaleMetrics = customMetric.type && customMetric.type.isScalable();
export const getBucketAvgMetricAgg = ({
getInternalStartServices,
}: BucketAvgMetricAggDependencies) => {
return new MetricAggType(
{
name: METRIC_TYPES.AVG_BUCKET,
title: averageBucketTitle,
makeLabel: agg => makeNestedLabel(agg, overallAverageLabel),
subtype: siblingPipelineAggHelper.subtype,
params: [...siblingPipelineAggHelper.params()],
getFormat: siblingPipelineAggHelper.getFormat,
getValue(agg, bucket) {
const customMetric = agg.getParam('customMetric');
const customBucket = agg.getParam('customBucket');
const scaleMetrics = customMetric.type && customMetric.type.isScalable();
let value = bucket[agg.id] && bucket[agg.id].value;
let value = bucket[agg.id] && bucket[agg.id].value;
if (scaleMetrics && customBucket.type.name === 'date_histogram') {
const aggInfo = customBucket.write();
if (scaleMetrics && customBucket.type.name === 'date_histogram') {
const aggInfo = customBucket.write();
value *= get(aggInfo, 'bucketInterval.scale', 1);
value *= get(aggInfo, 'bucketInterval.scale', 1);
}
return value;
},
},
{
getInternalStartServices,
}
return value;
},
});
);
};

View file

@ -22,6 +22,11 @@ import { MetricAggType } from './metric_agg_type';
import { makeNestedLabel } from './lib/make_nested_label';
import { siblingPipelineAggHelper } from './lib/sibling_pipeline_agg_helper';
import { METRIC_TYPES } from './metric_agg_types';
import { GetInternalStartServicesFn } from '../../../types';
export interface BucketMaxMetricAggDependencies {
getInternalStartServices: GetInternalStartServicesFn;
}
const overallMaxLabel = i18n.translate('data.search.aggs.metrics.overallMaxLabel', {
defaultMessage: 'overall max',
@ -31,11 +36,20 @@ const maxBucketTitle = i18n.translate('data.search.aggs.metrics.maxBucketTitle',
defaultMessage: 'Max Bucket',
});
export const bucketMaxMetricAgg = new MetricAggType({
name: METRIC_TYPES.MAX_BUCKET,
title: maxBucketTitle,
makeLabel: agg => makeNestedLabel(agg, overallMaxLabel),
subtype: siblingPipelineAggHelper.subtype,
params: [...siblingPipelineAggHelper.params()],
getFormat: siblingPipelineAggHelper.getFormat,
});
export const getBucketMaxMetricAgg = ({
getInternalStartServices,
}: BucketMaxMetricAggDependencies) => {
return new MetricAggType(
{
name: METRIC_TYPES.MAX_BUCKET,
title: maxBucketTitle,
makeLabel: agg => makeNestedLabel(agg, overallMaxLabel),
subtype: siblingPipelineAggHelper.subtype,
params: [...siblingPipelineAggHelper.params()],
getFormat: siblingPipelineAggHelper.getFormat,
},
{
getInternalStartServices,
}
);
};

View file

@ -22,6 +22,11 @@ import { MetricAggType } from './metric_agg_type';
import { makeNestedLabel } from './lib/make_nested_label';
import { siblingPipelineAggHelper } from './lib/sibling_pipeline_agg_helper';
import { METRIC_TYPES } from './metric_agg_types';
import { GetInternalStartServicesFn } from '../../../types';
export interface BucketMinMetricAggDependencies {
getInternalStartServices: GetInternalStartServicesFn;
}
const overallMinLabel = i18n.translate('data.search.aggs.metrics.overallMinLabel', {
defaultMessage: 'overall min',
@ -31,11 +36,20 @@ const minBucketTitle = i18n.translate('data.search.aggs.metrics.minBucketTitle',
defaultMessage: 'Min Bucket',
});
export const bucketMinMetricAgg = new MetricAggType({
name: METRIC_TYPES.MIN_BUCKET,
title: minBucketTitle,
makeLabel: agg => makeNestedLabel(agg, overallMinLabel),
subtype: siblingPipelineAggHelper.subtype,
params: [...siblingPipelineAggHelper.params()],
getFormat: siblingPipelineAggHelper.getFormat,
});
export const getBucketMinMetricAgg = ({
getInternalStartServices,
}: BucketMinMetricAggDependencies) => {
return new MetricAggType(
{
name: METRIC_TYPES.MIN_BUCKET,
title: minBucketTitle,
makeLabel: agg => makeNestedLabel(agg, overallMinLabel),
subtype: siblingPipelineAggHelper.subtype,
params: [...siblingPipelineAggHelper.params()],
getFormat: siblingPipelineAggHelper.getFormat,
},
{
getInternalStartServices,
}
);
};

View file

@ -22,6 +22,11 @@ import { MetricAggType } from './metric_agg_type';
import { makeNestedLabel } from './lib/make_nested_label';
import { siblingPipelineAggHelper } from './lib/sibling_pipeline_agg_helper';
import { METRIC_TYPES } from './metric_agg_types';
import { GetInternalStartServicesFn } from '../../../types';
export interface BucketSumMetricAggDependencies {
getInternalStartServices: GetInternalStartServicesFn;
}
const overallSumLabel = i18n.translate('data.search.aggs.metrics.overallSumLabel', {
defaultMessage: 'overall sum',
@ -31,11 +36,20 @@ const sumBucketTitle = i18n.translate('data.search.aggs.metrics.sumBucketTitle',
defaultMessage: 'Sum Bucket',
});
export const bucketSumMetricAgg = new MetricAggType({
name: METRIC_TYPES.SUM_BUCKET,
title: sumBucketTitle,
makeLabel: agg => makeNestedLabel(agg, overallSumLabel),
subtype: siblingPipelineAggHelper.subtype,
params: [...siblingPipelineAggHelper.params()],
getFormat: siblingPipelineAggHelper.getFormat,
});
export const getBucketSumMetricAgg = ({
getInternalStartServices,
}: BucketSumMetricAggDependencies) => {
return new MetricAggType(
{
name: METRIC_TYPES.SUM_BUCKET,
title: sumBucketTitle,
makeLabel: agg => makeNestedLabel(agg, overallSumLabel),
subtype: siblingPipelineAggHelper.subtype,
params: [...siblingPipelineAggHelper.params()],
getFormat: siblingPipelineAggHelper.getFormat,
},
{
getInternalStartServices,
}
);
};

View file

@ -18,36 +18,48 @@
*/
import { i18n } from '@kbn/i18n';
import { MetricAggType } from './metric_agg_type';
import { MetricAggType, IMetricAggConfig } from './metric_agg_type';
import { METRIC_TYPES } from './metric_agg_types';
import { KBN_FIELD_TYPES } from '../../../../common';
import { getFieldFormats } from '../../../../public/services';
import { GetInternalStartServicesFn } from '../../../types';
const uniqueCountTitle = i18n.translate('data.search.aggs.metrics.uniqueCountTitle', {
defaultMessage: 'Unique Count',
});
export const cardinalityMetricAgg = new MetricAggType({
name: METRIC_TYPES.CARDINALITY,
title: uniqueCountTitle,
makeLabel(aggConfig) {
return i18n.translate('data.search.aggs.metrics.uniqueCountLabel', {
defaultMessage: 'Unique count of {field}',
values: { field: aggConfig.getFieldDisplayName() },
});
},
getFormat() {
const fieldFormatsService = getFieldFormats();
export interface CardinalityMetricAggDependencies {
getInternalStartServices: GetInternalStartServicesFn;
}
return fieldFormatsService.getDefaultInstance(KBN_FIELD_TYPES.NUMBER);
},
params: [
export const getCardinalityMetricAgg = ({
getInternalStartServices,
}: CardinalityMetricAggDependencies) =>
new MetricAggType(
{
name: 'field',
type: 'field',
filterFieldTypes: Object.values(KBN_FIELD_TYPES).filter(
type => type !== KBN_FIELD_TYPES.HISTOGRAM
),
name: METRIC_TYPES.CARDINALITY,
title: uniqueCountTitle,
makeLabel(aggConfig: IMetricAggConfig) {
return i18n.translate('data.search.aggs.metrics.uniqueCountLabel', {
defaultMessage: 'Unique count of {field}',
values: { field: aggConfig.getFieldDisplayName() },
});
},
getFormat() {
const { fieldFormats } = getInternalStartServices();
return fieldFormats.getDefaultInstance(KBN_FIELD_TYPES.NUMBER);
},
params: [
{
name: 'field',
type: 'field',
filterFieldTypes: Object.values(KBN_FIELD_TYPES).filter(
type => type !== KBN_FIELD_TYPES.HISTOGRAM
),
},
],
},
],
});
{
getInternalStartServices,
}
);

View file

@ -21,28 +21,38 @@ import { i18n } from '@kbn/i18n';
import { MetricAggType } from './metric_agg_type';
import { METRIC_TYPES } from './metric_agg_types';
import { KBN_FIELD_TYPES } from '../../../../common';
import { getFieldFormats } from '../../../../public/services';
import { GetInternalStartServicesFn } from '../../../types';
export const countMetricAgg = new MetricAggType({
name: METRIC_TYPES.COUNT,
title: i18n.translate('data.search.aggs.metrics.countTitle', {
defaultMessage: 'Count',
}),
hasNoDsl: true,
makeLabel() {
return i18n.translate('data.search.aggs.metrics.countLabel', {
defaultMessage: 'Count',
});
},
getFormat() {
const fieldFormatsService = getFieldFormats();
export interface CountMetricAggDependencies {
getInternalStartServices: GetInternalStartServicesFn;
}
return fieldFormatsService.getDefaultInstance(KBN_FIELD_TYPES.NUMBER);
},
getValue(agg, bucket) {
return bucket.doc_count;
},
isScalable() {
return true;
},
});
export const getCountMetricAgg = ({ getInternalStartServices }: CountMetricAggDependencies) =>
new MetricAggType(
{
name: METRIC_TYPES.COUNT,
title: i18n.translate('data.search.aggs.metrics.countTitle', {
defaultMessage: 'Count',
}),
hasNoDsl: true,
makeLabel() {
return i18n.translate('data.search.aggs.metrics.countLabel', {
defaultMessage: 'Count',
});
},
getFormat() {
const { fieldFormats } = getInternalStartServices();
return fieldFormats.getDefaultInstance(KBN_FIELD_TYPES.NUMBER);
},
getValue(agg, bucket) {
return bucket.doc_count;
},
isScalable() {
return true;
},
},
{
getInternalStartServices,
}
);

View file

@ -22,6 +22,11 @@ import { MetricAggType } from './metric_agg_type';
import { parentPipelineAggHelper } from './lib/parent_pipeline_agg_helper';
import { makeNestedLabel } from './lib/make_nested_label';
import { METRIC_TYPES } from './metric_agg_types';
import { GetInternalStartServicesFn } from '../../../types';
export interface CumulativeSumMetricAggDependencies {
getInternalStartServices: GetInternalStartServicesFn;
}
const cumulativeSumLabel = i18n.translate('data.search.aggs.metrics.cumulativeSumLabel', {
defaultMessage: 'cumulative sum',
@ -31,11 +36,20 @@ const cumulativeSumTitle = i18n.translate('data.search.aggs.metrics.cumulativeSu
defaultMessage: 'Cumulative Sum',
});
export const cumulativeSumMetricAgg = new MetricAggType({
name: METRIC_TYPES.CUMULATIVE_SUM,
title: cumulativeSumTitle,
subtype: parentPipelineAggHelper.subtype,
makeLabel: agg => makeNestedLabel(agg, cumulativeSumLabel),
params: [...parentPipelineAggHelper.params()],
getFormat: parentPipelineAggHelper.getFormat,
});
export const getCumulativeSumMetricAgg = ({
getInternalStartServices,
}: CumulativeSumMetricAggDependencies) => {
return new MetricAggType(
{
name: METRIC_TYPES.CUMULATIVE_SUM,
title: cumulativeSumTitle,
subtype: parentPipelineAggHelper.subtype,
makeLabel: agg => makeNestedLabel(agg, cumulativeSumLabel),
params: [...parentPipelineAggHelper.params()],
getFormat: parentPipelineAggHelper.getFormat,
},
{
getInternalStartServices,
}
);
};

View file

@ -22,6 +22,11 @@ import { MetricAggType } from './metric_agg_type';
import { parentPipelineAggHelper } from './lib/parent_pipeline_agg_helper';
import { makeNestedLabel } from './lib/make_nested_label';
import { METRIC_TYPES } from './metric_agg_types';
import { GetInternalStartServicesFn } from '../../../types';
export interface DerivativeMetricAggDependencies {
getInternalStartServices: GetInternalStartServicesFn;
}
const derivativeLabel = i18n.translate('data.search.aggs.metrics.derivativeLabel', {
defaultMessage: 'derivative',
@ -31,13 +36,22 @@ const derivativeTitle = i18n.translate('data.search.aggs.metrics.derivativeTitle
defaultMessage: 'Derivative',
});
export const derivativeMetricAgg = new MetricAggType({
name: METRIC_TYPES.DERIVATIVE,
title: derivativeTitle,
subtype: parentPipelineAggHelper.subtype,
makeLabel(agg) {
return makeNestedLabel(agg, derivativeLabel);
},
params: [...parentPipelineAggHelper.params()],
getFormat: parentPipelineAggHelper.getFormat,
});
export const getDerivativeMetricAgg = ({
getInternalStartServices,
}: DerivativeMetricAggDependencies) => {
return new MetricAggType(
{
name: METRIC_TYPES.DERIVATIVE,
title: derivativeTitle,
subtype: parentPipelineAggHelper.subtype,
makeLabel(agg) {
return makeNestedLabel(agg, derivativeLabel);
},
params: [...parentPipelineAggHelper.params()],
getFormat: parentPipelineAggHelper.getFormat,
},
{
getInternalStartServices,
}
);
};

View file

@ -21,6 +21,11 @@ import { i18n } from '@kbn/i18n';
import { MetricAggType } from './metric_agg_type';
import { METRIC_TYPES } from './metric_agg_types';
import { KBN_FIELD_TYPES } from '../../../../common';
import { GetInternalStartServicesFn } from '../../../types';
export interface GeoBoundsMetricAggDependencies {
getInternalStartServices: GetInternalStartServicesFn;
}
const geoBoundsTitle = i18n.translate('data.search.aggs.metrics.geoBoundsTitle', {
defaultMessage: 'Geo Bounds',
@ -30,15 +35,24 @@ const geoBoundsLabel = i18n.translate('data.search.aggs.metrics.geoBoundsLabel',
defaultMessage: 'Geo Bounds',
});
export const geoBoundsMetricAgg = new MetricAggType({
name: METRIC_TYPES.GEO_BOUNDS,
title: geoBoundsTitle,
makeLabel: () => geoBoundsLabel,
params: [
export const getGeoBoundsMetricAgg = ({
getInternalStartServices,
}: GeoBoundsMetricAggDependencies) => {
return new MetricAggType(
{
name: 'field',
type: 'field',
filterFieldTypes: KBN_FIELD_TYPES.GEO_POINT,
name: METRIC_TYPES.GEO_BOUNDS,
title: geoBoundsTitle,
makeLabel: () => geoBoundsLabel,
params: [
{
name: 'field',
type: 'field',
filterFieldTypes: KBN_FIELD_TYPES.GEO_POINT,
},
],
},
],
});
{
getInternalStartServices,
}
);
};

View file

@ -21,6 +21,11 @@ import { i18n } from '@kbn/i18n';
import { MetricAggType } from './metric_agg_type';
import { METRIC_TYPES } from './metric_agg_types';
import { KBN_FIELD_TYPES } from '../../../../common';
import { GetInternalStartServicesFn } from '../../../types';
export interface GeoCentroidMetricAggDependencies {
getInternalStartServices: GetInternalStartServicesFn;
}
const geoCentroidTitle = i18n.translate('data.search.aggs.metrics.geoCentroidTitle', {
defaultMessage: 'Geo Centroid',
@ -30,18 +35,27 @@ const geoCentroidLabel = i18n.translate('data.search.aggs.metrics.geoCentroidLab
defaultMessage: 'Geo Centroid',
});
export const geoCentroidMetricAgg = new MetricAggType({
name: METRIC_TYPES.GEO_CENTROID,
title: geoCentroidTitle,
makeLabel: () => geoCentroidLabel,
params: [
export const getGeoCentroidMetricAgg = ({
getInternalStartServices,
}: GeoCentroidMetricAggDependencies) => {
return new MetricAggType(
{
name: 'field',
type: 'field',
filterFieldTypes: KBN_FIELD_TYPES.GEO_POINT,
name: METRIC_TYPES.GEO_CENTROID,
title: geoCentroidTitle,
makeLabel: () => geoCentroidLabel,
params: [
{
name: 'field',
type: 'field',
filterFieldTypes: KBN_FIELD_TYPES.GEO_POINT,
},
],
getValue(agg, bucket) {
return bucket[agg.id] && bucket[agg.id].location;
},
},
],
getValue(agg, bucket) {
return bucket[agg.id] && bucket[agg.id].location;
},
});
{
getInternalStartServices,
}
);
};

View file

@ -21,25 +21,37 @@ import { i18n } from '@kbn/i18n';
import { MetricAggType } from './metric_agg_type';
import { METRIC_TYPES } from './metric_agg_types';
import { KBN_FIELD_TYPES } from '../../../../common';
import { GetInternalStartServicesFn } from '../../../types';
const maxTitle = i18n.translate('data.search.aggs.metrics.maxTitle', {
defaultMessage: 'Max',
});
export const maxMetricAgg = new MetricAggType({
name: METRIC_TYPES.MAX,
title: maxTitle,
makeLabel(aggConfig) {
return i18n.translate('data.search.aggs.metrics.maxLabel', {
defaultMessage: 'Max {field}',
values: { field: aggConfig.getFieldDisplayName() },
});
},
params: [
export interface MaxMetricAggDependencies {
getInternalStartServices: GetInternalStartServicesFn;
}
export const getMaxMetricAgg = ({ getInternalStartServices }: MaxMetricAggDependencies) => {
return new MetricAggType(
{
name: 'field',
type: 'field',
filterFieldTypes: [KBN_FIELD_TYPES.NUMBER, KBN_FIELD_TYPES.DATE],
name: METRIC_TYPES.MAX,
title: maxTitle,
makeLabel(aggConfig) {
return i18n.translate('data.search.aggs.metrics.maxLabel', {
defaultMessage: 'Max {field}',
values: { field: aggConfig.getFieldDisplayName() },
});
},
params: [
{
name: 'field',
type: 'field',
filterFieldTypes: [KBN_FIELD_TYPES.NUMBER, KBN_FIELD_TYPES.DATE],
},
],
},
],
});
{
getInternalStartServices,
}
);
};

View file

@ -17,16 +17,24 @@
* under the License.
*/
import { medianMetricAgg } from './median';
import { getMedianMetricAgg, MedianMetricAggDependencies } from './median';
import { AggConfigs, IAggConfigs } from '../agg_configs';
import { mockAggTypesRegistry } from '../test_helpers';
import { METRIC_TYPES } from './metric_agg_types';
import { fieldFormatsServiceMock } from '../../../field_formats/mocks';
import { notificationServiceMock } from '../../../../../../../src/core/public/mocks';
describe('AggTypeMetricMedianProvider class', () => {
let aggConfigs: IAggConfigs;
const aggTypesDependencies: MedianMetricAggDependencies = {
getInternalStartServices: () => ({
fieldFormats: fieldFormatsServiceMock.createStartContract(),
notifications: notificationServiceMock.createStartContract(),
}),
};
beforeEach(() => {
const typesRegistry = mockAggTypesRegistry([medianMetricAgg]);
const typesRegistry = mockAggTypesRegistry([getMedianMetricAgg(aggTypesDependencies)]);
const field = {
name: 'bytes',
};

View file

@ -21,33 +21,49 @@ import { i18n } from '@kbn/i18n';
import { MetricAggType } from './metric_agg_type';
import { METRIC_TYPES } from './metric_agg_types';
import { KBN_FIELD_TYPES } from '../../../../common';
import { GetInternalStartServicesFn } from '../../../types';
const medianTitle = i18n.translate('data.search.aggs.metrics.medianTitle', {
defaultMessage: 'Median',
});
export const medianMetricAgg = new MetricAggType({
name: METRIC_TYPES.MEDIAN,
dslName: 'percentiles',
title: medianTitle,
makeLabel(aggConfig) {
return i18n.translate('data.search.aggs.metrics.medianLabel', {
defaultMessage: 'Median {field}',
values: { field: aggConfig.getFieldDisplayName() },
});
},
params: [
export interface MedianMetricAggDependencies {
getInternalStartServices: GetInternalStartServicesFn;
}
export const getMedianMetricAgg = ({ getInternalStartServices }: MedianMetricAggDependencies) => {
return new MetricAggType(
{
name: 'field',
type: 'field',
filterFieldTypes: [KBN_FIELD_TYPES.NUMBER, KBN_FIELD_TYPES.DATE, KBN_FIELD_TYPES.HISTOGRAM],
write(agg, output) {
output.params.field = agg.getParam('field').name;
output.params.percents = [50];
name: METRIC_TYPES.MEDIAN,
dslName: 'percentiles',
title: medianTitle,
makeLabel(aggConfig) {
return i18n.translate('data.search.aggs.metrics.medianLabel', {
defaultMessage: 'Median {field}',
values: { field: aggConfig.getFieldDisplayName() },
});
},
params: [
{
name: 'field',
type: 'field',
filterFieldTypes: [
KBN_FIELD_TYPES.NUMBER,
KBN_FIELD_TYPES.DATE,
KBN_FIELD_TYPES.HISTOGRAM,
],
write(agg, output) {
output.params.field = agg.getParam('field').name;
output.params.percents = [50];
},
},
],
getValue(agg, bucket) {
return bucket[agg.id].values['50.0'];
},
},
],
getValue(agg, bucket) {
return bucket[agg.id].values['50.0'];
},
});
{
getInternalStartServices,
}
);
};

View file

@ -23,8 +23,8 @@ import { AggParamType } from '../param_types/agg';
import { AggConfig } from '../agg_config';
import { METRIC_TYPES } from './metric_agg_types';
import { KBN_FIELD_TYPES } from '../../../../common';
import { getFieldFormats } from '../../../../public/services';
import { FieldTypes } from '../param_types';
import { GetInternalStartServicesFn } from '../../../types';
export interface IMetricAggConfig extends AggConfig {
type: InstanceType<typeof MetricAggType>;
@ -44,6 +44,10 @@ interface MetricAggTypeConfig<TMetricAggConfig extends AggConfig>
subtype?: string;
}
interface MetricAggTypeDependencies {
getInternalStartServices: GetInternalStartServicesFn;
}
// TODO need to make a more explicit interface for this
export type IMetricAggType = MetricAggType;
@ -57,8 +61,11 @@ export class MetricAggType<TMetricAggConfig extends AggConfig = IMetricAggConfig
getKey = () => {};
constructor(config: MetricAggTypeConfig<TMetricAggConfig>) {
super(config);
constructor(
config: MetricAggTypeConfig<TMetricAggConfig>,
dependencies: MetricAggTypeDependencies
) {
super(config, dependencies);
this.getValue =
config.getValue ||
@ -78,11 +85,9 @@ export class MetricAggType<TMetricAggConfig extends AggConfig = IMetricAggConfig
this.getFormat =
config.getFormat ||
(agg => {
const fieldFormatsService = getFieldFormats();
const { fieldFormats } = dependencies.getInternalStartServices();
const field = agg.getField();
return field
? field.format
: fieldFormatsService.getDefaultInstance(KBN_FIELD_TYPES.NUMBER);
return field ? field.format : fieldFormats.getDefaultInstance(KBN_FIELD_TYPES.NUMBER);
});
this.subtype =

View file

@ -21,25 +21,37 @@ import { i18n } from '@kbn/i18n';
import { MetricAggType } from './metric_agg_type';
import { METRIC_TYPES } from './metric_agg_types';
import { KBN_FIELD_TYPES } from '../../../../common';
import { GetInternalStartServicesFn } from '../../../types';
const minTitle = i18n.translate('data.search.aggs.metrics.minTitle', {
defaultMessage: 'Min',
});
export const minMetricAgg = new MetricAggType({
name: METRIC_TYPES.MIN,
title: minTitle,
makeLabel(aggConfig) {
return i18n.translate('data.search.aggs.metrics.minLabel', {
defaultMessage: 'Min {field}',
values: { field: aggConfig.getFieldDisplayName() },
});
},
params: [
export interface MinMetricAggDependencies {
getInternalStartServices: GetInternalStartServicesFn;
}
export const getMinMetricAgg = ({ getInternalStartServices }: MinMetricAggDependencies) => {
return new MetricAggType(
{
name: 'field',
type: 'field',
filterFieldTypes: [KBN_FIELD_TYPES.NUMBER, KBN_FIELD_TYPES.DATE],
name: METRIC_TYPES.MIN,
title: minTitle,
makeLabel(aggConfig) {
return i18n.translate('data.search.aggs.metrics.minLabel', {
defaultMessage: 'Min {field}',
values: { field: aggConfig.getFieldDisplayName() },
});
},
params: [
{
name: 'field',
type: 'field',
filterFieldTypes: [KBN_FIELD_TYPES.NUMBER, KBN_FIELD_TYPES.DATE],
},
],
},
],
});
{
getInternalStartServices,
}
);
};

View file

@ -22,6 +22,11 @@ import { MetricAggType } from './metric_agg_type';
import { parentPipelineAggHelper } from './lib/parent_pipeline_agg_helper';
import { makeNestedLabel } from './lib/make_nested_label';
import { METRIC_TYPES } from './metric_agg_types';
import { GetInternalStartServicesFn } from '../../../types';
export interface MovingAvgMetricAggDependencies {
getInternalStartServices: GetInternalStartServicesFn;
}
const movingAvgTitle = i18n.translate('data.search.aggs.metrics.movingAvgTitle', {
defaultMessage: 'Moving Avg',
@ -31,34 +36,43 @@ const movingAvgLabel = i18n.translate('data.search.aggs.metrics.movingAvgLabel',
defaultMessage: 'moving avg',
});
export const movingAvgMetricAgg = new MetricAggType({
name: METRIC_TYPES.MOVING_FN,
dslName: 'moving_fn',
title: movingAvgTitle,
subtype: parentPipelineAggHelper.subtype,
makeLabel: agg => makeNestedLabel(agg, movingAvgLabel),
params: [
...parentPipelineAggHelper.params(),
export const getMovingAvgMetricAgg = ({
getInternalStartServices,
}: MovingAvgMetricAggDependencies) => {
return new MetricAggType(
{
name: 'window',
default: 5,
name: METRIC_TYPES.MOVING_FN,
dslName: 'moving_fn',
title: movingAvgTitle,
subtype: parentPipelineAggHelper.subtype,
makeLabel: agg => makeNestedLabel(agg, movingAvgLabel),
params: [
...parentPipelineAggHelper.params(),
{
name: 'window',
default: 5,
},
{
name: 'script',
default: 'MovingFunctions.unweightedAvg(values)',
},
],
getValue(agg, bucket) {
/**
* The previous implementation using `moving_avg` did not
* return any bucket in case there are no documents or empty window.
* The `moving_fn` aggregation returns buckets with the value null if the
* window is empty or doesn't return any value if the sibiling metric
* is null. Since our generic MetricAggType.getValue implementation
* would return the value 0 for null buckets, we need a specific
* implementation here, that preserves the null value.
*/
return bucket[agg.id] ? bucket[agg.id].value : null;
},
getFormat: parentPipelineAggHelper.getFormat,
},
{
name: 'script',
default: 'MovingFunctions.unweightedAvg(values)',
},
],
getValue(agg, bucket) {
/**
* The previous implementation using `moving_avg` did not
* return any bucket in case there are no documents or empty window.
* The `moving_fn` aggregation returns buckets with the value null if the
* window is empty or doesn't return any value if the sibiling metric
* is null. Since our generic MetricAggType.getValue implementation
* would return the value 0 for null buckets, we need a specific
* implementation here, that preserves the null value.
*/
return bucket[agg.id] ? bucket[agg.id].value : null;
},
getFormat: parentPipelineAggHelper.getFormat,
});
getInternalStartServices,
}
);
};

View file

@ -17,26 +17,47 @@
* under the License.
*/
import { derivativeMetricAgg } from './derivative';
import { cumulativeSumMetricAgg } from './cumulative_sum';
import { movingAvgMetricAgg } from './moving_avg';
import { serialDiffMetricAgg } from './serial_diff';
import { getDerivativeMetricAgg } from './derivative';
import { getCumulativeSumMetricAgg } from './cumulative_sum';
import { getMovingAvgMetricAgg } from './moving_avg';
import { getSerialDiffMetricAgg } from './serial_diff';
import { AggConfigs } from '../agg_configs';
import { mockDataServices, mockAggTypesRegistry } from '../test_helpers';
import { mockAggTypesRegistry } from '../test_helpers';
import { IMetricAggConfig, MetricAggType } from './metric_agg_type';
import { fieldFormatsServiceMock } from '../../../field_formats/mocks';
import { GetInternalStartServicesFn } from '../../../types';
import { notificationServiceMock } from '../../../../../../../src/core/public/mocks';
describe('parent pipeline aggs', function() {
beforeEach(() => {
mockDataServices();
const getInternalStartServices: GetInternalStartServicesFn = () => ({
fieldFormats: fieldFormatsServiceMock.createStartContract(),
notifications: notificationServiceMock.createStartContract(),
});
const typesRegistry = mockAggTypesRegistry();
const metrics = [
{ name: 'derivative', title: 'Derivative', provider: derivativeMetricAgg },
{ name: 'cumulative_sum', title: 'Cumulative Sum', provider: cumulativeSumMetricAgg },
{ name: 'moving_avg', title: 'Moving Avg', provider: movingAvgMetricAgg, dslName: 'moving_fn' },
{ name: 'serial_diff', title: 'Serial Diff', provider: serialDiffMetricAgg },
{
name: 'derivative',
title: 'Derivative',
provider: getDerivativeMetricAgg({ getInternalStartServices }),
},
{
name: 'cumulative_sum',
title: 'Cumulative Sum',
provider: getCumulativeSumMetricAgg({ getInternalStartServices }),
},
{
name: 'moving_avg',
title: 'Moving Avg',
provider: getMovingAvgMetricAgg({ getInternalStartServices }),
dslName: 'moving_fn',
},
{
name: 'serial_diff',
title: 'Serial Diff',
provider: getSerialDiffMetricAgg({ getInternalStartServices }),
},
];
metrics.forEach(metric => {

View file

@ -17,18 +17,28 @@
* under the License.
*/
import { IPercentileRanksAggConfig, percentileRanksMetricAgg } from './percentile_ranks';
import {
IPercentileRanksAggConfig,
getPercentileRanksMetricAgg,
PercentileRanksMetricAggDependencies,
} from './percentile_ranks';
import { AggConfigs, IAggConfigs } from '../agg_configs';
import { mockDataServices, mockAggTypesRegistry } from '../test_helpers';
import { mockAggTypesRegistry } from '../test_helpers';
import { METRIC_TYPES } from './metric_agg_types';
import { fieldFormatsServiceMock } from '../../../field_formats/mocks';
import { notificationServiceMock } from '../../../../../../../src/core/public/mocks';
describe('AggTypesMetricsPercentileRanksProvider class', function() {
let aggConfigs: IAggConfigs;
const aggTypesDependencies: PercentileRanksMetricAggDependencies = {
getInternalStartServices: () => ({
fieldFormats: fieldFormatsServiceMock.createStartContract(),
notifications: notificationServiceMock.createStartContract(),
}),
};
beforeEach(() => {
mockDataServices();
const typesRegistry = mockAggTypesRegistry([percentileRanksMetricAgg]);
const typesRegistry = mockAggTypesRegistry([getPercentileRanksMetricAgg(aggTypesDependencies)]);
const field = {
name: 'bytes',
};
@ -65,7 +75,7 @@ describe('AggTypesMetricsPercentileRanksProvider class', function() {
});
it('uses the custom label if it is set', function() {
const responseAggs: any = percentileRanksMetricAgg.getResponseAggs(
const responseAggs: any = getPercentileRanksMetricAgg(aggTypesDependencies).getResponseAggs(
aggConfigs.aggs[0] as IPercentileRanksAggConfig
);

View file

@ -23,68 +23,86 @@ import { getResponseAggConfigClass, IResponseAggConfig } from './lib/get_respons
import { getPercentileValue } from './percentiles_get_value';
import { METRIC_TYPES } from './metric_agg_types';
import { FIELD_FORMAT_IDS, KBN_FIELD_TYPES } from '../../../../common';
import { getFieldFormats } from '../../../../public/services';
import { GetInternalStartServicesFn } from '../../../types';
// required by the values editor
export type IPercentileRanksAggConfig = IResponseAggConfig;
const valueProps = {
makeLabel(this: IPercentileRanksAggConfig) {
const fieldFormatsService = getFieldFormats();
const field = this.getField();
const format =
(field && field.format) || fieldFormatsService.getDefaultInstance(KBN_FIELD_TYPES.NUMBER);
const customLabel = this.getParam('customLabel');
const label = customLabel || this.getFieldDisplayName();
export interface PercentileRanksMetricAggDependencies {
getInternalStartServices: GetInternalStartServicesFn;
}
return i18n.translate('data.search.aggs.metrics.percentileRanks.valuePropsLabel', {
defaultMessage: 'Percentile rank {format} of "{label}"',
values: { format: format.convert(this.key, 'text'), label },
});
},
const getValueProps = (getInternalStartServices: GetInternalStartServicesFn) => {
return {
makeLabel(this: IPercentileRanksAggConfig) {
const { fieldFormats } = getInternalStartServices();
const field = this.getField();
const format =
(field && field.format) || fieldFormats.getDefaultInstance(KBN_FIELD_TYPES.NUMBER);
const customLabel = this.getParam('customLabel');
const label = customLabel || this.getFieldDisplayName();
return i18n.translate('data.search.aggs.metrics.percentileRanks.valuePropsLabel', {
defaultMessage: 'Percentile rank {format} of "{label}"',
values: { format: format.convert(this.key, 'text'), label },
});
},
};
};
export const percentileRanksMetricAgg = new MetricAggType<IPercentileRanksAggConfig>({
name: METRIC_TYPES.PERCENTILE_RANKS,
title: i18n.translate('data.search.aggs.metrics.percentileRanksTitle', {
defaultMessage: 'Percentile Ranks',
}),
makeLabel(agg) {
return i18n.translate('data.search.aggs.metrics.percentileRanksLabel', {
defaultMessage: 'Percentile ranks of {field}',
values: { field: agg.getFieldDisplayName() },
});
},
params: [
export const getPercentileRanksMetricAgg = ({
getInternalStartServices,
}: PercentileRanksMetricAggDependencies) => {
return new MetricAggType<IPercentileRanksAggConfig>(
{
name: 'field',
type: 'field',
filterFieldTypes: [KBN_FIELD_TYPES.NUMBER, KBN_FIELD_TYPES.HISTOGRAM],
},
{
name: 'values',
default: [],
},
{
write(agg, output) {
output.params.keyed = false;
name: METRIC_TYPES.PERCENTILE_RANKS,
title: i18n.translate('data.search.aggs.metrics.percentileRanksTitle', {
defaultMessage: 'Percentile Ranks',
}),
makeLabel(agg) {
return i18n.translate('data.search.aggs.metrics.percentileRanksLabel', {
defaultMessage: 'Percentile ranks of {field}',
values: { field: agg.getFieldDisplayName() },
});
},
params: [
{
name: 'field',
type: 'field',
filterFieldTypes: [KBN_FIELD_TYPES.NUMBER, KBN_FIELD_TYPES.HISTOGRAM],
},
{
name: 'values',
default: [],
},
{
write(agg, output) {
output.params.keyed = false;
},
},
],
getResponseAggs(agg) {
const ValueAggConfig = getResponseAggConfigClass(
agg,
getValueProps(getInternalStartServices)
);
const values = agg.getParam('values');
return values.map((value: any) => new ValueAggConfig(value));
},
getFormat() {
const { fieldFormats } = getInternalStartServices();
return (
fieldFormats.getInstance(FIELD_FORMAT_IDS.PERCENT) ||
fieldFormats.getDefaultInstance(KBN_FIELD_TYPES.NUMBER)
);
},
getValue(agg, bucket) {
return getPercentileValue(agg, bucket) / 100;
},
},
],
getResponseAggs(agg) {
const ValueAggConfig = getResponseAggConfigClass(agg, valueProps);
const values = agg.getParam('values');
return values.map((value: any) => new ValueAggConfig(value));
},
getFormat() {
const fieldFormatsService = getFieldFormats();
return (
fieldFormatsService.getInstance(FIELD_FORMAT_IDS.PERCENT) ||
fieldFormatsService.getDefaultInstance(KBN_FIELD_TYPES.NUMBER)
);
},
getValue(agg, bucket) {
return getPercentileValue(agg, bucket) / 100;
},
});
{
getInternalStartServices,
}
);
};

View file

@ -17,16 +17,28 @@
* under the License.
*/
import { IPercentileAggConfig, percentilesMetricAgg } from './percentiles';
import {
IPercentileAggConfig,
getPercentilesMetricAgg,
PercentilesMetricAggDependencies,
} from './percentiles';
import { AggConfigs, IAggConfigs } from '../agg_configs';
import { mockAggTypesRegistry } from '../test_helpers';
import { METRIC_TYPES } from './metric_agg_types';
import { fieldFormatsServiceMock } from '../../../field_formats/mocks';
import { notificationServiceMock } from '../../../../../../../src/core/public/mocks';
describe('AggTypesMetricsPercentilesProvider class', () => {
let aggConfigs: IAggConfigs;
const aggTypesDependencies: PercentilesMetricAggDependencies = {
getInternalStartServices: () => ({
fieldFormats: fieldFormatsServiceMock.createStartContract(),
notifications: notificationServiceMock.createStartContract(),
}),
};
beforeEach(() => {
const typesRegistry = mockAggTypesRegistry([percentilesMetricAgg]);
const typesRegistry = mockAggTypesRegistry([getPercentilesMetricAgg(aggTypesDependencies)]);
const field = {
name: 'bytes',
};
@ -63,7 +75,7 @@ describe('AggTypesMetricsPercentilesProvider class', () => {
});
it('uses the custom label if it is set', () => {
const responseAggs: any = percentilesMetricAgg.getResponseAggs(
const responseAggs: any = getPercentilesMetricAgg(aggTypesDependencies).getResponseAggs(
aggConfigs.aggs[0] as IPercentileAggConfig
);

View file

@ -24,9 +24,14 @@ import { KBN_FIELD_TYPES } from '../../../../common';
import { getResponseAggConfigClass, IResponseAggConfig } from './lib/get_response_agg_config_class';
import { getPercentileValue } from './percentiles_get_value';
import { ordinalSuffix } from './lib/ordinal_suffix';
import { GetInternalStartServicesFn } from '../../../types';
export type IPercentileAggConfig = IResponseAggConfig;
export interface PercentilesMetricAggDependencies {
getInternalStartServices: GetInternalStartServicesFn;
}
const valueProps = {
makeLabel(this: IPercentileAggConfig) {
const customLabel = this.getParam('customLabel');
@ -39,38 +44,51 @@ const valueProps = {
},
};
export const percentilesMetricAgg = new MetricAggType<IPercentileAggConfig>({
name: METRIC_TYPES.PERCENTILES,
title: i18n.translate('data.search.aggs.metrics.percentilesTitle', {
defaultMessage: 'Percentiles',
}),
makeLabel(agg) {
return i18n.translate('data.search.aggs.metrics.percentilesLabel', {
defaultMessage: 'Percentiles of {field}',
values: { field: agg.getFieldDisplayName() },
});
},
params: [
export const getPercentilesMetricAgg = ({
getInternalStartServices,
}: PercentilesMetricAggDependencies) => {
return new MetricAggType<IPercentileAggConfig>(
{
name: 'field',
type: 'field',
filterFieldTypes: [KBN_FIELD_TYPES.NUMBER, KBN_FIELD_TYPES.DATE, KBN_FIELD_TYPES.HISTOGRAM],
},
{
name: 'percents',
default: [1, 5, 25, 50, 75, 95, 99],
},
{
write(agg, output) {
output.params.keyed = false;
name: METRIC_TYPES.PERCENTILES,
title: i18n.translate('data.search.aggs.metrics.percentilesTitle', {
defaultMessage: 'Percentiles',
}),
makeLabel(agg) {
return i18n.translate('data.search.aggs.metrics.percentilesLabel', {
defaultMessage: 'Percentiles of {field}',
values: { field: agg.getFieldDisplayName() },
});
},
params: [
{
name: 'field',
type: 'field',
filterFieldTypes: [
KBN_FIELD_TYPES.NUMBER,
KBN_FIELD_TYPES.DATE,
KBN_FIELD_TYPES.HISTOGRAM,
],
},
{
name: 'percents',
default: [1, 5, 25, 50, 75, 95, 99],
},
{
write(agg, output) {
output.params.keyed = false;
},
},
],
getResponseAggs(agg) {
const ValueAggConfig = getResponseAggConfigClass(agg, valueProps);
return agg.getParam('percents').map((percent: any) => new ValueAggConfig(percent));
},
getValue: getPercentileValue,
},
],
getResponseAggs(agg) {
const ValueAggConfig = getResponseAggConfigClass(agg, valueProps);
return agg.getParam('percents').map((percent: any) => new ValueAggConfig(percent));
},
getValue: getPercentileValue,
});
{
getInternalStartServices,
}
);
};

View file

@ -22,6 +22,11 @@ import { MetricAggType } from './metric_agg_type';
import { parentPipelineAggHelper } from './lib/parent_pipeline_agg_helper';
import { makeNestedLabel } from './lib/make_nested_label';
import { METRIC_TYPES } from './metric_agg_types';
import { GetInternalStartServicesFn } from '../../../types';
export interface SerialDiffMetricAggDependencies {
getInternalStartServices: GetInternalStartServicesFn;
}
const serialDiffTitle = i18n.translate('data.search.aggs.metrics.serialDiffTitle', {
defaultMessage: 'Serial Diff',
@ -31,11 +36,20 @@ const serialDiffLabel = i18n.translate('data.search.aggs.metrics.serialDiffLabel
defaultMessage: 'serial diff',
});
export const serialDiffMetricAgg = new MetricAggType({
name: METRIC_TYPES.SERIAL_DIFF,
title: serialDiffTitle,
subtype: parentPipelineAggHelper.subtype,
makeLabel: agg => makeNestedLabel(agg, serialDiffLabel),
params: [...parentPipelineAggHelper.params()],
getFormat: parentPipelineAggHelper.getFormat,
});
export const getSerialDiffMetricAgg = ({
getInternalStartServices,
}: SerialDiffMetricAggDependencies) => {
return new MetricAggType(
{
name: METRIC_TYPES.SERIAL_DIFF,
title: serialDiffTitle,
subtype: parentPipelineAggHelper.subtype,
makeLabel: agg => makeNestedLabel(agg, serialDiffLabel),
params: [...parentPipelineAggHelper.params()],
getFormat: parentPipelineAggHelper.getFormat,
},
{
getInternalStartServices,
}
);
};

View file

@ -17,27 +17,47 @@
* under the License.
*/
import { bucketSumMetricAgg } from './bucket_sum';
import { bucketAvgMetricAgg } from './bucket_avg';
import { bucketMinMetricAgg } from './bucket_min';
import { bucketMaxMetricAgg } from './bucket_max';
import { getBucketSumMetricAgg } from './bucket_sum';
import { getBucketAvgMetricAgg } from './bucket_avg';
import { getBucketMinMetricAgg } from './bucket_min';
import { getBucketMaxMetricAgg } from './bucket_max';
import { AggConfigs } from '../agg_configs';
import { IMetricAggConfig, MetricAggType } from './metric_agg_type';
import { mockDataServices, mockAggTypesRegistry } from '../test_helpers';
import { mockAggTypesRegistry } from '../test_helpers';
import { fieldFormatsServiceMock } from '../../../field_formats/mocks';
import { GetInternalStartServicesFn } from '../../../types';
import { notificationServiceMock } from '../../../../../../../src/core/public/mocks';
describe('sibling pipeline aggs', () => {
beforeEach(() => {
mockDataServices();
const getInternalStartServices: GetInternalStartServicesFn = () => ({
fieldFormats: fieldFormatsServiceMock.createStartContract(),
notifications: notificationServiceMock.createStartContract(),
});
const typesRegistry = mockAggTypesRegistry();
const metrics = [
{ name: 'sum_bucket', title: 'Overall Sum', provider: bucketSumMetricAgg },
{ name: 'avg_bucket', title: 'Overall Average', provider: bucketAvgMetricAgg },
{ name: 'min_bucket', title: 'Overall Min', provider: bucketMinMetricAgg },
{ name: 'max_bucket', title: 'Overall Max', provider: bucketMaxMetricAgg },
{
name: 'sum_bucket',
title: 'Overall Sum',
provider: getBucketSumMetricAgg({ getInternalStartServices }),
},
{
name: 'avg_bucket',
title: 'Overall Average',
provider: getBucketAvgMetricAgg({ getInternalStartServices }),
},
{
name: 'min_bucket',
title: 'Overall Min',
provider: getBucketMinMetricAgg({ getInternalStartServices }),
},
{
name: 'max_bucket',
title: 'Overall Max',
provider: getBucketMaxMetricAgg({ getInternalStartServices }),
},
];
metrics.forEach(metric => {

View file

@ -17,13 +17,25 @@
* under the License.
*/
import { IStdDevAggConfig, stdDeviationMetricAgg } from './std_deviation';
import {
IStdDevAggConfig,
getStdDeviationMetricAgg,
StdDeviationMetricAggDependencies,
} from './std_deviation';
import { AggConfigs } from '../agg_configs';
import { mockAggTypesRegistry } from '../test_helpers';
import { METRIC_TYPES } from './metric_agg_types';
import { fieldFormatsServiceMock } from '../../../field_formats/mocks';
import { notificationServiceMock } from '../../../../../../../src/core/public/mocks';
describe('AggTypeMetricStandardDeviationProvider class', () => {
const typesRegistry = mockAggTypesRegistry([stdDeviationMetricAgg]);
const aggTypesDependencies: StdDeviationMetricAggDependencies = {
getInternalStartServices: () => ({
fieldFormats: fieldFormatsServiceMock.createStartContract(),
notifications: notificationServiceMock.createStartContract(),
}),
};
const typesRegistry = mockAggTypesRegistry([getStdDeviationMetricAgg(aggTypesDependencies)]);
const getAggConfigs = (customLabel?: string) => {
const field = {
name: 'memory',
@ -58,7 +70,7 @@ describe('AggTypeMetricStandardDeviationProvider class', () => {
it('uses the custom label if it is set', () => {
const aggConfigs = getAggConfigs('custom label');
const responseAggs: any = stdDeviationMetricAgg.getResponseAggs(
const responseAggs: any = getStdDeviationMetricAgg(aggTypesDependencies).getResponseAggs(
aggConfigs.aggs[0] as IStdDevAggConfig
);
@ -72,7 +84,7 @@ describe('AggTypeMetricStandardDeviationProvider class', () => {
it('uses the default labels if custom label is not set', () => {
const aggConfigs = getAggConfigs();
const responseAggs: any = stdDeviationMetricAgg.getResponseAggs(
const responseAggs: any = getStdDeviationMetricAgg(aggTypesDependencies).getResponseAggs(
aggConfigs.aggs[0] as IStdDevAggConfig
);

View file

@ -23,6 +23,7 @@ import { MetricAggType } from './metric_agg_type';
import { METRIC_TYPES } from './metric_agg_types';
import { getResponseAggConfigClass, IResponseAggConfig } from './lib/get_response_agg_config_class';
import { KBN_FIELD_TYPES } from '../../../../common';
import { GetInternalStartServicesFn } from '../../../types';
interface ValProp {
valProp: string[];
@ -34,6 +35,10 @@ export interface IStdDevAggConfig extends IResponseAggConfig {
valProp: () => ValProp;
}
export interface StdDeviationMetricAggDependencies {
getInternalStartServices: GetInternalStartServicesFn;
}
const responseAggConfigProps = {
valProp(this: IStdDevAggConfig) {
const customLabel = this.getParam('customLabel');
@ -75,33 +80,42 @@ const responseAggConfigProps = {
},
};
export const stdDeviationMetricAgg = new MetricAggType<IStdDevAggConfig>({
name: METRIC_TYPES.STD_DEV,
dslName: 'extended_stats',
title: i18n.translate('data.search.aggs.metrics.standardDeviationTitle', {
defaultMessage: 'Standard Deviation',
}),
makeLabel(agg) {
return i18n.translate('data.search.aggs.metrics.standardDeviationLabel', {
defaultMessage: 'Standard Deviation of {field}',
values: { field: agg.getFieldDisplayName() },
});
},
params: [
export const getStdDeviationMetricAgg = ({
getInternalStartServices,
}: StdDeviationMetricAggDependencies) => {
return new MetricAggType<IStdDevAggConfig>(
{
name: 'field',
type: 'field',
filterFieldTypes: KBN_FIELD_TYPES.NUMBER,
name: METRIC_TYPES.STD_DEV,
dslName: 'extended_stats',
title: i18n.translate('data.search.aggs.metrics.standardDeviationTitle', {
defaultMessage: 'Standard Deviation',
}),
makeLabel(agg) {
return i18n.translate('data.search.aggs.metrics.standardDeviationLabel', {
defaultMessage: 'Standard Deviation of {field}',
values: { field: agg.getFieldDisplayName() },
});
},
params: [
{
name: 'field',
type: 'field',
filterFieldTypes: KBN_FIELD_TYPES.NUMBER,
},
],
getResponseAggs(agg) {
const ValueAggConfig = getResponseAggConfigClass(agg, responseAggConfigProps);
return [new ValueAggConfig('std_lower'), new ValueAggConfig('std_upper')];
},
getValue(agg, bucket) {
return get(bucket[agg.parentId], agg.valProp());
},
},
],
getResponseAggs(agg) {
const ValueAggConfig = getResponseAggConfigClass(agg, responseAggConfigProps);
return [new ValueAggConfig('std_lower'), new ValueAggConfig('std_upper')];
},
getValue(agg, bucket) {
return get(bucket[agg.parentId], agg.valProp());
},
});
{
getInternalStartServices,
}
);
};

View file

@ -21,28 +21,40 @@ import { i18n } from '@kbn/i18n';
import { MetricAggType } from './metric_agg_type';
import { METRIC_TYPES } from './metric_agg_types';
import { KBN_FIELD_TYPES } from '../../../../common';
import { GetInternalStartServicesFn } from '../../../types';
const sumTitle = i18n.translate('data.search.aggs.metrics.sumTitle', {
defaultMessage: 'Sum',
});
export const sumMetricAgg = new MetricAggType({
name: METRIC_TYPES.SUM,
title: sumTitle,
makeLabel(aggConfig) {
return i18n.translate('data.search.aggs.metrics.sumLabel', {
defaultMessage: 'Sum of {field}',
values: { field: aggConfig.getFieldDisplayName() },
});
},
isScalable() {
return true;
},
params: [
export interface SumMetricAggDependencies {
getInternalStartServices: GetInternalStartServicesFn;
}
export const getSumMetricAgg = ({ getInternalStartServices }: SumMetricAggDependencies) => {
return new MetricAggType(
{
name: 'field',
type: 'field',
filterFieldTypes: KBN_FIELD_TYPES.NUMBER,
name: METRIC_TYPES.SUM,
title: sumTitle,
makeLabel(aggConfig) {
return i18n.translate('data.search.aggs.metrics.sumLabel', {
defaultMessage: 'Sum of {field}',
values: { field: aggConfig.getFieldDisplayName() },
});
},
isScalable() {
return true;
},
params: [
{
name: 'field',
type: 'field',
filterFieldTypes: KBN_FIELD_TYPES.NUMBER,
},
],
},
],
});
{
getInternalStartServices,
}
);
};

View file

@ -18,15 +18,23 @@
*/
import { dropRight, last } from 'lodash';
import { topHitMetricAgg } from './top_hit';
import { getTopHitMetricAgg, TopHitMetricAggDependencies } from './top_hit';
import { AggConfigs } from '../agg_configs';
import { mockAggTypesRegistry } from '../test_helpers';
import { IMetricAggConfig } from './metric_agg_type';
import { KBN_FIELD_TYPES } from '../../../../common';
import { fieldFormatsServiceMock } from '../../../field_formats/mocks';
import { notificationServiceMock } from '../../../../../../../src/core/public/mocks';
describe('Top hit metric', () => {
let aggDsl: Record<string, any>;
let aggConfig: IMetricAggConfig;
const aggTypesDependencies: TopHitMetricAggDependencies = {
getInternalStartServices: () => ({
fieldFormats: fieldFormatsServiceMock.createStartContract(),
notifications: notificationServiceMock.createStartContract(),
}),
};
const init = ({
fieldName = 'field',
@ -36,7 +44,7 @@ describe('Top hit metric', () => {
fieldType = KBN_FIELD_TYPES.NUMBER,
size = 1,
}: any) => {
const typesRegistry = mockAggTypesRegistry([topHitMetricAgg]);
const typesRegistry = mockAggTypesRegistry([getTopHitMetricAgg(aggTypesDependencies)]);
const field = {
name: fieldName,
displayName: fieldName,
@ -91,7 +99,7 @@ describe('Top hit metric', () => {
it('should return a label prefixed with Last if sorting in descending order', () => {
init({ fieldName: 'bytes' });
expect(topHitMetricAgg.makeLabel(aggConfig)).toEqual('Last bytes');
expect(getTopHitMetricAgg(aggTypesDependencies).makeLabel(aggConfig)).toEqual('Last bytes');
});
it('should return a label prefixed with First if sorting in ascending order', () => {
@ -99,7 +107,7 @@ describe('Top hit metric', () => {
fieldName: 'bytes',
sortOrder: 'asc',
});
expect(topHitMetricAgg.makeLabel(aggConfig)).toEqual('First bytes');
expect(getTopHitMetricAgg(aggTypesDependencies).makeLabel(aggConfig)).toEqual('First bytes');
});
it('should request the _source field', () => {
@ -140,7 +148,7 @@ describe('Top hit metric', () => {
};
init({ fieldName: '@tags' });
expect(topHitMetricAgg.getValue(aggConfig, bucket)).toBe(null);
expect(getTopHitMetricAgg(aggTypesDependencies).getValue(aggConfig, bucket)).toBe(null);
});
//
it('should return undefined if the field does not appear in the source', () => {
@ -159,7 +167,7 @@ describe('Top hit metric', () => {
};
init({ fieldName: '@tags' });
expect(topHitMetricAgg.getValue(aggConfig, bucket)).toBe(undefined);
expect(getTopHitMetricAgg(aggTypesDependencies).getValue(aggConfig, bucket)).toBe(undefined);
});
it('should return the field value from the top hit', () => {
@ -178,7 +186,7 @@ describe('Top hit metric', () => {
};
init({ fieldName: '@tags' });
expect(topHitMetricAgg.getValue(aggConfig, bucket)).toBe('aaa');
expect(getTopHitMetricAgg(aggTypesDependencies).getValue(aggConfig, bucket)).toBe('aaa');
});
it('should return the object if the field value is an object', () => {
@ -200,7 +208,9 @@ describe('Top hit metric', () => {
init({ fieldName: '@tags' });
expect(topHitMetricAgg.getValue(aggConfig, bucket)).toEqual({ label: 'aaa' });
expect(getTopHitMetricAgg(aggTypesDependencies).getValue(aggConfig, bucket)).toEqual({
label: 'aaa',
});
});
it('should return an array if the field has more than one values', () => {
@ -219,7 +229,10 @@ describe('Top hit metric', () => {
};
init({ fieldName: '@tags' });
expect(topHitMetricAgg.getValue(aggConfig, bucket)).toEqual(['aaa', 'bbb']);
expect(getTopHitMetricAgg(aggTypesDependencies).getValue(aggConfig, bucket)).toEqual([
'aaa',
'bbb',
]);
});
it('should return undefined if the field is not in the source nor in the doc_values field', () => {
@ -241,7 +254,7 @@ describe('Top hit metric', () => {
};
init({ fieldName: 'machine.os.raw', readFromDocValues: true });
expect(topHitMetricAgg.getValue(aggConfig, bucket)).toBe(undefined);
expect(getTopHitMetricAgg(aggTypesDependencies).getValue(aggConfig, bucket)).toBe(undefined);
});
describe('Multivalued field and first/last X docs', () => {
@ -250,7 +263,9 @@ describe('Top hit metric', () => {
fieldName: 'bytes',
size: 2,
});
expect(topHitMetricAgg.makeLabel(aggConfig)).toEqual('Last 2 bytes');
expect(getTopHitMetricAgg(aggTypesDependencies).makeLabel(aggConfig)).toEqual(
'Last 2 bytes'
);
});
it('should return a label prefixed with First X docs if sorting in ascending order', () => {
@ -259,7 +274,9 @@ describe('Top hit metric', () => {
size: 2,
sortOrder: 'asc',
});
expect(topHitMetricAgg.makeLabel(aggConfig)).toEqual('First 2 bytes');
expect(getTopHitMetricAgg(aggTypesDependencies).makeLabel(aggConfig)).toEqual(
'First 2 bytes'
);
});
[
@ -334,7 +351,9 @@ describe('Top hit metric', () => {
};
init({ fieldName: 'bytes', aggregate: agg.type });
expect(topHitMetricAgg.getValue(aggConfig, bucket)).toEqual(agg.result);
expect(getTopHitMetricAgg(aggTypesDependencies).getValue(aggConfig, bucket)).toEqual(
agg.result
);
});
it(`should return the result of the ${agg.type} aggregation over the last X docs - ${agg.description}`, () => {
@ -358,7 +377,9 @@ describe('Top hit metric', () => {
};
init({ fieldName: 'bytes', aggregate: agg.type });
expect(topHitMetricAgg.getValue(aggConfig, bucket)).toEqual(agg.result);
expect(getTopHitMetricAgg(aggTypesDependencies).getValue(aggConfig, bucket)).toEqual(
agg.result
);
});
});
});

View file

@ -22,6 +22,11 @@ import { i18n } from '@kbn/i18n';
import { IMetricAggConfig, MetricAggType } from './metric_agg_type';
import { METRIC_TYPES } from './metric_agg_types';
import { KBN_FIELD_TYPES } from '../../../../common';
import { GetInternalStartServicesFn } from '../../../types';
export interface TopHitMetricAggDependencies {
getInternalStartServices: GetInternalStartServicesFn;
}
const isNumericFieldSelected = (agg: IMetricAggConfig) => {
const field = agg.getParam('field');
@ -29,214 +34,225 @@ const isNumericFieldSelected = (agg: IMetricAggConfig) => {
return field && field.type && field.type === KBN_FIELD_TYPES.NUMBER;
};
export const topHitMetricAgg = new MetricAggType({
name: METRIC_TYPES.TOP_HITS,
title: i18n.translate('data.search.aggs.metrics.topHitTitle', {
defaultMessage: 'Top Hit',
}),
makeLabel(aggConfig) {
const lastPrefixLabel = i18n.translate('data.search.aggs.metrics.topHit.lastPrefixLabel', {
defaultMessage: 'Last',
});
const firstPrefixLabel = i18n.translate('data.search.aggs.metrics.topHit.firstPrefixLabel', {
defaultMessage: 'First',
});
let prefix =
aggConfig.getParam('sortOrder').value === 'desc' ? lastPrefixLabel : firstPrefixLabel;
const size = aggConfig.getParam('size');
if (size !== 1) {
prefix += ` ${size}`;
}
const field = aggConfig.getParam('field');
return `${prefix} ${field ? field.displayName : ''}`;
},
params: [
export const getTopHitMetricAgg = ({ getInternalStartServices }: TopHitMetricAggDependencies) => {
return new MetricAggType(
{
name: 'field',
type: 'field',
onlyAggregatable: false,
filterFieldTypes: Object.values(KBN_FIELD_TYPES).filter(
type => type !== KBN_FIELD_TYPES.HISTOGRAM
),
write(agg, output) {
const field = agg.getParam('field');
output.params = {};
if (field.scripted) {
output.params.script_fields = {
[field.name]: {
script: {
source: field.script,
lang: field.lang,
},
},
};
} else {
if (field.readFromDocValues) {
// always format date fields as date_time to avoid
// displaying unformatted dates like epoch_millis
// or other not-accepted momentjs formats
const format = field.type === KBN_FIELD_TYPES.DATE ? 'date_time' : 'use_field_mapping';
output.params.docvalue_fields = [{ field: field.name, format }];
name: METRIC_TYPES.TOP_HITS,
title: i18n.translate('data.search.aggs.metrics.topHitTitle', {
defaultMessage: 'Top Hit',
}),
makeLabel(aggConfig) {
const lastPrefixLabel = i18n.translate('data.search.aggs.metrics.topHit.lastPrefixLabel', {
defaultMessage: 'Last',
});
const firstPrefixLabel = i18n.translate(
'data.search.aggs.metrics.topHit.firstPrefixLabel',
{
defaultMessage: 'First',
}
output.params._source = field.name === '_source' ? true : field.name;
}
},
},
{
name: 'aggregate',
type: 'optioned',
options: [
{
text: i18n.translate('data.search.aggs.metrics.topHit.minLabel', {
defaultMessage: 'Min',
}),
isCompatible: isNumericFieldSelected,
disabled: true,
value: 'min',
},
{
text: i18n.translate('data.search.aggs.metrics.topHit.maxLabel', {
defaultMessage: 'Max',
}),
isCompatible: isNumericFieldSelected,
disabled: true,
value: 'max',
},
{
text: i18n.translate('data.search.aggs.metrics.topHit.sumLabel', {
defaultMessage: 'Sum',
}),
isCompatible: isNumericFieldSelected,
disabled: true,
value: 'sum',
},
{
text: i18n.translate('data.search.aggs.metrics.topHit.averageLabel', {
defaultMessage: 'Average',
}),
isCompatible: isNumericFieldSelected,
disabled: true,
value: 'average',
},
{
text: i18n.translate('data.search.aggs.metrics.topHit.concatenateLabel', {
defaultMessage: 'Concatenate',
}),
isCompatible(aggConfig: IMetricAggConfig) {
return _.get(aggConfig.params, 'field.filterFieldTypes', '*') === '*';
},
disabled: true,
value: 'concat',
},
],
write: _.noop,
},
{
name: 'size',
default: 1,
},
{
name: 'sortField',
type: 'field',
filterFieldTypes: [
KBN_FIELD_TYPES.NUMBER,
KBN_FIELD_TYPES.DATE,
KBN_FIELD_TYPES.IP,
KBN_FIELD_TYPES.STRING,
],
default(agg: IMetricAggConfig) {
return agg.getIndexPattern().timeFieldName;
},
write: _.noop, // prevent default write, it is handled below
},
{
name: 'sortOrder',
type: 'optioned',
default: 'desc',
options: [
{
text: i18n.translate('data.search.aggs.metrics.topHit.descendingLabel', {
defaultMessage: 'Descending',
}),
value: 'desc',
},
{
text: i18n.translate('data.search.aggs.metrics.topHit.ascendingLabel', {
defaultMessage: 'Ascending',
}),
value: 'asc',
},
],
write(agg, output) {
const sortField = agg.params.sortField;
const sortOrder = agg.params.sortOrder;
);
if (sortField.scripted) {
output.params.sort = [
{
_script: {
script: {
source: sortField.script,
lang: sortField.lang,
let prefix =
aggConfig.getParam('sortOrder').value === 'desc' ? lastPrefixLabel : firstPrefixLabel;
const size = aggConfig.getParam('size');
if (size !== 1) {
prefix += ` ${size}`;
}
const field = aggConfig.getParam('field');
return `${prefix} ${field ? field.displayName : ''}`;
},
params: [
{
name: 'field',
type: 'field',
onlyAggregatable: false,
filterFieldTypes: Object.values(KBN_FIELD_TYPES).filter(
type => type !== KBN_FIELD_TYPES.HISTOGRAM
),
write(agg, output) {
const field = agg.getParam('field');
output.params = {};
if (field.scripted) {
output.params.script_fields = {
[field.name]: {
script: {
source: field.script,
lang: field.lang,
},
},
type: sortField.type,
order: sortOrder.value,
},
},
];
} else {
output.params.sort = [
};
} else {
if (field.readFromDocValues) {
// always format date fields as date_time to avoid
// displaying unformatted dates like epoch_millis
// or other not-accepted momentjs formats
const format =
field.type === KBN_FIELD_TYPES.DATE ? 'date_time' : 'use_field_mapping';
output.params.docvalue_fields = [{ field: field.name, format }];
}
output.params._source = field.name === '_source' ? true : field.name;
}
},
},
{
name: 'aggregate',
type: 'optioned',
options: [
{
[sortField.name]: {
order: sortOrder.value,
},
text: i18n.translate('data.search.aggs.metrics.topHit.minLabel', {
defaultMessage: 'Min',
}),
isCompatible: isNumericFieldSelected,
disabled: true,
value: 'min',
},
];
{
text: i18n.translate('data.search.aggs.metrics.topHit.maxLabel', {
defaultMessage: 'Max',
}),
isCompatible: isNumericFieldSelected,
disabled: true,
value: 'max',
},
{
text: i18n.translate('data.search.aggs.metrics.topHit.sumLabel', {
defaultMessage: 'Sum',
}),
isCompatible: isNumericFieldSelected,
disabled: true,
value: 'sum',
},
{
text: i18n.translate('data.search.aggs.metrics.topHit.averageLabel', {
defaultMessage: 'Average',
}),
isCompatible: isNumericFieldSelected,
disabled: true,
value: 'average',
},
{
text: i18n.translate('data.search.aggs.metrics.topHit.concatenateLabel', {
defaultMessage: 'Concatenate',
}),
isCompatible(aggConfig: IMetricAggConfig) {
return _.get(aggConfig.params, 'field.filterFieldTypes', '*') === '*';
},
disabled: true,
value: 'concat',
},
],
write: _.noop,
},
{
name: 'size',
default: 1,
},
{
name: 'sortField',
type: 'field',
filterFieldTypes: [
KBN_FIELD_TYPES.NUMBER,
KBN_FIELD_TYPES.DATE,
KBN_FIELD_TYPES.IP,
KBN_FIELD_TYPES.STRING,
],
default(agg: IMetricAggConfig) {
return agg.getIndexPattern().timeFieldName;
},
write: _.noop, // prevent default write, it is handled below
},
{
name: 'sortOrder',
type: 'optioned',
default: 'desc',
options: [
{
text: i18n.translate('data.search.aggs.metrics.topHit.descendingLabel', {
defaultMessage: 'Descending',
}),
value: 'desc',
},
{
text: i18n.translate('data.search.aggs.metrics.topHit.ascendingLabel', {
defaultMessage: 'Ascending',
}),
value: 'asc',
},
],
write(agg, output) {
const sortField = agg.params.sortField;
const sortOrder = agg.params.sortOrder;
if (sortField.scripted) {
output.params.sort = [
{
_script: {
script: {
source: sortField.script,
lang: sortField.lang,
},
type: sortField.type,
order: sortOrder.value,
},
},
];
} else {
output.params.sort = [
{
[sortField.name]: {
order: sortOrder.value,
},
},
];
}
},
},
],
getValue(agg, bucket) {
const hits: any[] = _.get(bucket, `${agg.id}.hits.hits`);
if (!hits || !hits.length) {
return null;
}
const path = agg.getParam('field').name;
let values = _.flatten(
hits.map(hit =>
path === '_source' ? hit._source : agg.getIndexPattern().flattenHit(hit, true)[path]
)
);
if (values.length === 1) {
values = values[0];
}
if (Array.isArray(values)) {
if (!_.compact(values).length) {
return null;
}
const aggregate = agg.getParam('aggregate');
switch (aggregate.value) {
case 'max':
return _.max(values);
case 'min':
return _.min(values);
case 'sum':
return _.sum(values);
case 'average':
return _.sum(values) / values.length;
}
}
return values;
},
},
],
getValue(agg, bucket) {
const hits: any[] = _.get(bucket, `${agg.id}.hits.hits`);
if (!hits || !hits.length) {
return null;
{
getInternalStartServices,
}
const path = agg.getParam('field').name;
let values = _.flatten(
hits.map(hit =>
path === '_source' ? hit._source : agg.getIndexPattern().flattenHit(hit, true)[path]
)
);
if (values.length === 1) {
values = values[0];
}
if (Array.isArray(values)) {
if (!_.compact(values).length) {
return null;
}
const aggregate = agg.getParam('aggregate');
switch (aggregate.value) {
case 'max':
return _.max(values);
case 'min':
return _.min(values);
case 'sum':
return _.sum(values);
case 'average':
return _.sum(values) / values.length;
}
}
return values;
},
});
);
};

View file

@ -18,11 +18,20 @@
*/
import { BaseParamType } from './base';
import { FieldParamType } from './field';
import { FieldParamType, FieldParamTypeDependencies } from './field';
import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '../../../../common';
import { IAggConfig } from '../agg_config';
import { fieldFormatsServiceMock } from '../../../field_formats/mocks';
import { notificationServiceMock } from '../../../../../../../src/core/public/mocks';
describe('Field', () => {
const fieldParamTypeDependencies: FieldParamTypeDependencies = {
getInternalStartServices: () => ({
fieldFormats: fieldFormatsServiceMock.createStartContract(),
notifications: notificationServiceMock.createStartContract(),
}),
};
const indexPattern = {
id: '1234',
title: 'logstash-*',
@ -52,10 +61,13 @@ describe('Field', () => {
describe('constructor', () => {
it('it is an instance of BaseParamType', () => {
const aggParam = new FieldParamType({
name: 'field',
type: 'field',
});
const aggParam = new FieldParamType(
{
name: 'field',
type: 'field',
},
fieldParamTypeDependencies
);
expect(aggParam instanceof BaseParamType).toBeTruthy();
});
@ -63,10 +75,13 @@ describe('Field', () => {
describe('getAvailableFields', () => {
it('should return only aggregatable fields by default', () => {
const aggParam = new FieldParamType({
name: 'field',
type: 'field',
});
const aggParam = new FieldParamType(
{
name: 'field',
type: 'field',
},
fieldParamTypeDependencies
);
const fields = aggParam.getAvailableFields(agg);
@ -78,10 +93,13 @@ describe('Field', () => {
});
it('should return all fields if onlyAggregatable is false', () => {
const aggParam = new FieldParamType({
name: 'field',
type: 'field',
});
const aggParam = new FieldParamType(
{
name: 'field',
type: 'field',
},
fieldParamTypeDependencies
);
aggParam.onlyAggregatable = false;
@ -91,10 +109,13 @@ describe('Field', () => {
});
it('should return all fields if filterFieldTypes was not specified', () => {
const aggParam = new FieldParamType({
name: 'field',
type: 'field',
});
const aggParam = new FieldParamType(
{
name: 'field',
type: 'field',
},
fieldParamTypeDependencies
);
indexPattern.fields[1].aggregatable = true;

View file

@ -24,7 +24,7 @@ import { BaseParamType } from './base';
import { propFilter } from '../filter';
import { isNestedField, KBN_FIELD_TYPES } from '../../../../common';
import { Field as IndexPatternField } from '../../../index_patterns';
import { getNotifications } from '../../../../public/services';
import { GetInternalStartServicesFn } from '../../../types';
const filterByType = propFilter('type');
@ -32,13 +32,20 @@ export type FieldTypes = KBN_FIELD_TYPES | KBN_FIELD_TYPES[] | '*';
// TODO need to make a more explicit interface for this
export type IFieldParamType = FieldParamType;
export interface FieldParamTypeDependencies {
getInternalStartServices: GetInternalStartServicesFn;
}
export class FieldParamType extends BaseParamType {
required = true;
scriptable = true;
filterFieldTypes: FieldTypes;
onlyAggregatable: boolean;
constructor(config: Record<string, any>) {
constructor(
config: Record<string, any>,
{ getInternalStartServices }: FieldParamTypeDependencies
) {
super(config);
this.filterFieldTypes = config.filterFieldTypes || '*';
@ -87,7 +94,7 @@ export class FieldParamType extends BaseParamType {
// @ts-ignore
const validField = this.getAvailableFields(aggConfig).find((f: any) => f.name === fieldName);
if (!validField) {
getNotifications().toasts.addDanger(
getInternalStartServices().notifications.toasts.addDanger(
i18n.translate(
'data.search.aggs.paramTypes.field.invalidSavedFieldParameterErrorMessage',
{

View file

@ -18,12 +18,13 @@
*/
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { coreMock } from '../../../../../../../src/core/public/mocks';
import { coreMock, notificationServiceMock } from '../../../../../../../src/core/public/mocks';
import { AggTypesRegistry, AggTypesRegistryStart } from '../agg_types_registry';
import { getAggTypes } from '../agg_types';
import { BucketAggType } from '../buckets/_bucket_agg_type';
import { BucketAggType } from '../buckets/bucket_agg_type';
import { MetricAggType } from '../metrics/metric_agg_type';
import { queryServiceMock } from '../../../query/mocks';
import { fieldFormatsServiceMock } from '../../../field_formats/mocks';
/**
* Testing utility which creates a new instance of AggTypesRegistry,
@ -55,8 +56,11 @@ export function mockAggTypesRegistry<T extends BucketAggType<any> | MetricAggTyp
const core = coreMock.createSetup();
const aggTypes = getAggTypes({
uiSettings: core.uiSettings,
notifications: core.notifications,
query: queryServiceMock.createSetupContract(),
getInternalStartServices: () => ({
fieldFormats: fieldFormatsServiceMock.createStartContract(),
notifications: notificationServiceMock.createStartContract(),
}),
});
aggTypes.buckets.forEach(type => registrySetup.registerBucket(type));

View file

@ -26,6 +26,7 @@ import { getEsClient, LegacyApiCaller } from './es_client';
import { ES_SEARCH_STRATEGY, DEFAULT_SEARCH_STRATEGY } from '../../common/search';
import { esSearchStrategyProvider } from './es_search/es_search_strategy';
import { QuerySetup } from '../query/query_service';
import { GetInternalStartServicesFn } from '../types';
import { SearchInterceptor } from './search_interceptor';
import {
getAggTypes,
@ -44,6 +45,7 @@ import {
interface SearchServiceSetupDependencies {
packageInfo: PackageInfo;
query: QuerySetup;
getInternalStartServices: GetInternalStartServicesFn;
}
/**
@ -81,7 +83,7 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
public setup(
core: CoreSetup,
{ packageInfo, query }: SearchServiceSetupDependencies
{ packageInfo, query, getInternalStartServices }: SearchServiceSetupDependencies
): ISearchSetup {
this.esClient = getEsClient(core.injectedMetadata, core.http, packageInfo);
this.registerSearchStrategyProvider(SYNC_SEARCH_STRATEGY, syncSearchStrategyProvider);
@ -91,7 +93,7 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
const aggTypes = getAggTypes({
query,
uiSettings: core.uiSettings,
notifications: core.notifications,
getInternalStartServices,
});
aggTypes.buckets.forEach(b => aggTypesSetup.registerBucket(b));

View file

@ -71,3 +71,12 @@ export interface IDataPluginServices extends Partial<CoreStart> {
storage: IStorageWrapper;
data: DataPublicPluginStart;
}
/** @internal **/
export interface InternalStartServices {
fieldFormats: FieldFormatsStart;
notifications: CoreStart['notifications'];
}
/** @internal **/
export type GetInternalStartServicesFn = () => InternalStartServices;