diff --git a/x-pack/plugins/lens/common/expressions/format_column/format_column.test.ts b/x-pack/plugins/lens/common/expressions/format_column/format_column.test.ts index 17192103efaae..d3e074d4cdafc 100644 --- a/x-pack/plugins/lens/common/expressions/format_column/format_column.test.ts +++ b/x-pack/plugins/lens/common/expressions/format_column/format_column.test.ts @@ -63,6 +63,26 @@ describe('format_column', () => { }); }); + it('wraps in suffix formatter if provided', async () => { + datatable.columns[0].meta.params = { id: 'myformatter', params: {} }; + const result = await fn(datatable, { + columnId: 'test', + format: 'number', + decimals: 5, + suffix: 'ABC', + }); + expect(result.columns[0].meta.params).toEqual({ + id: 'suffix', + params: { + suffixString: 'ABC', + id: 'number', + params: { + pattern: '0,0.00000', + }, + }, + }); + }); + it('has special handling for 0 decimals', async () => { datatable.columns[0].meta.params = { id: 'myformatter', params: {} }; const result = await fn(datatable, { columnId: 'test', format: 'number', decimals: 0 }); @@ -140,6 +160,32 @@ describe('format_column', () => { }); }); + it('double-nests suffix formatters', async () => { + datatable.columns[0].meta.params = { + id: 'suffix', + params: { suffixString: 'ABC', id: 'myformatter', params: { innerParam: 456 } }, + }; + const result = await fn(datatable, { + columnId: 'test', + format: '', + parentFormat: JSON.stringify({ id: 'suffix', params: { suffixString: 'DEF' } }), + }); + expect(result.columns[0].meta.params).toEqual({ + id: 'suffix', + params: { + suffixString: 'DEF', + id: 'suffix', + params: { + suffixString: 'ABC', + id: 'myformatter', + params: { + innerParam: 456, + }, + }, + }, + }); + }); + it('overwrites format with well known pattern including decimals', async () => { datatable.columns[0].meta.params = { id: 'previousWrapper', diff --git a/x-pack/plugins/lens/common/expressions/format_column/format_column_fn.ts b/x-pack/plugins/lens/common/expressions/format_column/format_column_fn.ts index 93b54e777b645..183ec74ac301d 100644 --- a/x-pack/plugins/lens/common/expressions/format_column/format_column_fn.ts +++ b/x-pack/plugins/lens/common/expressions/format_column/format_column_fn.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { SerializedFieldFormat } from 'src/plugins/field_formats/common'; import { supportedFormats } from './supported_formats'; import type { DatatableColumn } from '../../../../../../src/plugins/expressions'; import type { FormatColumnArgs } from './index'; @@ -12,7 +13,8 @@ import type { FormatColumnExpressionFunction } from './types'; function isNestedFormat(params: DatatableColumn['meta']['params']) { // if there is a nested params object with an id, it's a nested format - return !!params?.params?.id; + // suffix formatters do not count as nested + return !!params?.params?.id && params.id !== 'suffix'; } function withParams(col: DatatableColumn, params: Record) { @@ -21,17 +23,27 @@ function withParams(col: DatatableColumn, params: Record) { export const formatColumnFn: FormatColumnExpressionFunction['fn'] = ( input, - { format, columnId, decimals, parentFormat }: FormatColumnArgs + { format, columnId, decimals, suffix, parentFormat }: FormatColumnArgs ) => ({ ...input, columns: input.columns.map((col) => { if (col.id === columnId) { if (!parentFormat) { if (supportedFormats[format]) { - return withParams(col, { + let serializedFormat: SerializedFieldFormat = { id: format, params: { pattern: supportedFormats[format].decimalsToPattern(decimals) }, - }); + }; + if (suffix) { + serializedFormat = { + id: 'suffix', + params: { + ...serializedFormat, + suffixString: suffix, + }, + }; + } + return withParams(col, serializedFormat as Record); } else if (format) { return withParams(col, { id: format }); } else { diff --git a/x-pack/plugins/lens/common/expressions/format_column/index.ts b/x-pack/plugins/lens/common/expressions/format_column/index.ts index 0fc99ff8f7089..2a6721ad993b7 100644 --- a/x-pack/plugins/lens/common/expressions/format_column/index.ts +++ b/x-pack/plugins/lens/common/expressions/format_column/index.ts @@ -11,6 +11,7 @@ export interface FormatColumnArgs { format: string; columnId: string; decimals?: number; + suffix?: string; parentFormat?: string; } @@ -33,6 +34,10 @@ export const formatColumn: FormatColumnExpressionFunction = { types: ['number'], help: '', }, + suffix: { + types: ['string'], + help: '', + }, parentFormat: { types: ['string'], help: '', diff --git a/x-pack/plugins/lens/common/suffix_formatter/index.ts b/x-pack/plugins/lens/common/suffix_formatter/index.ts index 4fa6457f0125d..d2af0df25f132 100644 --- a/x-pack/plugins/lens/common/suffix_formatter/index.ts +++ b/x-pack/plugins/lens/common/suffix_formatter/index.ts @@ -49,7 +49,7 @@ export function getSuffixFormatter(getFormatFactory: () => FormatFactory): Field textConvert = (val: unknown) => { const unit = this.param('unit') as TimeScaleUnit | undefined; - const suffix = unit ? unitSuffixes[unit] : undefined; + const suffix = unit ? unitSuffixes[unit] : this.param('suffixString'); const nestedFormatter = this.param('id'); const nestedParams = this.param('params'); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/format_selector.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/format_selector.test.tsx index 1321d765e119f..60fdb382322a6 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/format_selector.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/format_selector.test.tsx @@ -6,11 +6,20 @@ */ import React from 'react'; -import { shallow } from 'enzyme'; +import { mount, shallow } from 'enzyme'; import { FormatSelector } from './format_selector'; import { act } from 'react-dom/test-utils'; import { GenericIndexPatternColumn } from '../..'; +jest.mock('lodash', () => { + const original = jest.requireActual('lodash'); + + return { + ...original, + debounce: (fn: unknown) => fn, + }; +}); + const bytesColumn: GenericIndexPatternColumn = { label: 'Max of bytes', dataType: 'number', @@ -63,4 +72,18 @@ describe('FormatSelector', () => { }); expect(props.onChange).toBeCalledWith({ id: 'bytes', params: { decimals: 0 } }); }); + it('updates the suffix', async () => { + const props = getDefaultProps(); + const component = mount(); + await act(async () => { + component + .find('[data-test-subj="indexPattern-dimension-formatSuffix"]') + .last() + .prop('onChange')!({ + currentTarget: { value: 'GB' }, + } as React.ChangeEvent); + }); + component.update(); + expect(props.onChange).toBeCalledWith({ id: 'bytes', params: { suffix: 'GB' } }); + }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/format_selector.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/format_selector.tsx index efe7966870531..49231e64d53be 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/format_selector.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/format_selector.tsx @@ -7,9 +7,10 @@ import React, { useCallback, useMemo, useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiFormRow, EuiComboBox, EuiSpacer, EuiRange } from '@elastic/eui'; +import { EuiFormRow, EuiComboBox, EuiSpacer, EuiRange, EuiFieldText } from '@elastic/eui'; import { GenericIndexPatternColumn } from '../indexpattern'; import { isColumnFormatted } from '../operations/definitions/helpers'; +import { useDebouncedValue } from '../../shared_components'; const supportedFormats: Record = { number: { @@ -46,6 +47,10 @@ const decimalsLabel = i18n.translate('xpack.lens.indexPattern.decimalPlacesLabel defaultMessage: 'Decimals', }); +const suffixLabel = i18n.translate('xpack.lens.indexPattern.suffixLabel', { + defaultMessage: 'Suffix', +}); + interface FormatSelectorProps { selectedColumn: GenericIndexPatternColumn; onChange: (newFormat?: { id: string; params?: Record }) => void; @@ -62,6 +67,30 @@ export function FormatSelector(props: FormatSelectorProps) { const [decimals, setDecimals] = useState(currentFormat?.params?.decimals ?? 2); + const onChangeSuffix = useCallback( + (suffix: string) => { + if (!currentFormat) { + return; + } + onChange({ + id: currentFormat.id, + params: { + ...currentFormat.params, + suffix, + }, + }); + }, + [currentFormat, onChange] + ); + + const { handleInputChange: setSuffix, inputValue: suffix } = useDebouncedValue( + { + onChange: onChangeSuffix, + value: currentFormat?.params?.suffix ?? '', + }, + { allowFalsyValue: true } + ); + const selectedFormat = currentFormat?.id ? supportedFormats[currentFormat.id] : undefined; const stableOptions = useMemo( () => [ @@ -135,6 +164,7 @@ export function FormatSelector(props: FormatSelectorProps) { onChange({ id: currentFormat.id, params: { + ...currentFormat.params, decimals: validatedValue, }, }); @@ -145,6 +175,18 @@ export function FormatSelector(props: FormatSelectorProps) { prepend={decimalsLabel} aria-label={decimalsLabel} /> + + { + setSuffix(e.currentTarget.value); + }} + data-test-subj="indexPattern-dimension-formatSuffix" + compressed + fullWidth + prepend={suffixLabel} + aria-label={suffixLabel} + /> ) : null} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx index 8490b48ad320e..e65d89547d567 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx @@ -12,7 +12,7 @@ import { euiThemeVars } from '@kbn/ui-theme'; import { AggFunctionsMapping } from '../../../../../../../src/plugins/data/public'; import { buildExpressionFunction } from '../../../../../../../src/plugins/expressions/public'; import { OperationDefinition, ParamEditorProps } from './index'; -import { FieldBasedIndexPatternColumn, FormatParams } from './column_types'; +import { FieldBasedIndexPatternColumn, ValueFormatConfig } from './column_types'; import { getFormatFromPreviousColumn, @@ -60,7 +60,7 @@ export interface CardinalityIndexPatternColumn extends FieldBasedIndexPatternCol operationType: typeof OPERATION_TYPE; params?: { emptyAsNull?: boolean; - format?: FormatParams; + format?: ValueFormatConfig; }; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/column_types.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/column_types.ts index 333312116949f..029f2b7bed7de 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/column_types.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/column_types.ts @@ -19,17 +19,18 @@ export interface BaseIndexPatternColumn extends Operation { timeShift?: string; } -export interface FormatParams { +export interface ValueFormatConfig { id: string; params?: { decimals: number; + suffix?: string; }; } // Formatting can optionally be added to any column export interface FormattedIndexPatternColumn extends BaseIndexPatternColumn { params?: { - format?: FormatParams; + format?: ValueFormatConfig; }; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx index 7ecd5a4970c95..0643e6b2ca365 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx @@ -12,7 +12,7 @@ import { EuiSwitch } from '@elastic/eui'; import { AggFunctionsMapping } from '../../../../../../../src/plugins/data/public'; import { buildExpressionFunction } from '../../../../../../../src/plugins/expressions/public'; import { OperationDefinition, ParamEditorProps } from './index'; -import { FieldBasedIndexPatternColumn, FormatParams } from './column_types'; +import { FieldBasedIndexPatternColumn, ValueFormatConfig } from './column_types'; import { IndexPatternField } from '../../types'; import { getInvalidFieldMessage, @@ -36,7 +36,7 @@ export type CountIndexPatternColumn = FieldBasedIndexPatternColumn & { operationType: 'count'; params?: { emptyAsNull?: boolean; - format?: FormatParams; + format?: ValueFormatConfig; }; }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/math.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/math.tsx index 7aae35f496923..4797ac348c71f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/math.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/math.tsx @@ -7,7 +7,7 @@ import type { TinymathAST } from '@kbn/tinymath'; import { OperationDefinition } from '../index'; -import { ReferenceBasedIndexPatternColumn } from '../column_types'; +import { ValueFormatConfig, ReferenceBasedIndexPatternColumn } from '../column_types'; import { IndexPattern } from '../../../types'; export interface MathIndexPatternColumn extends ReferenceBasedIndexPatternColumn { @@ -15,12 +15,7 @@ export interface MathIndexPatternColumn extends ReferenceBasedIndexPatternColumn params: { tinymathAst: TinymathAST | string; // last value on numeric fields can be formatted - format?: { - id: string; - params?: { - decimals: number; - }; - }; + format?: ValueFormatConfig; }; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx index 1ebbf6d87b92f..b8fe5a1584d35 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx @@ -18,7 +18,7 @@ import { import { AggFunctionsMapping } from '../../../../../../../src/plugins/data/public'; import { buildExpressionFunction } from '../../../../../../../src/plugins/expressions/public'; import { OperationDefinition } from './index'; -import { FieldBasedIndexPatternColumn } from './column_types'; +import { FieldBasedIndexPatternColumn, ValueFormatConfig } from './column_types'; import { IndexPatternField, IndexPattern } from '../../types'; import { adjustColumnReferencesForChangedColumn, updateColumnParam } from '../layer_helpers'; import { DataType } from '../../../types'; @@ -108,12 +108,7 @@ export interface LastValueIndexPatternColumn extends FieldBasedIndexPatternColum sortField: string; showArrayValues: boolean; // last value on numeric fields can be formatted - format?: { - id: string; - params?: { - decimals: number; - }; - }; + format?: ValueFormatConfig; }; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx index 2b46e52defdba..438902962fbcf 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx @@ -19,7 +19,11 @@ import { combineErrorMessages, isColumnOfType, } from './helpers'; -import { FieldBasedIndexPatternColumn, BaseIndexPatternColumn, FormatParams } from './column_types'; +import { + FieldBasedIndexPatternColumn, + BaseIndexPatternColumn, + ValueFormatConfig, +} from './column_types'; import { adjustTimeScaleLabelSuffix, adjustTimeScaleOnOtherColumnChange, @@ -31,7 +35,7 @@ type MetricColumn = FieldBasedIndexPatternColumn & { operationType: T; params?: { emptyAsNull?: boolean; - format?: FormatParams; + format?: ValueFormatConfig; }; }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.tsx index d41ddaf26fb3f..39122808c9c0b 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.tsx @@ -8,7 +8,11 @@ import React, { useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFieldNumber, EuiFormLabel, EuiSpacer } from '@elastic/eui'; import { OperationDefinition } from './index'; -import { ReferenceBasedIndexPatternColumn, GenericIndexPatternColumn } from './column_types'; +import { + ReferenceBasedIndexPatternColumn, + GenericIndexPatternColumn, + ValueFormatConfig, +} from './column_types'; import type { IndexPattern } from '../../types'; import { useDebouncedValue } from '../../../shared_components'; import { getFormatFromPreviousColumn, isValidNumber } from './helpers'; @@ -37,12 +41,7 @@ export interface StaticValueIndexPatternColumn extends ReferenceBasedIndexPatter operationType: 'static_value'; params: { value?: string; - format?: { - id: string; - params?: { - decimals: number; - }; - }; + format?: ValueFormatConfig; }; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/types.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/types.ts index 1284870327653..eb4425987cc31 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/types.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/types.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FieldBasedIndexPatternColumn } from '../column_types'; +import { FieldBasedIndexPatternColumn, ValueFormatConfig } from '../column_types'; export interface TermsIndexPatternColumn extends FieldBasedIndexPatternColumn { operationType: 'terms'; @@ -22,12 +22,7 @@ export interface TermsIndexPatternColumn extends FieldBasedIndexPatternColumn { missingBucket?: boolean; secondaryFields?: string[]; // Terms on numeric fields can be formatted - format?: { - id: string; - params?: { - decimals: number; - }; - }; + format?: ValueFormatConfig; parentFormat?: { id: string; }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts index f9fe8701949e1..f229407ce23ff 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts @@ -194,6 +194,10 @@ function getExpressionForLayer( format: format ? [format.id] : [''], columnId: [id], decimals: typeof format?.params?.decimals === 'number' ? [format.params.decimals] : [], + suffix: + format?.params && 'suffix' in format.params && format.params.suffix + ? [format.params.suffix] + : [], parentFormat: parentFormat ? [JSON.stringify(parentFormat)] : [], }, };