Skip to content

Commit

Permalink
[data.search.aggs] Move agg-specific field formats to search service (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
lukeelmers committed Jun 24, 2020
1 parent cfe3083 commit ec472ec
Show file tree
Hide file tree
Showing 5 changed files with 238 additions and 87 deletions.
107 changes: 21 additions & 86 deletions src/plugins/data/public/field_formats/utils/deserialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 new DefaultFieldFormat();
};

return format.convert(val, type);
};
// decorate getFormat to handle custom types created by aggs
const getFieldFormat = getFormatWithAggs(getFormat);

return {
convert,
getConverterFor: (type: FieldFormatsContentType) => (val: string) => convert(val, type),
} as IFieldFormat;
} else {
return getFieldFormat(this, id, mapping.params);
}
return getFieldFormat(serializedFieldFormat);
};
Original file line number Diff line number Diff line change
@@ -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);
});
});
116 changes: 116 additions & 0 deletions src/plugins/data/public/search/aggs/utils/get_format_with_aggs.ts
Original file line number Diff line number Diff line change
@@ -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]();
};
}
1 change: 1 addition & 0 deletions src/plugins/data/public/search/aggs/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
2 changes: 1 addition & 1 deletion src/plugins/expressions/common/types/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

0 comments on commit ec472ec

Please sign in to comment.