[Lens] Custom labels for ranges (#79628)

This commit is contained in:
Joe Reuter 2020-10-14 14:21:27 +02:00 committed by GitHub
parent 136eda1d78
commit 5fd7ad3b34
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 181 additions and 9 deletions

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) &gt; [Range](./kibana-plugin-plugins-expressions-public.range.md) &gt; [label](./kibana-plugin-plugins-expressions-public.range.label.md)
## Range.label property
<b>Signature:</b>
```typescript
label?: string;
```

View file

@ -15,6 +15,7 @@ export interface Range
| Property | Type | Description |
| --- | --- | --- |
| [from](./kibana-plugin-plugins-expressions-public.range.from.md) | <code>number</code> | |
| [label](./kibana-plugin-plugins-expressions-public.range.label.md) | <code>string</code> | |
| [to](./kibana-plugin-plugins-expressions-public.range.to.md) | <code>number</code> | |
| [type](./kibana-plugin-plugins-expressions-public.range.type.md) | <code>typeof name</code> | |

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) &gt; [Range](./kibana-plugin-plugins-expressions-server.range.md) &gt; [label](./kibana-plugin-plugins-expressions-server.range.label.md)
## Range.label property
<b>Signature:</b>
```typescript
label?: string;
```

View file

@ -15,6 +15,7 @@ export interface Range
| Property | Type | Description |
| --- | --- | --- |
| [from](./kibana-plugin-plugins-expressions-server.range.from.md) | <code>number</code> | |
| [label](./kibana-plugin-plugins-expressions-server.range.label.md) | <code>string</code> | |
| [to](./kibana-plugin-plugins-expressions-server.range.to.md) | <code>number</code> | |
| [type](./kibana-plugin-plugins-expressions-server.range.type.md) | <code>typeof name</code> | |

View file

@ -68,6 +68,7 @@ describe('AggConfig Filters', () => {
{
gte: 1024,
lt: 2048.0,
label: 'A custom label',
}
);
@ -78,6 +79,7 @@ describe('AggConfig Filters', () => {
expect(filter.range).toHaveProperty('bytes');
expect(filter.range.bytes).toHaveProperty('gte', 1024.0);
expect(filter.range.bytes).toHaveProperty('lt', 2048.0);
expect(filter.range.bytes).not.toHaveProperty('label');
expect(filter.meta).toHaveProperty('formattedValue');
});
});

View file

