mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Lens] Add suffix formatter (#128246)
This commit is contained in:
parent
94858d7449
commit
a0518b61ea
15 changed files with 164 additions and 43 deletions
|
@ -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',
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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: '',
|
||||
|
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -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' } });
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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)] : [],
|
||||
},
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue