Skip to content

Commit 06278dc

Browse files
dej611cchaoskibanamachinewylieconlonflash1293
authored
[7.x] [Lens] Improved range formatter (#80132) (#81826)
Co-authored-by: Caroline Horn <549577+cchaos@users.noreply.github.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Wylie Conlon <wylieconlon@gmail.com> Co-authored-by: Joe Reuter <johannes.reuter@elastic.co>
1 parent 9ed004c commit 06278dc

File tree

17 files changed

+384
-144
lines changed

17 files changed

+384
-144
lines changed

src/plugins/data/common/search/aggs/utils/get_format_with_aggs.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,33 @@ describe('getFormatWithAggs', () => {
7979
expect(getFormat).toHaveBeenCalledTimes(1);
8080
});
8181

82+
test('creates alternative format for range using the template parameter', () => {
83+
const mapping = { id: 'range', params: { template: 'arrow_right' } };
84+
const getFieldFormat = getFormatWithAggs(getFormat);
85+
const format = getFieldFormat(mapping);
86+
87+
expect(format.convert({ gte: 1, lt: 20 })).toBe('1 → 20');
88+
expect(getFormat).toHaveBeenCalledTimes(1);
89+
});
90+
91+
test('handles Infinity values internally when no nestedFormatter is passed', () => {
92+
const mapping = { id: 'range', params: { replaceInfinity: true } };
93+
const getFieldFormat = getFormatWithAggs(getFormat);
94+
const format = getFieldFormat(mapping);
95+
96+
expect(format.convert({ gte: -Infinity, lt: Infinity })).toBe('≥ −∞ and < +∞');
97+
expect(getFormat).toHaveBeenCalledTimes(1);
98+
});
99+
100+
test('lets Infinity values handling to nestedFormatter even when flag is on', () => {
101+
const mapping = { id: 'range', params: { replaceInfinity: true, id: 'any' } };
102+
const getFieldFormat = getFormatWithAggs(getFormat);
103+
const format = getFieldFormat(mapping);
104+
105+
expect(format.convert({ gte: -Infinity, lt: Infinity })).toBe('≥ -Infinity and < Infinity');
106+
expect(getFormat).toHaveBeenCalledTimes(1);
107+
});
108+
82109
test('returns custom label for range if provided', () => {
83110
const mapping = { id: 'range', params: {} };
84111
const getFieldFormat = getFormatWithAggs(getFormat);

src/plugins/data/common/search/aggs/utils/get_format_with_aggs.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,15 +56,35 @@ export function getFormatWithAggs(getFieldFormat: GetFieldFormat): GetFieldForma
5656
id: nestedFormatter.id,
5757
params: nestedFormatter.params,
5858
});
59+
5960
const gte = '\u2265';
6061
const lt = '\u003c';
62+
let fromValue = format.convert(range.gte);
63+
let toValue = format.convert(range.lt);
64+
// In case of identity formatter and a specific flag, replace Infinity values by specific strings
65+
if (params.replaceInfinity && nestedFormatter.id == null) {
66+
const FROM_PLACEHOLDER = '\u2212\u221E';
67+
const TO_PLACEHOLDER = '+\u221E';
68+
fromValue = isFinite(range.gte) ? fromValue : FROM_PLACEHOLDER;
69+
toValue = isFinite(range.lt) ? toValue : TO_PLACEHOLDER;
70+
}
71+
72+
if (params.template === 'arrow_right') {
73+
return i18n.translate('data.aggTypes.buckets.ranges.rangesFormatMessageArrowRight', {
74+
defaultMessage: '{from} → {to}',
75+
values: {
76+
from: fromValue,
77+
to: toValue,
78+
},
79+
});
80+
}
6181
return i18n.translate('data.aggTypes.buckets.ranges.rangesFormatMessage', {
6282
defaultMessage: '{gte} {from} and {lt} {to}',
6383
values: {
6484
gte,
65-
from: format.convert(range.gte),
85+
from: fromValue,
6686
lt,
67-
to: format.convert(range.lt),
87+
to: toValue,
6888
},
6989
});
7090
});

x-pack/plugins/lens/public/editor_frame_service/format_column.ts

Lines changed: 0 additions & 100 deletions
This file was deleted.

