Skip to content

Commit

Permalink
[data views] Default field formatters based on field meta values (#17…
Browse files Browse the repository at this point in the history
…4973)

## 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.
#176112


Closes: #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
}

```
  • Loading branch information
mattkime authored Feb 6, 2024
1 parent a50f0ae commit cde8f2b
Show file tree
Hide file tree
Showing 10 changed files with 128 additions and 1 deletion.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -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 '..';

Expand Down Expand Up @@ -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[]
Expand Down
Original file line number Diff line number Diff line change
@@ -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'),
};
6 changes: 6 additions & 0 deletions src/plugins/data_views/common/fields/data_view_field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -370,6 +374,7 @@ export class DataViewField implements DataViewFieldBase {
readFromDocValues: this.readFromDocValues,
subType: this.subType,
customLabel: this.customLabel,
defaultFormatter: this.defaultFormatter,
};
}

Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/plugins/data_views/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export interface FieldDescriptor {
timeZone?: string[];
timeSeriesMetric?: estypes.MappingTimeSeriesMetricType;
timeSeriesDimension?: boolean;
defaultFormatter?: string;
}

interface FieldSubType {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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> = {
Expand Down
45 changes: 45 additions & 0 deletions test/functional/apps/management/data_views/_field_formatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
});

/**
Expand Down

0 comments on commit cde8f2b

Please sign in to comment.