[Vis: Default editor] EUIficate time interval control (#34991) (#36006)

* EUIficate time interval control

* Update tests

* Remove empty option, update fix tests

* Bind vis to scope for react component only

* Combine two interval inputs into one EuiCombobox

* Add error message

* Add migration script; remove unused translations

* Update fuctional tests

* Update unit test

* Update tests; refactoring

* Use flow to invoke several functions

* Update test

* Refactoring

* Reset options when timeBase

* Add type for editorConfig prop

* Add placeholder

* Fix lint errors

* Call write after interval changing

* Fix code review comments

* Make replace for model name global

* Revert error catch

* Remove old dependency

* Add unit test for migration test

* Fix message

* Fix code review comments

* Update functional test
This commit is contained in:
Daniil Suleiman 2019-05-03 12:03:42 +03:00 committed by GitHub
parent 813b6ef8b7
commit aba6d6233c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 524 additions and 258 deletions

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { cloneDeep, get, omit, has } from 'lodash';
import { cloneDeep, get, omit, has, flow } from 'lodash';
function migrateIndexPattern(doc) {
const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON');
@ -57,6 +57,85 @@ function migrateIndexPattern(doc) {
doc.attributes.kibanaSavedObjectMeta.searchSourceJSON = JSON.stringify(searchSource);
}
// [TSVB] Migrate percentile-rank aggregation (value -> values)
const migratePercentileRankAggregation = doc => {
const visStateJSON = get(doc, 'attributes.visState');
let visState;
if (visStateJSON) {
try {
visState = JSON.parse(visStateJSON);
} catch (e) {
// Let it go, the data is invalid and we'll leave it as is
}
if (visState && visState.type === 'metrics') {
const series = get(visState, 'params.series') || [];
series.forEach(part => {
(part.metrics || []).forEach(metric => {
if (metric.type === 'percentile_rank' && has(metric, 'value')) {
metric.values = [metric.value];
delete metric.value;
}
});
});
return {
...doc,
attributes: {
...doc.attributes,
visState: JSON.stringify(visState),
},
};
}
}
return doc;
};
// Migrate date histogram aggregation (remove customInterval)
const migrateDateHistogramAggregation = doc => {
const visStateJSON = get(doc, 'attributes.visState');
let visState;
if (visStateJSON) {
try {
visState = JSON.parse(visStateJSON);
} catch (e) {
// Let it go, the data is invalid and we'll leave it as is
}
if (visState && visState.aggs) {
visState.aggs.forEach(agg => {
if (agg.type === 'date_histogram' && agg.params) {
if (agg.params.interval === 'custom') {
agg.params.interval = agg.params.customInterval;
}
delete agg.params.customInterval;
}
if (get(agg, 'params.customBucket.type', null) === 'date_histogram'
&& agg.params.customBucket.params
) {
if (agg.params.customBucket.params.interval === 'custom') {
agg.params.customBucket.params.interval = agg.params.customBucket.params.customInterval;
}
delete agg.params.customBucket.params.customInterval;
}
});
return {
...doc,
attributes: {
...doc.attributes,
visState: JSON.stringify(visState),
}
};
}
}
return doc;
};
const executeMigrations710 = flow(migratePercentileRankAggregation, migrateDateHistogramAggregation);
function removeDateHistogramTimeZones(doc) {
const visStateJSON = get(doc, 'attributes.visState');
if (visStateJSON) {
@ -185,44 +264,7 @@ export const migrations = {
}
},
'7.0.1': removeDateHistogramTimeZones,
'7.1.0': doc => {
// [TSVB] Migrate percentile-rank aggregation (value -> values)
const migratePercentileRankAggregation = doc => {
const visStateJSON = get(doc, 'attributes.visState');
let visState;
if (visStateJSON) {
try {
visState = JSON.parse(visStateJSON);
} catch (e) {
// Let it go, the data is invalid and we'll leave it as is
}
if (visState && visState.type === 'metrics') {
const series = get(visState, 'params.series') || [];
series.forEach(part => {
(part.metrics || []).forEach(metric => {
if (metric.type === 'percentile_rank' && has(metric, 'value')) {
metric.values = [metric.value];
delete metric.value;
}
});
});
return {
...doc,
attributes: {
...doc.attributes,
visState: JSON.stringify(visState),
},
};
}
}
return doc;
};
return migratePercentileRankAggregation(doc);
}
'7.1.0': doc => executeMigrations710(doc)
},
dashboard: {
'7.0.0': (doc) => {

View file

@ -716,6 +716,133 @@ Object {
expect(() => migrate(doc)).toThrowError(/My Vis/);
});
});
describe('date histogram custom interval removal', () => {
const migrate = doc => migrations.visualization['7.1.0'](doc);
let doc;
beforeEach(() => {
doc = {
attributes: {
visState: JSON.stringify({
aggs: [
{
'enabled': true,
'id': '1',
'params': {
'customInterval': '1h'
},
'schema': 'metric',
'type': 'count'
},
{
'enabled': true,
'id': '2',
'params': {
'customInterval': '2h',
'drop_partials': false,
'extended_bounds': {},
'field': 'timestamp',
'interval': 'auto',
'min_doc_count': 1,
'useNormalizedEsInterval': true
},
'schema': 'segment',
'type': 'date_histogram'
},
{
'enabled': true,
'id': '4',
'params': {
'customInterval': '2h',
'drop_partials': false,
'extended_bounds': {},
'field': 'timestamp',
'interval': 'custom',
'min_doc_count': 1,
'useNormalizedEsInterval': true
},
'schema': 'segment',
'type': 'date_histogram'
},
{
'enabled': true,
'id': '3',
'params': {
'customBucket': {
'enabled': true,
'id': '1-bucket',
'params': {
'customInterval': '2h',
'drop_partials': false,
'extended_bounds': {},
'field': 'timestamp',
'interval': 'custom',
'min_doc_count': 1,
'useNormalizedEsInterval': true
},
'type': 'date_histogram'
},
'customMetric': {
'enabled': true,
'id': '1-metric',
'params': {},
'type': 'count'
}
},
'schema': 'metric',
'type': 'max_bucket'
},
]
}),
}
};
});
it('should remove customInterval from date_histogram aggregations', () => {
const migratedDoc = migrate(doc);
const aggs = JSON.parse(migratedDoc.attributes.visState).aggs;
expect(aggs[1]).not.toHaveProperty('params.customInterval');
});
it('should not change interval from date_histogram aggregations', () => {
const migratedDoc = migrate(doc);
const aggs = JSON.parse(migratedDoc.attributes.visState).aggs;
expect(aggs[1].params.interval).toBe(JSON.parse(doc.attributes.visState).aggs[1].params.interval);
});
it('should not remove customInterval from non date_histogram aggregations', () => {
const migratedDoc = migrate(doc);
const aggs = JSON.parse(migratedDoc.attributes.visState).aggs;
expect(aggs[0]).toHaveProperty('params.customInterval');
});
it('should set interval with customInterval value and remove customInterval when interval equals "custom"', () => {
const migratedDoc = migrate(doc);
const aggs = JSON.parse(migratedDoc.attributes.visState).aggs;
expect(aggs[2].params.interval).toBe(JSON.parse(doc.attributes.visState).aggs[2].params.customInterval);
expect(aggs[2]).not.toHaveProperty('params.customInterval');
});
it('should remove customInterval from nested aggregations', () => {
const migratedDoc = migrate(doc);
const aggs = JSON.parse(migratedDoc.attributes.visState).aggs;
expect(aggs[3]).not.toHaveProperty('params.customBucket.params.customInterval');
});
it('should remove customInterval from nested aggregations and set interval with customInterval value', () => {
const migratedDoc = migrate(doc);
const aggs = JSON.parse(migratedDoc.attributes.visState).aggs;
expect(aggs[3].params.customBucket.params.interval)
.toBe(JSON.parse(doc.attributes.visState).aggs[3].params.customBucket.params.customInterval);
expect(aggs[3]).not.toHaveProperty('params.customBucket.params.customInterval');
});
it('should not fail on date histograms without a customInterval', () => {
const migratedDoc = migrate(doc);
const aggs = JSON.parse(migratedDoc.attributes.visState).aggs;
expect(aggs[3]).not.toHaveProperty('params.customInterval');
});
});
});
describe('dashboard', () => {

View file

@ -100,7 +100,7 @@ describe('editor', function () {
beforeEach(ngMock.inject(function () {
field = _.sample(indexPattern.fields);
interval = _.sample(intervalOptions);
params = render({ field: field, interval: interval });
params = render({ field: field, interval: interval.val });
}));
it('renders the field editor', function () {
@ -112,11 +112,11 @@ describe('editor', function () {
});
it('renders the interval editor', function () {
expect(agg.params.interval).to.be(interval);
expect(agg.params.interval).to.be(interval.val);
expect(params).to.have.property('interval');
expect(params.interval).to.have.property('$el');
expect(params.interval.modelValue()).to.be(interval);
expect($scope.agg.params.interval).to.be(interval.val);
});
});

View file

@ -68,9 +68,8 @@ describe('date_histogram params', function () {
expect(output.params).to.have.property('interval', '1d');
});
it('ignores invalid intervals', function () {
const output = writeInterval('foo');
expect(output.params).to.have.property('interval', '0ms');
it('throws error when interval is invalid', function () {
expect(() => writeInterval('foo')).to.throw('TypeError: "foo" is not a valid interval.');
});
it('automatically picks an interval', function () {

View file

@ -22,10 +22,17 @@ import { AggConfig } from '../vis';
interface AggParam {
type: string;
name: string;
options?: AggParamOption[];
required?: boolean;
displayName?: string;
onChange?(agg: AggConfig): void;
shouldShow?(agg: AggConfig): boolean;
}
export { AggParam };
interface AggParamOption {
val: string;
display: string;
enabled?(agg: AggConfig): void;
}
export { AggParam, AggParamOption };

View file

@ -77,11 +77,5 @@ export const intervalOptions = [
defaultMessage: 'Yearly',
}),
val: 'y'
},
{
display: i18n.translate('common.ui.aggTypes.buckets.intervalOptions.customDisplayName', {
defaultMessage: 'Custom',
}),
val: 'custom'
}
];

View file

@ -20,12 +20,11 @@
import _ from 'lodash';
import chrome from '../../chrome';
import moment from 'moment-timezone';
import '../directives/validate_date_interval';
import { BucketAggType } from './_bucket_agg_type';
import { TimeBuckets } from '../../time_buckets';
import { createFilterDateHistogram } from './create_filter/date_histogram';
import { intervalOptions } from './_interval_options';
import intervalTemplate from '../controls/time_interval.html';
import { TimeIntervalParamEditor } from '../controls/time_interval';
import { timefilter } from '../../timefilter';
import { DropPartialsParamEditor } from '../controls/drop_partials';
import { i18n } from '@kbn/i18n';
@ -35,11 +34,7 @@ const detectedTimezone = moment.tz.guess();
const tzOffset = moment().format('Z');
function getInterval(agg) {
const interval = _.get(agg, ['params', 'interval']);
if (interval && interval.val === 'custom') {
return _.get(agg, ['params', 'customInterval']);
}
return interval;
return _.get(agg, ['params', 'interval']);
}
export function setBounds(agg, force) {
@ -99,7 +94,7 @@ export const dateHistogramBucketAgg = new BucketAggType({
return agg.getIndexPattern().timeFieldName;
},
onChange: function (agg) {
if (_.get(agg, 'params.interval.val') === 'auto' && !agg.fieldIsTimeField()) {
if (_.get(agg, 'params.interval') === 'auto' && !agg.fieldIsTimeField()) {
delete agg.params.interval;
}
@ -118,18 +113,24 @@ export const dateHistogramBucketAgg = new BucketAggType({
},
{
name: 'interval',
type: 'optioned',
deserialize: function (state) {
editorComponent: TimeIntervalParamEditor,
deserialize: function (state, agg) {
// For upgrading from 7.0.x to 7.1.x - intervals are now stored as key of options or custom value
if (state === 'custom') {
return _.get(agg, 'params.customInterval');
}
const interval = _.find(intervalOptions, { val: state });
return interval || _.find(intervalOptions, function (option) {
// For upgrading from 4.0.x to 4.1.x - intervals are now stored as 'y' instead of 'year',
// but this maps the old values to the new values
return Number(moment.duration(1, state)) === Number(moment.duration(1, option.val));
});
// For upgrading from 4.0.x to 4.1.x - intervals are now stored as 'y' instead of 'year',
// but this maps the old values to the new values
if (!interval && state === 'year') {
return 'y';
}
return state;
},
default: 'auto',
options: intervalOptions,
editor: intervalTemplate,
modifyAggConfigOnSearchRequestStart: function (agg) {
setBounds(agg, true);
},
@ -186,12 +187,6 @@ export const dateHistogramBucketAgg = new BucketAggType({
return field && field.name && field.name === agg.getIndexPattern().timeFieldName;
},
},
{
name: 'customInterval',
default: '2h',
write: _.noop
},
{
name: 'format'
},

View file

@ -20,7 +20,6 @@
import _ from 'lodash';
import { toastNotifications } from 'ui/notify';
import '../directives/validate_date_interval';
import chrome from '../../chrome';
import { BucketAggType } from './_bucket_agg_type';
import { createFilterHistogram } from './create_filter/histogram';

View file

@ -1,58 +0,0 @@
<div class="form-group">
<label
for="visEditorInterval{{agg.id}}"
>
<span
i18n-id="common.ui.aggTypes.timeIntervalLabel"
i18n-default-message="Interval"
></span>
<icon-tip
ng-if="agg.buckets.getInterval().scaled && (agg.buckets.getInterval().scale <= 1)"
type="'alert'"
position="'right'"
content="'common.ui.aggTypes.intervalCreatesTooManyBucketsTooltip' | i18n: {
defaultMessage: 'This interval creates too many buckets to show in the selected time range, so it has been scaled to {bucketDescription}',
values: { bucketDescription: agg.buckets.getInterval().description } }"
></icon-tip>
<icon-tip
ng-if="agg.buckets.getInterval().scaled && (agg.buckets.getInterval().scale > 1)"
type="'alert'"
position="'right'"
content="'common.ui.aggTypes.intervalCreatesTooLargeBucketsTooltip' | i18n: {
defaultMessage: 'This interval creates buckets that are too large to show in the selected time range, so it has been scaled to {bucketDescription}',
values: { bucketDescription: agg.buckets.getInterval().description } }"
></icon-tip>
</label>
<select
ng-if="!editorConfig.customInterval.timeBase"
id="visEditorInterval{{agg.id}}"
ng-model="agg.params.interval"
ng-change="agg.write()"
required
ng-options="opt as opt.display for opt in aggParam.options.raw | filter: optionEnabled"
class="form-control"
name="interval">
<option
value=""
i18n-id="common.ui.aggTypes.selectTimeIntervalLabel"
i18n-default-message="-- select a valid interval --"
></option>
</select>
<input
aria-label="{{ ::'common.ui.aggTypes.customTimeIntervalAriaLabel' | i18n: { defaultMessage: 'Custom interval' } }}"
type="text"
name="customInterval"
ng-model="agg.params.customInterval"
validate-date-interval="{{editorConfig.customInterval.timeBase}}"
ng-change="aggForm.customInterval.$valid && agg.write()"
ng-if="agg.params.interval.val == 'custom'"
class="form-control"
required />
<div
ng-if="editorConfig.customInterval.help"
class="kuiSubText kuiSubduedText kuiVerticalRhythmSmall"
style="margin-top: 5px"
>
<span>{{editorConfig.customInterval.help}}</span>
</div>
</div>

View file

@ -0,0 +1,172 @@
/*
* 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 { get, find } from 'lodash';
import React, { useEffect } from 'react';
import { EuiFormRow, EuiIconTip, EuiComboBox, EuiComboBoxOptionProps } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { AggParamEditorProps } from '../../vis/editors/default';
import { AggParamOption } from '../agg_param';
import { isValidInterval } from '../utils';
interface ComboBoxOption extends EuiComboBoxOptionProps {
key: string;
}
function TimeIntervalParamEditor({
agg,
aggParam,
editorConfig,
value,
setValue,
showValidation,
setValidity,
}: AggParamEditorProps<string>) {
const timeBase: string = get(editorConfig, 'interval.timeBase');
const options = timeBase
? []
: (aggParam.options || []).reduce(
(filtered: ComboBoxOption[], option: AggParamOption) => {
if (option.enabled ? option.enabled(agg) : true) {
filtered.push({ label: option.display, key: option.val });
}
return filtered;
},
[] as ComboBoxOption[]
);
let selectedOptions: ComboBoxOption[] = [];
let definedOption: ComboBoxOption | undefined;
let isValid = false;
if (value) {
definedOption = find(options, { key: value });
selectedOptions = definedOption ? [definedOption] : [{ label: value, key: 'custom' }];
isValid = !!(definedOption || isValidInterval(value, timeBase));
}
const interval = get(agg, 'buckets.getInterval') && agg.buckets.getInterval();
const scaledHelpText =
interval && interval.scaled && isValid ? (
<strong className="eui-displayBlock">
<FormattedMessage
id="common.ui.aggTypes.timeInterval.scaledHelpText"
defaultMessage="Currently scaled to {bucketDescription}"
values={{ bucketDescription: get(interval, 'description') || '' }}
/>{' '}
<EuiIconTip
position="right"
type="questionInCircle"
content={interval.scale <= 1 ? tooManyBucketsTooltip : tooLargeBucketsTooltip}
/>
</strong>
) : null;
const helpText = (
<>
{scaledHelpText}
{get(editorConfig, 'interval.help') || selectOptionHelpText}
</>
);
const errors = [];
if (!isValid && value) {
errors.push(
i18n.translate('common.ui.aggTypes.timeInterval.invalidFormatErrorMessage', {
defaultMessage: 'Invalid interval format.',
})
);
}
const onCustomInterval = (customValue: string) => {
const normalizedCustomValue = customValue.trim();
setValue(normalizedCustomValue);
if (normalizedCustomValue && isValidInterval(normalizedCustomValue, timeBase)) {
agg.write();
}
};
const onChange = (opts: EuiComboBoxOptionProps[]) => {
const selectedOpt: ComboBoxOption = get(opts, '0');
setValue(selectedOpt ? selectedOpt.key : selectedOpt);
if (selectedOpt) {
agg.write();
}
};
useEffect(
() => {
setValidity(isValid);
},
[isValid]
);
return (
<EuiFormRow
className="visEditorSidebar__aggParamFormRow"
error={errors}
fullWidth={true}
helpText={helpText}
isInvalid={showValidation ? !isValid : false}
label={i18n.translate('common.ui.aggTypes.timeInterval.minimumIntervalLabel', {
defaultMessage: 'Minimum interval',
})}
>
<EuiComboBox
fullWidth={true}
data-test-subj="visEditorInterval"
isInvalid={showValidation ? !isValid : false}
noSuggestions={!!timeBase}
onChange={onChange}
onCreateOption={onCustomInterval}
options={options}
selectedOptions={selectedOptions}
singleSelection={{ asPlainText: true }}
placeholder={i18n.translate('common.ui.aggTypes.timeInterval.selectIntervalPlaceholder', {
defaultMessage: 'Select an interval',
})}
/>
</EuiFormRow>
);
}
const tooManyBucketsTooltip = (
<FormattedMessage
id="common.ui.aggTypes.timeInterval.createsTooManyBucketsTooltip"
defaultMessage="This interval creates too many buckets to show in the selected time range, so it has been scaled up."
/>
);
const tooLargeBucketsTooltip = (
<FormattedMessage
id="common.ui.aggTypes.timeInterval.createsTooLargeBucketsTooltip"
defaultMessage="This interval creates buckets that are too large to show in the selected time range, so it has been scaled down."
/>
);
const selectOptionHelpText = (
<FormattedMessage
id="common.ui.aggTypes.timeInterval.selectOptionHelpText"
defaultMessage="Select an option or create a custom value. Examples: 30s, 20m, 24h, 2d, 1w, 1M"
/>
);
export { TimeIntervalParamEditor };

View file

@ -1,59 +0,0 @@
/*
* 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 { parseInterval } from '../../utils/parse_interval';
import { uiModules } from '../../modules';
import { leastCommonInterval } from '../../vis/lib/least_common_interval';
uiModules
.get('kibana')
.directive('validateDateInterval', function () {
return {
restrict: 'A',
require: 'ngModel',
link: function ($scope, $el, attrs, ngModelCntrl) {
const baseInterval = attrs.validateDateInterval || null;
ngModelCntrl.$parsers.push(check);
ngModelCntrl.$formatters.push(check);
function check(value) {
if(baseInterval) {
ngModelCntrl.$setValidity('dateInterval', parseWithBase(value) === true);
} else {
ngModelCntrl.$setValidity('dateInterval', parseInterval(value) != null);
}
return value;
}
// When base interval is set, check for least common interval and allow
// input the value is the same. This means that the input interval is a
// multiple of the base interval.
function parseWithBase(value) {
try {
const interval = leastCommonInterval(baseInterval, value);
return interval === value.replace(/\s/g, '');
} catch(e) {
return false;
}
}
}
};
});

View file

@ -17,5 +17,5 @@
* under the License.
*/
export { AggParam } from './agg_param';
export { AggParam, AggParamOption } from './agg_param';
export { AggType } from './agg_type';

View file

@ -0,0 +1,72 @@
/*
* 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 { parseInterval } from '../utils/parse_interval';
import { leastCommonInterval } from '../vis/lib/least_common_interval';
/**
* Check a string if it's a valid JSON.
*
* @param {string} value a string that should be validated
* @returns {boolean} true if value is a valid JSON or if value is an empty string, or a string with whitespaces, otherwise false
*/
function isValidJson(value: string): boolean {
if (!value || value.length === 0) {
return true;
}
const trimmedValue = value.trim();
if (trimmedValue.length === 0) {
return true;
}
if (trimmedValue[0] === '{' || trimmedValue[0] === '[') {
try {
JSON.parse(trimmedValue);
return true;
} catch (e) {
return false;
}
} else {
return false;
}
}
function isValidInterval(value: string, baseInterval: string) {
if (baseInterval) {
return _parseWithBase(value, baseInterval);
} else {
return parseInterval(value) !== null;
}
}
// When base interval is set, check for least common interval and allow
// input the value is the same. This means that the input interval is a
// multiple of the base interval.
function _parseWithBase(value: string, baseInterval: string) {
try {
const interval = leastCommonInterval(baseInterval, value);
return interval === value.replace(/\s/g, '');
} catch (e) {
return false;
}
}
export { isValidJson, isValidInterval };

View file

@ -17,27 +17,6 @@
* under the License.
*/
function isValidJson(value: string): boolean {
if (!value || value.length === 0) {
return true;
}
import moment from 'moment';
const trimmedValue = value.trim();
if (trimmedValue.length === 0) {
return true;
}
if (trimmedValue[0] === '{' || trimmedValue[0] === '[') {
try {
JSON.parse(trimmedValue);
return true;
} catch (e) {
return false;
}
} else {
return false;
}
}
export { isValidJson };
export function parseInterval(interval: string): moment.Duration | null;

View file

@ -17,7 +17,6 @@
* under the License.
*/
import { isFunction } from 'lodash';
import { wrapInI18nContext } from 'ui/i18n';
import { uiModules } from '../../../modules';
import { AggParamReactWrapper } from './agg_param_react_wrapper';
@ -74,22 +73,12 @@ uiModules
link: {
pre: function ($scope, $el, attr) {
$scope.$bind('aggParam', attr.aggParam);
$scope.$bind('agg', attr.agg);
$scope.$bind('editorComponent', attr.editorComponent);
$scope.$bind('indexedFields', attr.indexedFields);
},
post: function ($scope, $el, attr, ngModelCtrl) {
$scope.config = config;
$scope.showValidation = false;
$scope.optionEnabled = function (option) {
if (option && isFunction(option.enabled)) {
return option.enabled($scope.agg);
}
return true;
};
if (attr.editorComponent) {
$scope.$watch('agg.params[aggParam.name]', (value) => {
// Whenever the value of the parameter changed (e.g. by a reset or actually by calling)
@ -105,14 +94,13 @@ uiModules
$scope.showValidation = true;
}
}, true);
$scope.paramValue = $scope.agg.params[$scope.aggParam.name];
}
$scope.onChange = (value) => {
// This is obviously not a good code quality, but without using scope binding (which we can't see above)
// to bind function values, this is right now the best temporary fix, until all of this will be gone.
$scope.$parent.onParamChange($scope.agg, $scope.aggParam.name, value);
$scope.paramValue = value;
$scope.onParamChange($scope.agg, $scope.aggParam.name, value);
$scope.showValidation = true;
ngModelCtrl.$setDirty();
};

View file

@ -19,6 +19,7 @@
import { AggParam } from '../../../agg_types';
import { AggConfig } from '../../agg_config';
import { FieldParamType } from '../../../agg_types/param_types';
import { EditorConfig } from '../config/types';
// NOTE: we cannot export the interface with export { InterfaceName }
@ -29,7 +30,7 @@ export interface AggParamEditorProps<T> {
agg: AggConfig;
aggParam: AggParam;
editorConfig: EditorConfig;
indexedFields?: any[];
indexedFields?: FieldParamType[];
showValidation: boolean;
value: T;
setValidity(isValid: boolean): void;

View file

@ -217,7 +217,7 @@ uiModules
}
function normalizeModelName(modelName = '') {
return modelName.replace('-', '_');
return modelName.replace(/-/g, '_');
}
}
};

View file

@ -50,7 +50,7 @@ export default function ({ getService, getPageObjects }) {
expect(fieldValues[0]).to.be('@timestamp');
const intervalValue = await PageObjects.visualize.getInterval();
log.debug('intervalValue = ' + intervalValue);
expect(intervalValue).to.be('Auto');
expect(intervalValue[0]).to.be('Auto');
return PageObjects.visualize.clickGo();
};

View file

@ -42,7 +42,7 @@ export default function ({ getService, getPageObjects }) {
log.debug('Field = @timestamp');
await PageObjects.visualize.selectField('@timestamp');
await PageObjects.visualize.setCustomInterval('3h');
await PageObjects.visualize.clickGo();
await PageObjects.visualize.waitForVisualizationRenderingStabilized();
};

View file

@ -567,23 +567,17 @@ export function VisualizePageProvider({ getService, getPageObjects, updateBaseli
}
async getInterval() {
const intervalElement = await find.byCssSelector(
`select[ng-model="agg.params.interval"] option[selected]`);
return await intervalElement.getProperty('label');
return await comboBox.getComboBoxSelectedOptions('visEditorInterval');
}
async setInterval(newValue) {
log.debug(`Visualize.setInterval(${newValue})`);
const input = await find.byCssSelector('select[ng-model="agg.params.interval"]');
const option = await input.findByCssSelector(`option[label="${newValue}"]`);
await option.click();
return await comboBox.set('visEditorInterval', newValue);
}
async setCustomInterval(newValue) {
await this.setInterval('Custom');
const input = await find.byCssSelector('input[name="customInterval"]');
await input.clearValue();
await input.type(newValue);
log.debug(`Visualize.setCustomInterval(${newValue})`);
return await comboBox.setCustom('visEditorInterval', newValue);
}
async getNumericInterval(agg = 2) {

View file

@ -17,13 +17,14 @@
* under the License.
*/
export function ComboBoxProvider({ getService }) {
export function ComboBoxProvider({ getService, getPageObjects }) {
const config = getService('config');
const testSubjects = getService('testSubjects');
const find = getService('find');
const log = getService('log');
const retry = getService('retry');
const browser = getService('browser');
const PageObjects = getPageObjects(['common']);
const WAIT_FOR_EXISTS_TIME = config.get('timeouts.waitForExists');
@ -60,6 +61,21 @@ export function ComboBoxProvider({ getService }) {
await this.closeOptionsList(comboBoxElement);
}
/**
* This method set custom value to comboBox.
* It applies changes by pressing Enter key. Sometimes it may lead to auto-submitting a form.
*
* @param {string} comboBoxSelector
* @param {string} value
*/
async setCustom(comboBoxSelector, value) {
log.debug(`comboBox.setCustom, comboBoxSelector: ${comboBoxSelector}, value: ${value}`);
const comboBoxElement = await testSubjects.find(comboBoxSelector);
await this._filterOptionsList(comboBoxElement, value);
await PageObjects.common.pressEnterKey();
await this.closeOptionsList(comboBoxElement);
}
async filterOptionsList(comboBoxSelector, filterValue) {
log.debug(`comboBox.filterOptionsList, comboBoxSelector: ${comboBoxSelector}, filter: ${filterValue}`);
const comboBox = await testSubjects.find(comboBoxSelector);

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import { editorConfigProviders } from 'ui/vis/editors/config/editor_config_providers';
export function initEditorConfig() {
@ -45,7 +46,10 @@ export function initEditorConfig() {
},
interval: {
base: interval,
help: `Must be a multiple of rollup configuration interval: ${interval}`
help: i18n.translate('xpack.rollupJobs.editorConfig.histogram.interval.helpText', {
defaultMessage: 'Must be a multiple of rollup configuration interval: {interval}',
values: { interval }
})
}
} : {};
}
@ -54,16 +58,16 @@ export function initEditorConfig() {
if (aggTypeName === 'date_histogram') {
const interval = fieldAgg.interval;
return {
interval: {
fixedValue: 'custom',
},
useNormalizedEsInterval: {
fixedValue: false,
},
customInterval: {
interval: {
default: interval,
timeBase: interval,
help: `Must be a multiple of rollup configuration interval: ${interval}`
help: i18n.translate('xpack.rollupJobs.editorConfig.dateHistogram.customInterval.helpText', {
defaultMessage: 'Must be a multiple of rollup configuration interval: {interval}',
values: { interval }
})
}
};
}

View file

@ -89,7 +89,6 @@
"common.ui.aggTypes.buckets.geohashGridTitle": "Geohash",
"common.ui.aggTypes.buckets.histogramTitle": "直方图",
"common.ui.aggTypes.buckets.intervalOptions.autoDisplayName": "自动",
"common.ui.aggTypes.buckets.intervalOptions.customDisplayName": "定制",
"common.ui.aggTypes.buckets.intervalOptions.dailyDisplayName": "每日",
"common.ui.aggTypes.buckets.intervalOptions.hourlyDisplayName": "每小时",
"common.ui.aggTypes.buckets.intervalOptions.millisecondDisplayName": "毫秒",
@ -112,7 +111,6 @@
"common.ui.aggTypes.buckets.termsTitle": "词",
"common.ui.aggTypes.changePrecisionLabel": "更改地图缩放的精确度",
"common.ui.aggTypes.customMetricLabel": "定制指标",
"common.ui.aggTypes.customTimeIntervalAriaLabel": "定制时间间隔",
"common.ui.aggTypes.dateRanges.acceptedDateFormatsLinkText": "已接受日期格式",
"common.ui.aggTypes.dateRanges.addRangeButtonLabel": "添加范围",
"common.ui.aggTypes.dateRanges.fromColumnLabel": "从",
@ -135,8 +133,6 @@
"common.ui.aggTypes.filters.requiredFilterLabel": "必需:",
"common.ui.aggTypes.filters.toggleFilterButtonAriaLabel": "切换筛选标签",
"common.ui.aggTypes.histogram.missingMaxMinValuesWarning": "无法检索最大值和最小值以自动缩放直方图存储桶。这可能会导致可视化性能低下。",
"common.ui.aggTypes.intervalCreatesTooLargeBucketsTooltip": "此时间间隔将创建过大而无法在选定时间范围内显示的存储桶,因此其已缩放至 {bucketDescription}",
"common.ui.aggTypes.intervalCreatesTooManyBucketsTooltip": "此时间间隔将创建过多的存储桶,而无法在选定时间范围内全部显示,因此其已缩放至 {bucketDescription}",
"common.ui.aggTypes.ipRanges.cidrMask.addRangeButtonLabel": "添加范围",
"common.ui.aggTypes.ipRanges.cidrMask.requiredIpRangeDescription": "必须指定至少一个 IP 范围。",
"common.ui.aggTypes.ipRanges.cidrMask.requiredIpRangeLabel": "必需:",
@ -239,7 +235,6 @@
"common.ui.aggTypes.ranges.requiredRangeDescription": "必须指定至少一个范围。",
"common.ui.aggTypes.ranges.requiredRangeTitle": "必需:",
"common.ui.aggTypes.ranges.toColumnLabel": "到",
"common.ui.aggTypes.selectTimeIntervalLabel": "-- 选择有效的时间间隔 --",
"common.ui.aggTypes.showEmptyBucketsLabel": "显示空存储桶",
"common.ui.aggTypes.showEmptyBucketsTooltip": "显示所有存储桶,不仅仅有结果的存储桶",
"common.ui.aggTypes.sizeLabel": "大小",
@ -247,7 +242,6 @@
"common.ui.aggTypes.sortOnLabel": "排序依据",
"common.ui.aggTypes.sortOnPlaceholder": "选择字段",
"common.ui.aggTypes.sortOnTooltip": "排序依据",
"common.ui.aggTypes.timeIntervalLabel": "时间间隔",
"common.ui.aggTypes.valuesLabel": "值",
"common.ui.chrome.bigUrlWarningNotificationMessage": "在“{advancedSettingsLink}”启用“{storeInSessionStorageParam}”选项,或简化屏幕视觉效果。",
"common.ui.chrome.bigUrlWarningNotificationMessage.advancedSettingsLinkText": "高级设置",