mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[data views] Default field formatters based on field meta values (#174973)
## Summary Default field formatters based on field meta units data. Note: the smallest unit our formatter will show is milliseconds which means micro and nanoseconds may round down to zero for smaller values. https://github.com/elastic/kibana/issues/176112 Closes: https://github.com/elastic/kibana/issues/82318 Mapping and doc setup for testing - ``` PUT my-index-000001 PUT my-index-000001/_mapping { "properties": { "nanos": { "type": "long", "meta": { "unit": "nanos" } }, "micros": { "type": "long", "meta": { "unit": "micros" } }, "ms": { "type": "long", "meta": { "unit": "ms" } }, "second": { "type": "long", "meta": { "unit": "s" } }, "minute": { "type": "long", "meta": { "unit": "m" } }, "hour": { "type": "long", "meta": { "unit": "h" } }, "day": { "type": "long", "meta": { "unit": "d" } }, "percent": { "type": "long", "meta": { "unit": "percent" } }, "bytes": { "type": "long", "meta": { "unit": "byte" } } } } POST my-index-000001/_doc { "nanos" : 1234.5, "micros" : 1234.5, "ms" : 1234.5, "second" : 1234.5, "minute" : 1234.5, "hour" : 1234.5, "day" : 1234.5, "percent" : 1234.5, "bytes" : 1234.5 } ```
This commit is contained in:
parent
a50f0ae1a6
commit
cde8f2be67
10 changed files with 128 additions and 1 deletions
|
@ -7,6 +7,7 @@ FldList [
|
|||
"conflictDescriptions": undefined,
|
||||
"count": 5,
|
||||
"customLabel": "A Runtime Field",
|
||||
"defaultFormatter": undefined,
|
||||
"esTypes": Array [
|
||||
"keyword",
|
||||
],
|
||||
|
|
|
@ -24,6 +24,7 @@ import type {
|
|||
RuntimeField,
|
||||
} from '../types';
|
||||
import { removeFieldAttrs } from './utils';
|
||||
import { metaUnitsToFormatter } from './meta_units_to_formatter';
|
||||
|
||||
import type { DataViewAttributes, FieldAttrs, FieldAttrSet } from '..';
|
||||
|
||||
|
@ -251,6 +252,11 @@ export abstract class AbstractDataView {
|
|||
return fieldFormat;
|
||||
}
|
||||
|
||||
const fmt = field.defaultFormatter ? metaUnitsToFormatter[field.defaultFormatter] : undefined;
|
||||
if (fmt) {
|
||||
return this.fieldFormats.getInstance(fmt.id, fmt.params);
|
||||
}
|
||||
|
||||
return this.fieldFormats.getDefaultInstance(
|
||||
field.type as KBN_FIELD_TYPES,
|
||||
field.esTypes as ES_FIELD_TYPES[]
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { FieldFormatParams } from '@kbn/field-formats-plugin/common';
|
||||
|
||||
const timeUnitToDurationFmt = (inputFormat = 'milliseconds') => {
|
||||
return {
|
||||
id: 'duration',
|
||||
params: {
|
||||
inputFormat,
|
||||
outputFormat: 'humanizePrecise',
|
||||
outputPrecision: 2,
|
||||
includeSpaceWithSuffix: true,
|
||||
useShortSuffix: true,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const metaUnitsToFormatter: Record<string, { id: string; params?: FieldFormatParams }> = {
|
||||
percent: { id: 'percent' },
|
||||
byte: { id: 'bytes' },
|
||||
nanos: timeUnitToDurationFmt('nanoseconds'),
|
||||
micros: timeUnitToDurationFmt('microseconds'),
|
||||
ms: timeUnitToDurationFmt('milliseconds'),
|
||||
s: timeUnitToDurationFmt('seconds'),
|
||||
m: timeUnitToDurationFmt('minutes'),
|
||||
h: timeUnitToDurationFmt('hours'),
|
||||
d: timeUnitToDurationFmt('days'),
|
||||
};
|
|
@ -69,6 +69,10 @@ export class DataViewField implements DataViewFieldBase {
|
|||
this.spec.count = count;
|
||||
}
|
||||
|
||||
public get defaultFormatter() {
|
||||
return this.spec.defaultFormatter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns runtime field definition or undefined if field is not runtime field.
|
||||
*/
|
||||
|
@ -370,6 +374,7 @@ export class DataViewField implements DataViewFieldBase {
|
|||
readFromDocValues: this.readFromDocValues,
|
||||
subType: this.subType,
|
||||
customLabel: this.customLabel,
|
||||
defaultFormatter: this.defaultFormatter,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -403,6 +408,7 @@ export class DataViewField implements DataViewFieldBase {
|
|||
timeSeriesMetric: this.spec.timeSeriesMetric,
|
||||
timeZone: this.spec.timeZone,
|
||||
fixedInterval: this.spec.fixedInterval,
|
||||
defaultFormatter: this.defaultFormatter,
|
||||
};
|
||||
|
||||
// Filter undefined values from the spec
|
||||
|
|
|
@ -462,6 +462,8 @@ export type FieldSpec = DataViewFieldBase & {
|
|||
* Name of parent field for composite runtime field subfields.
|
||||
*/
|
||||
parentName?: string;
|
||||
|
||||
defaultFormatter?: string;
|
||||
};
|
||||
|
||||
export type DataViewFieldMap = Record<string, FieldSpec>;
|
||||
|
|
|
@ -32,6 +32,7 @@ export interface FieldDescriptor {
|
|||
timeZone?: string[];
|
||||
timeSeriesMetric?: estypes.MappingTimeSeriesMetricType;
|
||||
timeSeriesDimension?: boolean;
|
||||
defaultFormatter?: string;
|
||||
}
|
||||
|
||||
interface FieldSubType {
|
||||
|
|
|
@ -165,5 +165,22 @@ describe('index_patterns/field_capabilities/field_caps_response', () => {
|
|||
expect(child).not.toHaveProperty('subType');
|
||||
});
|
||||
});
|
||||
|
||||
it('sets default field formatter', () => {
|
||||
const fields = readFieldCapsResponse({
|
||||
fields: {
|
||||
seconds: {
|
||||
long: {
|
||||
searchable: true,
|
||||
aggregatable: true,
|
||||
meta: {
|
||||
unit: ['s'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(fields[0].defaultFormatter).toEqual('s');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -12,6 +12,11 @@ import { castEsToKbnFieldTypeName } from '@kbn/field-types';
|
|||
import { shouldReadFieldFromDocValues } from './should_read_field_from_doc_values';
|
||||
import { FieldDescriptor } from '../..';
|
||||
|
||||
// The array will have different values if values vary across indices
|
||||
const unitsArrayToFormatter = (unitArr: string[]) => {
|
||||
return unitArr.find((unit) => unitArr[0] !== unit) ? undefined : unitArr[0];
|
||||
};
|
||||
|
||||
/**
|
||||
* Read the response from the _field_caps API to determine the type and
|
||||
* "aggregatable"/"searchable" status of each field.
|
||||
|
@ -134,7 +139,11 @@ export function readFieldCapsResponse(
|
|||
timeSeriesMetricType = 'position';
|
||||
}
|
||||
const esType = types[0];
|
||||
const field = {
|
||||
|
||||
const defaultFormatter =
|
||||
capsByType[types[0]].meta?.unit && unitsArrayToFormatter(capsByType[types[0]].meta?.unit);
|
||||
|
||||
const field: FieldDescriptor = {
|
||||
name: fieldName,
|
||||
type: castEsToKbnFieldTypeName(esType),
|
||||
esTypes: types,
|
||||
|
@ -147,6 +156,11 @@ export function readFieldCapsResponse(
|
|||
timeSeriesMetric: timeSeriesMetricType,
|
||||
timeSeriesDimension: capsByType[types[0]].time_series_dimension,
|
||||
};
|
||||
|
||||
if (defaultFormatter) {
|
||||
field.defaultFormatter = defaultFormatter;
|
||||
}
|
||||
|
||||
// This is intentionally using a "hash" and a "push" to be highly optimized with very large indexes
|
||||
agg.array.push(field);
|
||||
agg.hash[fieldName] = field;
|
||||
|
|
|
@ -99,6 +99,7 @@ const FieldDescriptorSchema = schema.object({
|
|||
conflictDescriptions: schema.maybe(
|
||||
schema.recordOf(schema.string(), schema.arrayOf(schema.string()))
|
||||
),
|
||||
defaultFormatter: schema.maybe(schema.string()),
|
||||
});
|
||||
|
||||
export const validate: FullValidationConfig<any, any, any> = {
|
||||
|
|
|
@ -413,6 +413,51 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('default formatter by field meta value', () => {
|
||||
const indexTitle = 'field_formats_management_functional_tests';
|
||||
|
||||
before(async () => {
|
||||
if (await es.indices.exists({ index: indexTitle })) {
|
||||
await es.indices.delete({ index: indexTitle });
|
||||
}
|
||||
});
|
||||
|
||||
it('should apply default formatter by field meta value', async () => {
|
||||
await es.indices.create({
|
||||
index: indexTitle,
|
||||
body: {
|
||||
mappings: {
|
||||
properties: {
|
||||
seconds: { type: 'long', meta: { unit: 's' } },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const docResult = await es.index({
|
||||
index: indexTitle,
|
||||
body: { seconds: 1234 },
|
||||
refresh: 'wait_for',
|
||||
});
|
||||
|
||||
const testDocumentId = docResult._id;
|
||||
|
||||
const indexPatternResult = await indexPatterns.create(
|
||||
{ title: `${indexTitle}*` }, // sidesteps field caching when index pattern is reused
|
||||
{ override: true }
|
||||
);
|
||||
|
||||
await PageObjects.common.navigateToApp('discover', {
|
||||
hash: `/doc/${indexPatternResult.id}/${indexTitle}?id=${testDocumentId}`,
|
||||
});
|
||||
await testSubjects.exists('doc-hit');
|
||||
|
||||
const renderedValue = await testSubjects.find(`tableDocViewRow-seconds-value`);
|
||||
const text = await renderedValue.getVisibleText();
|
||||
expect(text).to.be('20.57 min');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue