[Lens] Add suffix formatter (#128246)

This commit is contained in:
Joe Reuter 2022-03-28 15:57:17 +02:00 committed by GitHub
parent 94858d7449
commit a0518b61ea
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 164 additions and 43 deletions

View file

@ -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',

View file

@ -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<string, unknown>) {
@ -21,17 +23,27 @@ function withParams(col: DatatableColumn, params: Record<string, unknown>) {
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<string, unknown>);
} else if (format) {
return withParams(col, { id: format });
} else {

View file

@ -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: '',

View file

@ -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');

View file

@ -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(<FormatSelector {...props} />);
await act(async () => {
component
.find('[data-test-subj="indexPattern-dimension-formatSuffix"]')
.last()
.prop('onChange')!({
currentTarget: { value: 'GB' },
} as React.ChangeEvent<HTMLInputElement>);
});
component.update();
expect(props.onChange).toBeCalledWith({ id: 'bytes', params: { suffix: 'GB' } });
});
});

View file

@ -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<string, { title: string }> = {
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<string, unknown> }) => 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}
/>
<EuiSpacer size="xs" />
<EuiFieldText
value={suffix}
onChange={(e) => {
setSuffix(e.currentTarget.value);
}}
data-test-subj="indexPattern-dimension-formatSuffix"
compressed
fullWidth
prepend={suffixLabel}
aria-label={suffixLabel}
/>
</>
) : null}
</div>

View file

@ -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;
};
}

View file

@ -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;
};
}

View file

@ -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;
};
};

View file

@ -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;
};
}

View file

@ -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;
};
}

View file

@ -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<T> = FieldBasedIndexPatternColumn & {
operationType: T;
params?: {
emptyAsNull?: boolean;
format?: FormatParams;
format?: ValueFormatConfig;
};
};

View file

@ -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;
};
}

View file

@ -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;
};

View file

@ -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)] : [],
},
};