x-pack/plugins/lens/public/editor_frame_service/service.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import {
2323
} from '../types';
2424
import { Document } from '../persistence/saved_object_store';
2525
import { mergeTables } from './merge_tables';
26-
import { formatColumn } from './format_column';
2726
import { EmbeddableFactory, LensEmbeddableStartServices } from './embeddable/embeddable_factory';
2827
import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public';
2928
import { DashboardStart } from '../../../../../src/plugins/dashboard/public';
@@ -86,7 +85,6 @@ export class EditorFrameService {
8685
getAttributeService: () => Promise<LensAttributeService>
8786
): EditorFrameSetup {
8887
plugins.expressions.registerFunction(() => mergeTables);
89-
plugins.expressions.registerFunction(() => formatColumn);
9088

9189
const getStartServices = async (): Promise<LensEmbeddableStartServices> => {
9290
const [coreStart, deps] = await core.getStartServices();

x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -435,7 +435,8 @@ export function DimensionEditor(props: DimensionEditorProps) {
435435
/>
436436
)}
437437

438-
{selectedColumn && selectedColumn.dataType === 'number' ? (
438+
{selectedColumn &&
439+
(selectedColumn.dataType === 'number' || selectedColumn.operationType === 'range') ? (
439440
<FormatSelector
440441
selectedColumn={selectedColumn}
441442
onChange={(newFormat) => {
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import {
8+
ExpressionFunctionDefinition,
9+
Datatable,
10+
DatatableColumn,
11+
} from 'src/plugins/expressions/public';
12+
13+
interface FormatColumn {
14+
format: string;
15+
columnId: string;
16+
decimals?: number;
17+
parentFormat?: string;
18+
}
19+
20+
export const supportedFormats: Record<
21+
string,
22+
{ decimalsToPattern: (decimals?: number) => string }
23+
> = {
24+
number: {
25+
decimalsToPattern: (decimals = 2) => {
26+
if (decimals === 0) {
27+
return `0,0`;
28+
}
29+
return `0,0.${'0'.repeat(decimals)}`;
30+
},
31+
},
32+
percent: {
33+
decimalsToPattern: (decimals = 2) => {
34+
if (decimals === 0) {
35+
return `0,0%`;
36+
}
37+
return `0,0.${'0'.repeat(decimals)}%`;
38+
},
39+
},
40+
bytes: {
41+
decimalsToPattern: (decimals = 2) => {
42+
if (decimals === 0) {
43+
return `0,0b`;
44+
}
45+
return `0,0.${'0'.repeat(decimals)}b`;
46+
},
47+
},
48+
};
49+
50+
export const formatColumn: ExpressionFunctionDefinition<
51+
'lens_format_column',
52+
Datatable,
53+
FormatColumn,
54+
Datatable
55+
> = {
56+
name: 'lens_format_column',
57+
type: 'datatable',
58+
help: '',
59+
args: {
60+
format: {
61+
types: ['string'],
62+
help: '',
63+
required: true,
64+
},
65+
columnId: {
66+
types: ['string'],
67+
help: '',
68+
required: true,
69+
},
70+
decimals: {
71+
types: ['number'],
72+
help: '',
73+
},
74+
parentFormat: {
75+
types: ['string'],
76+
help: '',
77+
},
78+
},
79+
inputTypes: ['datatable'],
80+
fn(input, { format, columnId, decimals, parentFormat }: FormatColumn) {
81+
return {
82+
...input,
83+
columns: input.columns.map((col) => {
84+
if (col.id === columnId) {
85+
if (!parentFormat) {
86+
if (supportedFormats[format]) {
87+
return withParams(col, {
88+
id: format,
89+
params: { pattern: supportedFormats[format].decimalsToPattern(decimals) },
90+
});
91+
} else if (format) {
92+
return withParams(col, { id: format });
93+
} else {
94+
return col;
95+
}
96+
}
97+
98+
const parsedParentFormat = JSON.parse(parentFormat);
99+
const parentFormatId = parsedParentFormat.id;
100+
const parentFormatParams = parsedParentFormat.params ?? {};
101+
102+
if (!parentFormatId) {
103+
return col;
104+
}
105+
106+
if (format && supportedFormats[format]) {
107+
return withParams(col, {
108+
id: parentFormatId,
109+
params: {
110+
id: format,
111+
params: {
112+
pattern: supportedFormats[format].decimalsToPattern(decimals),
113+
},
114+
...parentFormatParams,
115+
},
116+
});
117+
}
118+
if (parentFormatParams) {
119+
const innerParams = (col.meta.params?.params as Record<string, unknown>) ?? {};
120+
return withParams(col, {
121+
...col.meta.params,
122+
params: {
123+
...innerParams,
124+
...parentFormatParams,
125+
},
126+
});
127+
}
128+
}
129+
return col;
130+
}),
131+
};
132+
},
133+
};
134+
135+
function withParams(col: DatatableColumn, params: Record<string, unknown>) {
136+
return { ...col, meta: { ...col.meta, params } };
137+
}

x-pack/plugins/lens/public/indexpattern_datasource/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,11 @@ export class IndexPatternDatasource {
3333
{ expressions, editorFrame, charts }: IndexPatternDatasourceSetupPlugins
3434
) {
3535
editorFrame.registerDatasource(async () => {
36-
const { getIndexPatternDatasource, renameColumns } = await import('../async_services');
36+
const { getIndexPatternDatasource, renameColumns, formatColumn } = await import(
37+
'../async_services'
38+
);
3739
expressions.registerFunction(renameColumns);
40+
expressions.registerFunction(formatColumn);
3841
return core.getStartServices().then(([coreStart, { data }]) =>
3942
getIndexPatternDatasource({
4043
core: coreStart,

x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ export function uniqueLabels(layers: Record<string, IndexPatternLayer>) {
107107
}
108108

109109
export * from './rename_columns';
110+
export * from './format_column';
110111

111112
export function getIndexPatternDatasource({
112113
core,

x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ const indexPattern2 = ({
117117
title: 'my-fake-restricted-pattern',
118118
timeFieldName: 'timestamp',
119119
hasRestrictions: true,
120+
fieldFormatMap: { bytes: { id: 'bytes', params: { pattern: '0.0' } } },
120121
fields: [
121122
{
122123
name: 'timestamp',

x-pack/plugins/lens/public/indexpattern_datasource/loader.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,14 @@ export async function loadIndexPatterns({
103103
id: indexPattern.id!, // id exists for sure because we got index patterns by id
104104
title,
105105
timeFieldName,
106-
fieldFormatMap,
106+
fieldFormatMap:
107+
fieldFormatMap &&
108+
Object.fromEntries(
109+
Object.entries(fieldFormatMap).map(([id, format]) => [
110+
id,
111+
'toJSON' in format ? format.toJSON() : format,
112+
])
113+
),
107114
fields: newFields,
108115
hasRestrictions: !!typeMeta?.aggs,
109116
};

0 commit comments

Comments
 (0)