Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Lens] Make default dimension labels auto translate #159089

Merged
merged 21 commits into from
Jun 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import moment from 'moment';
import dateMath, { Unit } from '@kbn/datemath';

import { i18n } from '@kbn/i18n';
import { parseEsInterval } from '../../../utils';

const unitsDesc = dateMath.unitsDesc;
Expand Down Expand Up @@ -71,3 +72,90 @@ export function convertIntervalToEsInterval(interval: string): EsInterval {
expression: interval,
};
}

declare module 'moment' {
interface Locale {
_config: moment.LocaleSpecification;
}
}

// Below 5 seconds the "humanize" call returns the "few seconds" sentence, which is not ok for ms
// This special config rewrite makes it sure to have precision also for sub-seconds durations
// ref: https://github.com/moment/moment/issues/348
export function getPreciseDurationDescription(
intervalValue: number,
unit: moment.unitOfTime.Base
): string {
// moment cannot format anything below seconds, so this requires a manual handling
if (unit === 'millisecond') {
return intervalValue === 1
? i18n.translate('data.search.aggs.buckets.intervalOptions.millisecond', {
defaultMessage: 'millisecond',
})
: i18n.translate('data.search.aggs.buckets.intervalOptions.milliseconds', {
defaultMessage: '{n} milliseconds',
values: { n: intervalValue },
});
stratoula marked this conversation as resolved.
Show resolved Hide resolved
}
// Save default values
const roundingDefault = moment.relativeTimeRounding();
const units = [
{ unit: 'm', value: 60 }, // This should prevent to round up 45 minutes to "an hour"
{ unit: 's', value: 60 }, // this should prevent to round up 45 seconds to "a minute"
{ unit: 'ss', value: 0 }, // This should prevent to round anything below 5 seconds to "few seconds"
{ unit: 'ms', value: 1000 }, // this should render precision at milliseconds level
];
const defaultValues = units.map(({ unit: u }) => moment.relativeTimeThreshold(u) as number);

const DIGITS = 2;
const powValue = Math.pow(10, DIGITS);
moment.relativeTimeRounding((t) => {
return Math.round(t * powValue) / powValue;
});
units.forEach(({ unit: u, value }) => moment.relativeTimeThreshold(u, value));

const defaultLocaleConfig = moment.localeData()._config;
moment.updateLocale(moment.locale(), {
relativeTime: {
ss: (n: number): string => {
return n === 1
? i18n.translate('data.search.aggs.buckets.intervalOptions.second', {
defaultMessage: 'second',
})
: i18n.translate('data.search.aggs.buckets.intervalOptions.seconds', {
defaultMessage: '{n} seconds',
values: { n },
});
},
stratoula marked this conversation as resolved.
Show resolved Hide resolved
m: i18n.translate('data.search.aggs.buckets.intervalOptions.minute', {
defaultMessage: 'minute',
}),
stratoula marked this conversation as resolved.
Show resolved Hide resolved
h: i18n.translate('data.search.aggs.buckets.intervalOptions.hourly', {
defaultMessage: 'hour',
}),
d: i18n.translate('data.search.aggs.buckets.intervalOptions.daily', {
defaultMessage: 'day',
}),
w: i18n.translate('data.search.aggs.buckets.intervalOptions.weekly', {
defaultMessage: 'week',
}),
M: i18n.translate('data.search.aggs.buckets.intervalOptions.monthly', {
defaultMessage: 'month',
}),
y: i18n.translate('data.search.aggs.buckets.intervalOptions.yearly', {
defaultMessage: 'year',
}),
},
});

// Execute the format/humanize call in the callback
const result = moment.duration(intervalValue, unit).locale(i18n.getLocale()).humanize();

// restore all the default values now in moment to not break it
units.forEach(({ unit: u }, i) => moment.relativeTimeThreshold(unit, defaultValues[i]));
moment.relativeTimeRounding(roundingDefault);

// restore all the default values now in moment to not break it
moment.updateLocale(moment.locale(), defaultLocaleConfig);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the small chance there's an exception, I'd probably favour a try/finally on the locale modification to avoid accidentally modifying global state.

return result;
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
convertDurationToNormalizedEsInterval,
convertIntervalToEsInterval,
EsInterval,
getPreciseDurationDescription,
} from './calc_es_interval';
import { autoInterval } from '../../_interval_options';

Expand Down Expand Up @@ -272,22 +273,19 @@ export class TimeBuckets {
? convertDurationToNormalizedEsInterval(interval, originalUnit)
: convertIntervalToEsInterval(this._originalInterval);

const prettyUnits = moment.normalizeUnits(esInterval.unit);
const prettyUnits = moment.normalizeUnits(esInterval.unit) as moment.unitOfTime.Base;
const durationDescription = getPreciseDurationDescription(esInterval.value, prettyUnits);

return Object.assign(interval, {
description:
esInterval.value === 1 ? prettyUnits : esInterval.value + ' ' + prettyUnits + 's',
description: durationDescription,
esValue: esInterval.value,
esUnit: esInterval.unit,
expression: esInterval.expression,
});
};

