[data.search.aggs] Move agg-specific field formats to search service (#69586)

This commit is contained in:
Luke Elmers 2020-06-24 10:15:33 -06:00 committed by GitHub
parent 1eede3f128
commit 16eaf82d5c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 238 additions and 87 deletions

View file

@ -18,107 +18,42 @@
*/
import { identity } from 'lodash';
import { i18n } from '@kbn/i18n';
import { convertDateRangeToString, DateRangeKey } from '../../search/aggs/buckets/lib/date_range';
import { convertIPRangeToString, IpRangeKey } from '../../search/aggs/buckets/lib/ip_range';
import { SerializedFieldFormat } from '../../../../expressions/common/types';
import { FieldFormatId, FieldFormatsContentType, IFieldFormat } from '../..';
import { FieldFormat } from '../../../common';
import { DataPublicPluginStart } from '../../../public';
import { getUiSettings } from '../../../public/services';
import { FormatFactory } from '../../../common/field_formats/utils';
interface TermsFieldFormatParams {
otherBucketLabel: string;
missingBucketLabel: string;
id: string;
}
function isTermsFieldFormat(
serializedFieldFormat: SerializedFieldFormat
): serializedFieldFormat is SerializedFieldFormat<TermsFieldFormatParams> {
return serializedFieldFormat.id === 'terms';
}
import { DataPublicPluginStart, IFieldFormat } from '../../../public';
import { getUiSettings } from '../../../public/services';
import { getFormatWithAggs } from '../../search/aggs/utils';
const getConfig = (key: string, defaultOverride?: any): any =>
getUiSettings().get(key, defaultOverride);
const DefaultFieldFormat = FieldFormat.from(identity);
const getFieldFormat = (
fieldFormatsService: DataPublicPluginStart['fieldFormats'],
id?: FieldFormatId,
params: object = {}
): IFieldFormat => {
if (id) {
const Format = fieldFormatsService.getType(id);
if (Format) {
return new Format(params, getConfig);
}
}
return new DefaultFieldFormat();
};
export const deserializeFieldFormat: FormatFactory = function (
this: DataPublicPluginStart['fieldFormats'],
mapping?: SerializedFieldFormat
serializedFieldFormat?: SerializedFieldFormat
) {
if (!mapping) {
if (!serializedFieldFormat) {
return new DefaultFieldFormat();
}
const { id } = mapping;
if (id === 'range') {
const RangeFormat = FieldFormat.from((range: any) => {
const nestedFormatter = mapping.params as SerializedFieldFormat;
const format = getFieldFormat(this, nestedFormatter.id, nestedFormatter.params);
const gte = '\u2265';
const lt = '\u003c';
return i18n.translate('data.aggTypes.buckets.ranges.rangesFormatMessage', {
defaultMessage: '{gte} {from} and {lt} {to}',
values: {
gte,
from: format.convert(range.gte),
lt,
to: format.convert(range.lt),
},
});
});
return new RangeFormat();
} else if (id === 'date_range') {
const nestedFormatter = mapping.params as SerializedFieldFormat;
const DateRangeFormat = FieldFormat.from((range: DateRangeKey) => {
const format = getFieldFormat(this, nestedFormatter.id, nestedFormatter.params);
return convertDateRangeToString(range, format.convert.bind(format));
});
return new DateRangeFormat();
} else if (id === 'ip_range') {
const nestedFormatter = mapping.params as SerializedFieldFormat;
const IpRangeFormat = FieldFormat.from((range: IpRangeKey) => {
const format = getFieldFormat(this, nestedFormatter.id, nestedFormatter.params);
return convertIPRangeToString(range, format.convert.bind(format));
});
return new IpRangeFormat();
} else if (isTermsFieldFormat(mapping) && mapping.params) {
const { params } = mapping;
const convert = (val: string, type: FieldFormatsContentType) => {
const format = getFieldFormat(this, params.id, mapping.params);
if (val === '__other__') {
return params.otherBucketLabel;
}
if (val === '__missing__') {
return params.missingBucketLabel;
const getFormat = (mapping: SerializedFieldFormat): IFieldFormat => {
const { id, params = {} } = mapping;
if (id) {
const Format = this.getType(id);
if (Format) {
return new Format(params, getConfig);
}
}
return format.convert(val, type);
};
return new DefaultFieldFormat();
};
return {
convert,
getConverterFor: (type: FieldFormatsContentType) => (val: string) => convert(val, type),
} as IFieldFormat;
} else {
return getFieldFormat(this, id, mapping.params);
}
// decorate getFormat to handle custom types created by aggs
const getFieldFormat = getFormatWithAggs(getFormat);
return getFieldFormat(serializedFieldFormat);
};

View file

@ -0,0 +1,99 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { identity } from 'lodash';
import { SerializedFieldFormat } from '../../../../../expressions/common/types';
import { FieldFormat } from '../../../../common';
import { IFieldFormat } from '../../../../public';
import { getFormatWithAggs } from './get_format_with_aggs';
describe('getFormatWithAggs', () => {
let getFormat: jest.MockedFunction<(mapping: SerializedFieldFormat) => IFieldFormat>;
beforeEach(() => {
getFormat = jest.fn().mockImplementation(() => {
const DefaultFieldFormat = FieldFormat.from(identity);
return new DefaultFieldFormat();
});
});
test('calls provided getFormat if no matching aggs exist', () => {
const mapping = { id: 'foo', params: {} };
const getFieldFormat = getFormatWithAggs(getFormat);
getFieldFormat(mapping);
expect(getFormat).toHaveBeenCalledTimes(1);
expect(getFormat).toHaveBeenCalledWith(mapping);
});
test('creates custom format for date_range', () => {
const mapping = { id: 'date_range', params: {} };
const getFieldFormat = getFormatWithAggs(getFormat);
const format = getFieldFormat(mapping);
expect(format.convert({ from: '2020-05-01', to: '2020-06-01' })).toBe(
'2020-05-01 to 2020-06-01'
);
expect(format.convert({ to: '2020-06-01' })).toBe('Before 2020-06-01');
expect(format.convert({ from: '2020-06-01' })).toBe('After 2020-06-01');
expect(getFormat).toHaveBeenCalledTimes(3);
});
test('creates custom format for ip_range', () => {
const mapping = { id: 'ip_range', params: {} };
const getFieldFormat = getFormatWithAggs(getFormat);
const format = getFieldFormat(mapping);
expect(format.convert({ type: 'range', from: '10.0.0.1', to: '10.0.0.10' })).toBe(
'10.0.0.1 to 10.0.0.10'
);
expect(format.convert({ type: 'range', to: '10.0.0.10' })).toBe('-Infinity to 10.0.0.10');
expect(format.convert({ type: 'range', from: '10.0.0.10' })).toBe('10.0.0.10 to Infinity');
format.convert({ type: 'mask', mask: '10.0.0.1/24' });
expect(getFormat).toHaveBeenCalledTimes(4);
});
test('creates custom format for range', () => {
const mapping = { id: 'range', params: {} };
const getFieldFormat = getFormatWithAggs(getFormat);
const format = getFieldFormat(mapping);
expect(format.convert({ gte: 1, lt: 20 })).toBe('≥ 1 and < 20');
expect(getFormat).toHaveBeenCalledTimes(1);
});
test('creates custom format for terms', () => {
const mapping = {
id: 'terms',
params: {
otherBucketLabel: 'other bucket',
missingBucketLabel: 'missing bucket',
},
};
const getFieldFormat = getFormatWithAggs(getFormat);
const format = getFieldFormat(mapping);
expect(format.convert('machine.os.keyword')).toBe('machine.os.keyword');
expect(format.convert('__other__')).toBe(mapping.params.otherBucketLabel);
expect(format.convert('__missing__')).toBe(mapping.params.missingBucketLabel);
expect(getFormat).toHaveBeenCalledTimes(3);
});
});

View file

@ -0,0 +1,116 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { i18n } from '@kbn/i18n';
import { SerializedFieldFormat } from '../../../../../expressions/common/types';
import { FieldFormat } from '../../../../common';
import { FieldFormatsContentType, IFieldFormat } from '../../../../public';
import { convertDateRangeToString, DateRangeKey } from '../buckets/lib/date_range';
import { convertIPRangeToString, IpRangeKey } from '../buckets/lib/ip_range';
type GetFieldFormat = (mapping: SerializedFieldFormat) => IFieldFormat;
/**
* Certain aggs have custom field formats that are not part of the field formats
* registry. This function will take the `getFormat` function which is used inside
* `deserializeFieldFormat` and decorate it with the additional custom formats
* that the field formats service doesn't know anything about.
*
* This function is internal to the data plugin, and only exists for use inside
* the field formats service.
*
* @internal
*/
export function getFormatWithAggs(getFieldFormat: GetFieldFormat): GetFieldFormat {
return (mapping) => {
const { id, params = {} } = mapping;
const customFormats: Record<string, () => IFieldFormat> = {
range: () => {
const RangeFormat = FieldFormat.from((range: any) => {
const nestedFormatter = params as SerializedFieldFormat;
const format = getFieldFormat({
id: nestedFormatter.id,
params: nestedFormatter.params,
});
const gte = '\u2265';
const lt = '\u003c';
return i18n.translate('data.aggTypes.buckets.ranges.rangesFormatMessage', {
defaultMessage: '{gte} {from} and {lt} {to}',
values: {
gte,
from: format.convert(range.gte),
lt,
to: format.convert(range.lt),
},
});
});
return new RangeFormat();
},
date_range: () => {
const nestedFormatter = params as SerializedFieldFormat;
const DateRangeFormat = FieldFormat.from((range: DateRangeKey) => {
const format = getFieldFormat({
id: nestedFormatter.id,
params: nestedFormatter.params,
});
return convertDateRangeToString(range, format.convert.bind(format));
});
return new DateRangeFormat();
},
ip_range: () => {
const nestedFormatter = params as SerializedFieldFormat;
const IpRangeFormat = FieldFormat.from((range: IpRangeKey) => {
const format = getFieldFormat({
id: nestedFormatter.id,
params: nestedFormatter.params,
});
return convertIPRangeToString(range, format.convert.bind(format));
});
return new IpRangeFormat();
},
terms: () => {
const convert = (val: string, type: FieldFormatsContentType) => {
const format = getFieldFormat({ id: params.id, params });
if (val === '__other__') {
return params.otherBucketLabel;
}
if (val === '__missing__') {
return params.missingBucketLabel;
}
return format.convert(val, type);
};
return {
convert,
getConverterFor: (type: FieldFormatsContentType) => (val: string) => convert(val, type),
} as IFieldFormat;
},
};
if (!id || !(id in customFormats)) {
return getFieldFormat(mapping);
}
return customFormats[id]();
};
}

View file

@ -18,5 +18,6 @@
*/
export * from './calculate_auto_time_expression';
export * from './get_format_with_aggs';
export * from './prop_filter';
export * from './to_angular_json';

View file

@ -61,7 +61,7 @@ export type UnmappedTypeStrings = 'date' | 'filter';
* Is used to carry information about how to format data in
* a data table as part of the column definition.
*/
export interface SerializedFieldFormat<TParams = object> {
export interface SerializedFieldFormat<TParams = Record<string, any>> {
id?: string;
params?: TParams;
}