Visual Builder duration in second showing as number (#35813) (#37309)

* Visual Builder duration in second showing as number

Fix: #35641 , #35805

* fix PR comments

* Visual Builder duration in second showing as number

* Visual Builder duration in second showing as number -fix placeholder
This commit is contained in:
Alexey Antonov 2019-05-29 16:56:58 +03:00 committed by GitHub
parent d0a0c3fc7e
commit e10c35156f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 193 additions and 96 deletions

View file

@ -23,35 +23,37 @@ import _ from 'lodash';
import {
htmlIdGenerator, EuiComboBox, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiFieldText, EuiLink,
} from '@elastic/eui';
import { durationOutputOptions, durationInputOptions } from './lib/durations';
import { durationOutputOptions, durationInputOptions, isDuration } from './lib/durations';
import { i18n } from '@kbn/i18n';
import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
const durationFormatTest = /[pnumshdwMY]+,[pnumshdwMY]+/;
const DEFAULT_OUTPUT_PRECISION = '2';
class DataFormatPickerUI extends Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.handleCustomChange = this.handleCustomChange.bind(this);
let from = 'ms';
let to = 'ms';
let decimals = 2;
if (durationFormatTest.test(props.value)) {
let from;
let to;
let decimals;
if (isDuration(props.value)) {
[from, to, decimals] = props.value.split(',');
}
this.state = {
from,
to,
decimals
from: from || 'ms',
to: to || 'ms',
decimals: decimals || '',
};
}
handleCustomChange() {
handleCustomChange = () => {
this.props.onChange([{ value: this.custom && this.custom.value || '' }]);
}
};
handleChange(selectedOptions) {
handleChange = selectedOptions => {
if (selectedOptions.length < 1) {
return;
}
@ -61,12 +63,12 @@ class DataFormatPickerUI extends Component {
} else if (selectedOptions[0].value === 'duration') {
const { from, to, decimals } = this.state;
this.props.onChange([{
value: `${from},${to},${decimals}`
value: `${from},${to},${decimals}`,
}]);
} else {
this.props.onChange(selectedOptions);
}
}
};
handleDurationChange(name) {
return (selectedOptions) => {
@ -82,11 +84,11 @@ class DataFormatPickerUI extends Component {
}
this.setState({
[name]: newValue
[name]: newValue,
}, () => {
const { from, to, decimals } = this.state;
this.props.onChange([{
value: `${from},${to},${decimals}`
value: `${from},${to},${decimals}`,
}]);
});
};
@ -99,16 +101,31 @@ class DataFormatPickerUI extends Component {
if (!_.includes(['bytes', 'number', 'percent'], value)) {
defaultValue = 'custom';
}
if (durationFormatTest.test(value)) {
if (isDuration(value)) {
defaultValue = 'duration';
}
const { intl } = this.props;
const options = [
{ label: intl.formatMessage({ id: 'tsvb.dataFormatPicker.bytesLabel', defaultMessage: 'Bytes' }), value: 'bytes' },
{ label: intl.formatMessage({ id: 'tsvb.dataFormatPicker.numberLabel', defaultMessage: 'Number' }), value: 'number' },
{ label: intl.formatMessage({ id: 'tsvb.dataFormatPicker.percentLabel', defaultMessage: 'Percent' }), value: 'percent' },
{ label: intl.formatMessage({ id: 'tsvb.dataFormatPicker.durationLabel', defaultMessage: 'Duration' }), value: 'duration' },
{ label: intl.formatMessage({ id: 'tsvb.dataFormatPicker.customLabel', defaultMessage: 'Custom' }), value: 'custom' }
{
label: intl.formatMessage({ id: 'tsvb.dataFormatPicker.bytesLabel', defaultMessage: 'Bytes' }),
value: 'bytes',
},
{
label: intl.formatMessage({ id: 'tsvb.dataFormatPicker.numberLabel', defaultMessage: 'Number' }),
value: 'number',
},
{
label: intl.formatMessage({ id: 'tsvb.dataFormatPicker.percentLabel', defaultMessage: 'Percent' }),
value: 'percent',
},
{
label: intl.formatMessage({ id: 'tsvb.dataFormatPicker.durationLabel', defaultMessage: 'Duration' }),
value: 'duration',
},
{
label: intl.formatMessage({ id: 'tsvb.dataFormatPicker.customLabel', defaultMessage: 'Custom' }),
value: 'custom',
},
];
const selectedOption = options.find(option => {
return defaultValue === option.value;
@ -118,7 +135,7 @@ class DataFormatPickerUI extends Component {
if (defaultValue === 'duration') {
const [from, to, decimals] = value.split(',');
const selectedFrom = durationInputOptions.find(option => from === option.value);
const selectedTo = durationOutputOptions.find(option => to === option.value);
const selectedTo = durationOutputOptions.find(option => to === option.value);
return (
<EuiFlexGroup responsive={false} gutterSize="s">
@ -167,21 +184,26 @@ class DataFormatPickerUI extends Component {
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFormRow
id={htmlId('decimal')}
label={(<FormattedMessage
id="tsvb.dataFormatPicker.decimalPlacesLabel"
defaultMessage="Decimal places"
/>)}
>
<EuiFieldText
defaultValue={decimals}
inputRef={(el) => this.decimals = el}
onChange={this.handleDurationChange('decimals')}
/>
</EuiFormRow>
</EuiFlexItem>
{selectedTo && selectedTo.value !== 'humanize' && (
<EuiFlexItem grow={false}>
<EuiFormRow
id={htmlId('decimal')}
label={(<FormattedMessage
id="tsvb.dataFormatPicker.decimalPlacesLabel"
defaultMessage="Decimal places"
/>)}
>
<EuiFieldText
defaultValue={decimals}
inputRef={(el) => this.decimals = el}
placeholder={DEFAULT_OUTPUT_PRECISION}
onChange={this.handleDurationChange('decimals')}
/>
</EuiFormRow>
</EuiFlexItem>)
}
</EuiFlexGroup>
);
}
@ -198,7 +220,9 @@ class DataFormatPickerUI extends Component {
<FormattedMessage
id="tsvb.dataFormatPicker.formatStringHelpText"
defaultMessage="See {numeralJsLink}"
values={{ numeralJsLink: (<EuiLink href="http://numeraljs.com/#format" target="_BLANK">Numeral.js</EuiLink>) }}
values={{
numeralJsLink: (<EuiLink href="http://numeraljs.com/#format" target="_BLANK">Numeral.js</EuiLink>),
}}
/>
</span>
}
@ -232,13 +256,13 @@ class DataFormatPickerUI extends Component {
}
DataFormatPickerUI.defaultProps = {
label: i18n.translate('tsvb.defaultDataFormatterLabel', { defaultMessage: 'Data Formatter' })
label: i18n.translate('tsvb.defaultDataFormatterLabel', { defaultMessage: 'Data Formatter' }),
};
DataFormatPickerUI.propTypes = {
value: PropTypes.string,
label: PropTypes.string,
onChange: PropTypes.func
onChange: PropTypes.func,
};
export const DataFormatPicker = injectI18n(DataFormatPickerUI);

View file

@ -19,53 +19,94 @@
import { i18n } from '@kbn/i18n';
const durationBaseOptions = [
{
label: i18n.translate('tsvb.durationOptions.millisecondsLabel', { defaultMessage: 'Milliseconds' }),
value: 'ms',
},
{
label: i18n.translate('tsvb.durationOptions.secondsLabel', { defaultMessage: 'Seconds' }),
value: 's',
},
{
label: i18n.translate('tsvb.durationOptions.minutesLabel', { defaultMessage: 'Minutes' }),
value: 'm',
},
{
label: i18n.translate('tsvb.durationOptions.hoursLabel', { defaultMessage: 'Hours' }),
value: 'h',
},
{
label: i18n.translate('tsvb.durationOptions.daysLabel', { defaultMessage: 'Days' }),
value: 'd',
},
{
label: i18n.translate('tsvb.durationOptions.weeksLabel', { defaultMessage: 'Weeks' }),
value: 'w',
},
{
label: i18n.translate('tsvb.durationOptions.monthsLabel', { defaultMessage: 'Months' }),
value: 'M',
},
{
label: i18n.translate('tsvb.durationOptions.yearsLabel', { defaultMessage: 'Years' }),
value: 'Y',
},
];
export const durationOutputOptions = [
{
label: i18n.translate('tsvb.durationOptions.millisecondsLabel', { defaultMessage: 'milliseconds' }),
value: 'ms'
label: i18n.translate('tsvb.durationOptions.humanize', { defaultMessage: 'Human readable' }),
value: 'humanize',
},
{
label: i18n.translate('tsvb.durationOptions.secondsLabel', { defaultMessage: 'seconds' }),
value: 's'
},
{
label: i18n.translate('tsvb.durationOptions.minutesLabel', { defaultMessage: 'minutes' }),
value: 'm'
},
{
label: i18n.translate('tsvb.durationOptions.hoursLabel', { defaultMessage: 'hours' }),
value: 'h'
},
{
label: i18n.translate('tsvb.durationOptions.daysLabel', { defaultMessage: 'days' }),
value: 'd'
},
{
label: i18n.translate('tsvb.durationOptions.weeksLabel', { defaultMessage: 'weeks' }),
value: 'w'
},
{
label: i18n.translate('tsvb.durationOptions.monthsLabel', { defaultMessage: 'months' }),
value: 'M'
},
{
label: i18n.translate('tsvb.durationOptions.yearsLabel', { defaultMessage: 'years' }),
value: 'Y'
}
...durationBaseOptions,
];
export const durationInputOptions = [
{
label: i18n.translate('tsvb.durationOptions.picosecondsLabel', { defaultMessage: 'picoseconds' }),
value: 'ps'
label: i18n.translate('tsvb.durationOptions.picosecondsLabel', { defaultMessage: 'Picoseconds' }),
value: 'ps',
},
{
label: i18n.translate('tsvb.durationOptions.nanosecondsLabel', { defaultMessage: 'nanoseconds' }),
value: 'ns'
label: i18n.translate('tsvb.durationOptions.nanosecondsLabel', { defaultMessage: 'Nanoseconds' }),
value: 'ns',
},
{
label: i18n.translate('tsvb.durationOptions.microsecondsLabel', { defaultMessage: 'microseconds' }),
value: 'us' },
...durationOutputOptions
label: i18n.translate('tsvb.durationOptions.microsecondsLabel', { defaultMessage: 'Microseconds' }),
value: 'us',
},
...durationBaseOptions,
];
export const inputFormats = {
'ps': 'picoseconds',
'ns': 'nanoseconds',
'us': 'microseconds',
'ms': 'milliseconds',
's': 'seconds',
'm': 'minutes',
'h': 'hours',
'd': 'days',
'w': 'weeks',
'M': 'months',
'Y': 'years',
};
export const outputFormats = {
'humanize': 'humanize',
'ms': 'asMilliseconds',
's': 'asSeconds',
'm': 'asMinutes',
'h': 'asHours',
'd': 'asDays',
'w': 'asWeeks',
'M': 'asMonths',
'Y': 'asYears',
};
export const isDuration = format => {
const splittedFormat = format.split(',');
const [input, output] = splittedFormat;
return Boolean(inputFormats[input] && outputFormats[output]) && splittedFormat.length === 3;
};

View file

@ -0,0 +1,39 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { isDuration } from './durations';
describe('durations', () => {
describe('isDuration', () => {
test('should return true for valid duration formats', () => {
expect(isDuration('ps,m,2')).toBeTruthy();
expect(isDuration('h,h,1')).toBeTruthy();
expect(isDuration('m,d,')).toBeTruthy();
expect(isDuration('s,Y,4')).toBeTruthy();
expect(isDuration('ps,humanize,')).toBeTruthy();
});
test('should return false for invalid duration formats', () => {
expect(isDuration('ps,j,2')).toBeFalsy();
expect(isDuration('i,h,1')).toBeFalsy();
expect(isDuration('m,d')).toBeFalsy();
expect(isDuration('s')).toBeFalsy();
expect(isDuration('humanize,s,2')).toBeFalsy();
});
});
});

View file

@ -18,36 +18,29 @@
*/
import handlebars from 'handlebars/dist/handlebars';
import { durationInputOptions } from './durations';
import { capitalize, isNumber } from 'lodash';
import { isNumber } from 'lodash';
import { fieldFormats } from 'ui/registry/field_formats';
const durationsLookup = durationInputOptions.reduce((acc, row) => {
acc[row.value] = row.label;
return acc;
}, {});
import { inputFormats, outputFormats, isDuration } from '../lib/durations';
export const tickFormatter = (format = '0,0.[00]', template, getConfig = null) => {
if (!template) template = '{{value}}';
const render = handlebars.compile(template, { knownHelpersOnly: true });
const durationFormatTest = /[pnumshdwMY]+,[pnumshdwMY]+,\d+/;
let formatter;
if (durationFormatTest.test(format)) {
if (isDuration(format)) {
const [from, to, decimals] = format.split(',');
const inputFormat = durationsLookup[from];
const outputFormat = `as${capitalize(durationsLookup[to])}`;
const DurationFormat = fieldFormats.getType('duration');
formatter = new DurationFormat({
inputFormat,
outputFormat,
outputPrecision: decimals
inputFormat: inputFormats[from],
outputFormat: outputFormats[to],
outputPrecision: decimals,
});
} else {
let FieldFormat = fieldFormats.getType(format);
if (FieldFormat) {
formatter = new FieldFormat(null, getConfig);
}
else {
} else {
FieldFormat = fieldFormats.getType('number');
formatter = new FieldFormat({ pattern: format }, getConfig);
}