if (useNormalizedEsInterval) {
return decorateInterval(maybeScaleInterval(parsedInterval));
} else {
return decorateInterval(parsedInterval);
}
return decorateInterval(
useNormalizedEsInterval ? maybeScaleInterval(parsedInterval) : parsedInterval
);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -247,32 +247,36 @@ describe('IndexPattern Data Source', () => {
dataType: 'number',
isBucketed: false,
label: 'Foo',
customLabel: true,
operationType: 'count',
sourceField: '___records___',
};
const map = FormBasedDatasource.uniqueLabels({
layers: {
a: {
columnOrder: ['a', 'b'],
columns: {
a: col,
b: col,
const map = FormBasedDatasource.uniqueLabels(
{
layers: {
a: {
columnOrder: ['a', 'b'],
columns: {
a: col,
b: col,
},
indexPatternId: 'foo',
},
indexPatternId: 'foo',
},
b: {
columnOrder: ['c', 'd'],
columns: {
c: col,
d: {
...col,
label: 'Foo [1]',
b: {
columnOrder: ['c', 'd'],
columns: {
c: col,
d: {
...col,
label: 'Foo [1]',
},
},
indexPatternId: 'foo',
},
indexPatternId: 'foo',
},
},
} as unknown as FormBasedPrivateState);
} as unknown as FormBasedPrivateState,
indexPatterns
);

expect(map).toMatchInlineSnapshot(`
Object {
Expand Down Expand Up @@ -583,7 +587,7 @@ describe('IndexPattern Data Source', () => {
Object {
"arguments": Object {
"idMap": Array [
"{\\"col-0-0\\":[{\\"label\\":\\"Count of records\\",\\"dataType\\":\\"number\\",\\"isBucketed\\":false,\\"sourceField\\":\\"___records___\\",\\"operationType\\":\\"count\\",\\"id\\":\\"col1\\"}],\\"col-1-1\\":[{\\"label\\":\\"Date\\",\\"dataType\\":\\"date\\",\\"isBucketed\\":true,\\"operationType\\":\\"date_histogram\\",\\"sourceField\\":\\"timestamp\\",\\"params\\":{\\"interval\\":\\"1d\\"},\\"id\\":\\"col2\\"}]}",
"{\\"col-0-0\\":[{\\"label\\":\\"Count of records\\",\\"dataType\\":\\"number\\",\\"isBucketed\\":false,\\"sourceField\\":\\"___records___\\",\\"operationType\\":\\"count\\",\\"id\\":\\"col1\\"}],\\"col-1-1\\":[{\\"label\\":\\"timestampLabel\\",\\"dataType\\":\\"date\\",\\"isBucketed\\":true,\\"operationType\\":\\"date_histogram\\",\\"sourceField\\":\\"timestamp\\",\\"params\\":{\\"interval\\":\\"1d\\"},\\"id\\":\\"col2\\"}]}",
],
},
"function": "lens_map_to_columns",
Expand Down Expand Up @@ -1127,7 +1131,7 @@ describe('IndexPattern Data Source', () => {
"col1",
],
"outputColumnName": Array [
"Count of records",
"Count of records per hour",
],
"reducedTimeRange": Array [],
"targetUnit": Array [
Expand Down Expand Up @@ -1570,7 +1574,7 @@ describe('IndexPattern Data Source', () => {
"dataType": "string",
"id": "col1",
"isBucketed": true,
"label": "My Op",
"label": "Top 5 values of Missing field",
"operationType": "terms",
"params": Object {
"orderBy": Object {
Expand All @@ -1588,7 +1592,7 @@ describe('IndexPattern Data Source', () => {
"dataType": "number",
"id": "col2",
"isBucketed": false,
"label": "Count of records",
"label": "Count of records per hour",
"operationType": "count",
"sourceField": "___records___",
"timeScale": "h",
Expand All @@ -1597,7 +1601,7 @@ describe('IndexPattern Data Source', () => {
"dataType": "number",
"id": "col3",
"isBucketed": false,
"label": "Count of records",
"label": "Count of records per hour",
"operationType": "count",
"sourceField": "___records___",
"timeScale": "h",
Expand All @@ -1606,7 +1610,7 @@ describe('IndexPattern Data Source', () => {
"dataType": "number",
"id": "col4",
"isBucketed": false,
"label": "Count of records",
"label": "Count of records per hour",
"operationType": "count",
"sourceField": "___records___",
"timeScale": "h",
Expand Down Expand Up @@ -2154,12 +2158,15 @@ describe('IndexPattern Data Source', () => {
describe('getOperationForColumnId', () => {
it('should get an operation for col1', () => {
expect(publicAPI.getOperationForColumnId('col1')).toEqual({
label: 'My Op',
label: 'Top 5 values of Missing field',
dataType: 'string',
isBucketed: true,
isStaticValue: false,
hasTimeShift: false,
hasReducedTimeRange: false,
scale: undefined,
sortingHint: undefined,
interval: undefined,
} as OperationDescriptor);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import type {
UserMessage,
FrameDatasourceAPI,
StateSetter,
IndexPatternMap,
} from '../../types';
import {
changeIndexPattern,
Expand Down Expand Up @@ -498,7 +499,7 @@ export function getFormBasedDatasource({
);
},

uniqueLabels(state: FormBasedPrivateState) {
uniqueLabels(state: FormBasedPrivateState, indexPatternsMap: IndexPatternMap) {
const layers = state.layers;
const columnLabelMap = {} as Record<string, string>;

Expand All @@ -509,7 +510,15 @@ export function getFormBasedDatasource({
return;
}
Object.entries(layer.columns).forEach(([columnId, column]) => {
columnLabelMap[columnId] = uniqueLabelGenerator(column.label);
columnLabelMap[columnId] = uniqueLabelGenerator(
column.customLabel
? column.label
: operationDefinitionMap[column.operationType].getDefaultLabel(
column,
indexPatternsMap[layer.indexPatternId],
layer.columns
)
);
});
});

Expand All @@ -520,7 +529,7 @@ export function getFormBasedDatasource({
domElement: Element,
props: DatasourceDimensionTriggerProps<FormBasedPrivateState>
) => {
const columnLabelMap = formBasedDatasource.uniqueLabels(props.state);
const columnLabelMap = formBasedDatasource.uniqueLabels(props.state, props.indexPatterns);
const uniqueLabel = columnLabelMap[props.columnId];
const formattedLabel = wrapOnDot(uniqueLabel);

Expand Down Expand Up @@ -552,7 +561,7 @@ export function getFormBasedDatasource({
domElement: Element,
props: DatasourceDimensionEditorProps<FormBasedPrivateState>
) => {
const columnLabelMap = formBasedDatasource.uniqueLabels(props.state);
const columnLabelMap = formBasedDatasource.uniqueLabels(props.state, props.indexPatterns);

render(
<KibanaThemeProvider theme$={core.theme.theme$}>
Expand Down Expand Up @@ -746,7 +755,7 @@ export function getFormBasedDatasource({
},

getPublicAPI({ state, layerId, indexPatterns }: PublicAPIProps<FormBasedPrivateState>) {
const columnLabelMap = formBasedDatasource.uniqueLabels(state);
const columnLabelMap = formBasedDatasource.uniqueLabels(state, indexPatterns);
const layer = state.layers[layerId];
const visibleColumnIds = layer.columnOrder.filter((colId) => !isReferenced(layer, colId));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ export const countOperation: OperationDefinition<CountIndexPatternColumn, 'field
}
},
getDefaultLabel: (column, indexPattern) => {
const field = indexPattern.getFieldByName(column.sourceField);
const field = indexPattern?.getFieldByName(column.sourceField);
return ofName(field, column.timeShift, column.timeScale, column.reducedTimeRange);
},
buildColumn({ field, previousColumn }, columnParams) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,8 @@ export function combineErrorMessages(
return messages.length ? messages : undefined;
}

export function getSafeName(name: string, indexPattern: IndexPattern): string {
const field = indexPattern.getFieldByName(name);
export function getSafeName(name: string, indexPattern: IndexPattern | undefined): string {
const field = indexPattern?.getFieldByName(name);
return field
? field.displayName
: i18n.translate('xpack.lens.indexPattern.missingFieldLabel', {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,13 @@ function getExpressionForLayer(
{
...col,
id: colId,
label: col.customLabel
? col.label
: operationDefinitionMap[col.operationType].getDefaultLabel(
col,
indexPattern,
layer.columns
),
},
];

Expand Down Expand Up @@ -368,7 +375,15 @@ function getExpressionForLayer(
dateColumnId: firstDateHistogramColumn?.length ? [firstDateHistogramColumn[0]] : [],
inputColumnId: [id],
outputColumnId: [id],
outputColumnName: [col.label],
outputColumnName: [
col.customLabel
? col.label
: operationDefinitionMap[col.operationType].getDefaultLabel(
col,
indexPattern,
layer.columns
),
],
targetUnit: [col.timeScale!],
reducedTimeRange: col.reducedTimeRange ? [col.reducedTimeRange] : [],
},
Expand Down
Loading