@ -25,7 +25,7 @@ import { IBucketAggConfig } from '../bucket_agg_type';
export const createFilterRange = (
getFieldFormatsStart: AggTypesDependencies['getFieldFormatsStart']
) => {
return (aggConfig: IBucketAggConfig, params: any) => {
return (aggConfig: IBucketAggConfig, { label, ...params }: any) => {
const { deserialize } = getFieldFormatsStart();
return buildRangeFilter(
aggConfig.params.field,

View file

@ -41,6 +41,7 @@ export interface AggParamsRange extends BaseAggParams {
ranges?: Array<{
from: number;
to: number;
label?: string;
}>;
}
@ -71,7 +72,7 @@ export const getRangeBucketAgg = ({ getFieldFormatsStart }: RangeBucketAggDepend
key = keys.get(id);
if (!key) {
key = new RangeKey(bucket);
key = new RangeKey(bucket, agg.params.ranges);
keys.set(id, key);
}
@ -102,7 +103,11 @@ export const getRangeBucketAgg = ({ getFieldFormatsStart }: RangeBucketAggDepend
{ from: 1000, to: 2000 },
],
write(aggConfig, output) {
output.params.ranges = aggConfig.params.ranges;
output.params.ranges = (aggConfig.params as AggParamsRange).ranges?.map((range) => ({
to: range.to,
from: range.from,
}));
output.params.keyed = true;
},
},

View file

@ -19,14 +19,36 @@
const id = Symbol('id');
type Ranges = Array<
Partial<{
from: string | number;
to: string | number;
label: string;
}>
>;
export class RangeKey {
[id]: string;
gte: string | number;
lt: string | number;
label?: string;
constructor(bucket: any) {
private findCustomLabel(
from: string | number | undefined | null,
to: string | number | undefined | null,
ranges?: Ranges
) {
return (ranges || []).find(
(range) =>
((from == null && range.from == null) || range.from === from) &&
((to == null && range.to == null) || range.to === to)
)?.label;
}
constructor(bucket: any, allRanges?: Ranges) {
this.gte = bucket.from == null ? -Infinity : bucket.from;
this.lt = bucket.to == null ? +Infinity : bucket.to;
this.label = this.findCustomLabel(bucket.from, bucket.to, allRanges);
this[id] = RangeKey.idBucket(bucket);
}

View file

@ -79,6 +79,16 @@ describe('getFormatWithAggs', () => {
expect(getFormat).toHaveBeenCalledTimes(1);
});
test('returns custom label for range if provided', () => {
const mapping = { id: 'range', params: {} };
const getFieldFormat = getFormatWithAggs(getFormat);
const format = getFieldFormat(mapping);
expect(format.convert({ gte: 1, lt: 20, label: 'custom' })).toBe('custom');
// underlying formatter is not called because custom label can be used directly
expect(getFormat).toHaveBeenCalledTimes(0);
});
test('creates custom format for terms', () => {
const mapping = {
id: 'terms',

View file

@ -48,6 +48,9 @@ export function getFormatWithAggs(getFieldFormat: GetFieldFormat): GetFieldForma
const customFormats: Record<string, () => IFieldFormat> = {
range: () => {
const RangeFormat = FieldFormat.from((range: any) => {
if (range.label) {
return range.label;
}
const nestedFormatter = params as SerializedFieldFormat;
const format = getFieldFormat({
id: nestedFormatter.id,

View file

@ -26,6 +26,7 @@ export interface Range {
type: typeof name;
from: number;
to: number;
label?: string;
}
export const range: ExpressionTypeDefinition<typeof name, Range> = {
@ -41,7 +42,7 @@ export const range: ExpressionTypeDefinition<typeof name, Range> = {
},
to: {
render: (value: Range): ExpressionValueRender<{ text: string }> => {
const text = `from ${value.from} to ${value.to}`;
const text = value?.label || `from ${value.from} to ${value.to}`;
return {
type: 'render',
as: 'text',

View file

@ -1069,6 +1069,8 @@ export interface Range {
// (undocumented)
from: number;
// (undocumented)
label?: string;
// (undocumented)
to: number;
// Warning: (ae-forgotten-export) The symbol "name" needs to be exported by the entry point index.d.ts
//

View file

@ -894,6 +894,8 @@ export interface Range {
// (undocumented)
from: number;
// (undocumented)
label?: string;
// (undocumented)
to: number;
// Warning: (ae-forgotten-export) The symbol "name" needs to be exported by the entry point index.d.ts
//

View file

@ -25,7 +25,12 @@ import { keys } from '@elastic/eui';
import { IFieldFormat } from '../../../../../../../../src/plugins/data/common';
import { RangeTypeLens, isValidRange, isValidNumber } from './ranges';
import { FROM_PLACEHOLDER, TO_PLACEHOLDER, TYPING_DEBOUNCE_TIME } from './constants';
import { NewBucketButton, DragDropBuckets, DraggableBucketContainer } from '../shared_components';
import {
NewBucketButton,
DragDropBuckets,
DraggableBucketContainer,
LabelInput,
} from '../shared_components';
const generateId = htmlIdGenerator();
@ -63,7 +68,7 @@ export const RangePopover = ({
// send the range back to the main state
setRange(newRange);
};
const { from, to } = tempRange;
const { from, to, label } = tempRange;
const lteAppendLabel = i18n.translate('xpack.lens.indexPattern.ranges.lessThanOrEqualAppend', {
defaultMessage: '\u2264',
@ -159,6 +164,25 @@ export const RangePopover = ({
</EuiFlexItem>
</EuiFlexGroup>
</EuiFormRow>
<EuiFormRow>
<LabelInput
value={label || ''}
onChange={(newLabel) => {
const newRange = {
...tempRange,
label: newLabel,
};
setTempRange(newRange);
saveRangeAndReset(newRange);
}}
placeholder={i18n.translate(
'xpack.lens.indexPattern.ranges.customRangeLabelPlaceholder',
{ defaultMessage: 'Custom label' }
)}
onSubmit={onSubmit}
dataTestSubj="indexPattern-ranges-label"
/>
</EuiFormRow>
</EuiPopover>
);
};

View file

@ -23,6 +23,7 @@ import {
} from './constants';
import { RangePopover } from './advanced_editor';
import { DragDropBuckets } from '../shared_components';
import { EuiFieldText } from '@elastic/eui';
const dataPluginMockValue = dataPluginMock.createStartContract();
// need to overwrite the formatter field first
@ -152,6 +153,25 @@ describe('ranges', () => {
})
);
});
it('should include custom labels', () => {
setToRangeMode();
(state.layers.first.columns.col1 as RangeIndexPatternColumn).params.ranges = [
{ from: 0, to: 100, label: 'customlabel' },
];
const esAggsConfig = rangeOperation.toEsAggsConfig(
state.layers.first.columns.col1 as RangeIndexPatternColumn,
'col1',
{} as IndexPattern
);
expect((esAggsConfig as { params: unknown }).params).toEqual(
expect.objectContaining({
ranges: [{ from: 0, to: 100, label: 'customlabel' }],
})
);
});
});
describe('getPossibleOperationForField', () => {
@ -419,6 +439,63 @@ describe('ranges', () => {
});
});
it('should add a new range with custom label', () => {
const setStateSpy = jest.fn();
const instance = mount(
<InlineOptions
{...defaultOptions}
state={state}
setState={setStateSpy}
columnId="col1"
currentColumn={state.layers.first.columns.col1 as RangeIndexPatternColumn}
layerId="first"
/>
);
// This series of act clojures are made to make it work properly the update flush
act(() => {
instance.find(EuiButtonEmpty).prop('onClick')!({} as ReactMouseEvent);
});
act(() => {
// need another wrapping for this in order to work
instance.update();
expect(instance.find(RangePopover)).toHaveLength(2);
// edit the label and check
instance.find(RangePopover).find(EuiFieldText).first().prop('onChange')!({
target: {
value: 'customlabel',
},
} as React.ChangeEvent<HTMLInputElement>);
jest.advanceTimersByTime(TYPING_DEBOUNCE_TIME * 4);
expect(setStateSpy).toHaveBeenCalledWith({
...state,
layers: {
first: {
...state.layers.first,
columns: {
...state.layers.first.columns,
col1: {
...state.layers.first.columns.col1,
params: {
...state.layers.first.columns.col1.params,
ranges: [
{ from: 0, to: DEFAULT_INTERVAL, label: '' },
{ from: DEFAULT_INTERVAL, to: Infinity, label: 'customlabel' },
],
},
},
},
},
},
});
});
});
it('should open a popover to edit an existing range', () => {
const setStateSpy = jest.fn();

View file

@ -61,9 +61,9 @@ function getEsAggsParams({ sourceField, params }: RangeIndexPatternColumn) {
field: sourceField,
ranges: params.ranges.filter(isValidRange).map<Partial<RangeType>>((range) => {
if (isFullRange(range)) {
return { from: range.from, to: range.to };
return range;
}
const partialRange: Partial<RangeType> = {};
const partialRange: Partial<RangeType> = { label: range.label };
// be careful with the fields to set on partial ranges
if (isValidNumber(range.from)) {
partialRange.from = range.from;