[Lens] Custom labels for ranges (#79628)
This commit is contained in:
parent
136eda1d78
commit
5fd7ad3b34
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [Range](./kibana-plugin-plugins-expressions-public.range.md) > [label](./kibana-plugin-plugins-expressions-public.range.label.md)
|
||||
|
||||
## Range.label property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
label?: string;
|
||||
```
|
|
@ -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> | |
|
||||
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [Range](./kibana-plugin-plugins-expressions-server.range.md) > [label](./kibana-plugin-plugins-expressions-server.range.label.md)
|
||||
|
||||
## Range.label property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
label?: string;
|
||||
```
|
|
@ -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> | |
|
||||
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
//
|
||||
|
|
|
@ -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
|
||||
//
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue