From aba6d6233cfb9c4fb3ce66bef4bea89fb38caccc Mon Sep 17 00:00:00 2001 From: Daniil Suleiman <31325372+sulemanof@users.noreply.github.com> Date: Fri, 3 May 2019 12:03:42 +0300 Subject: [PATCH] [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 --- src/legacy/core_plugins/kibana/migrations.js | 120 ++++++++---- .../core_plugins/kibana/migrations.test.js | 127 +++++++++++++ .../buckets/date_histogram/_editor.js | 6 +- .../buckets/date_histogram/_params.js | 5 +- src/legacy/ui/public/agg_types/agg_param.d.ts | 9 +- .../agg_types/buckets/_interval_options.js | 6 - .../agg_types/buckets/date_histogram.js | 39 ++-- .../ui/public/agg_types/buckets/histogram.js | 1 - .../agg_types/controls/time_interval.html | 58 ------ .../agg_types/controls/time_interval.tsx | 172 ++++++++++++++++++ .../directives/validate_date_interval.js | 59 ------ src/legacy/ui/public/agg_types/index.d.ts | 2 +- src/legacy/ui/public/agg_types/utils.ts | 72 ++++++++ .../utils.tsx => utils/parse_interval.d.ts} | 25 +-- .../public/vis/editors/default/agg_param.js | 18 +- .../editors/default/agg_param_editor_props.ts | 3 +- .../public/vis/editors/default/agg_params.js | 2 +- test/functional/apps/visualize/_area_chart.js | 2 +- .../_vertical_bar_chart_nontimeindex.js | 2 +- .../functional/page_objects/visualize_page.js | 14 +- test/functional/services/combo_box.js | 18 +- .../rollup/public/visualize/editor_config.js | 16 +- .../translations/translations/zh-CN.json | 6 - 23 files changed, 524 insertions(+), 258 deletions(-) delete mode 100644 src/legacy/ui/public/agg_types/controls/time_interval.html create mode 100644 src/legacy/ui/public/agg_types/controls/time_interval.tsx delete mode 100644 src/legacy/ui/public/agg_types/directives/validate_date_interval.js create mode 100644 src/legacy/ui/public/agg_types/utils.ts rename src/legacy/ui/public/{agg_types/utils.tsx => utils/parse_interval.d.ts} (65%) diff --git a/src/legacy/core_plugins/kibana/migrations.js b/src/legacy/core_plugins/kibana/migrations.js index 4e153e587ee9a..2ef85b9165b94 100644 --- a/src/legacy/core_plugins/kibana/migrations.js +++ b/src/legacy/core_plugins/kibana/migrations.js @@ -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) => { diff --git a/src/legacy/core_plugins/kibana/migrations.test.js b/src/legacy/core_plugins/kibana/migrations.test.js index f09696174ae78..5a862460eb8ed 100644 --- a/src/legacy/core_plugins/kibana/migrations.test.js +++ b/src/legacy/core_plugins/kibana/migrations.test.js @@ -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', () => { diff --git a/src/legacy/ui/public/agg_types/__tests__/buckets/date_histogram/_editor.js b/src/legacy/ui/public/agg_types/__tests__/buckets/date_histogram/_editor.js index 4f699ee462f62..296b1fb9b0b97 100644 --- a/src/legacy/ui/public/agg_types/__tests__/buckets/date_histogram/_editor.js +++ b/src/legacy/ui/public/agg_types/__tests__/buckets/date_histogram/_editor.js @@ -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); }); }); diff --git a/src/legacy/ui/public/agg_types/__tests__/buckets/date_histogram/_params.js b/src/legacy/ui/public/agg_types/__tests__/buckets/date_histogram/_params.js index 0d717784e39ff..ff45d8cfef519 100644 --- a/src/legacy/ui/public/agg_types/__tests__/buckets/date_histogram/_params.js +++ b/src/legacy/ui/public/agg_types/__tests__/buckets/date_histogram/_params.js @@ -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 () { diff --git a/src/legacy/ui/public/agg_types/agg_param.d.ts b/src/legacy/ui/public/agg_types/agg_param.d.ts index 91d78d98b0fa9..02c56d624acec 100644 --- a/src/legacy/ui/public/agg_types/agg_param.d.ts +++ b/src/legacy/ui/public/agg_types/agg_param.d.ts @@ -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 }; diff --git a/src/legacy/ui/public/agg_types/buckets/_interval_options.js b/src/legacy/ui/public/agg_types/buckets/_interval_options.js index 983daa0ca4335..4f1f306f68f21 100644 --- a/src/legacy/ui/public/agg_types/buckets/_interval_options.js +++ b/src/legacy/ui/public/agg_types/buckets/_interval_options.js @@ -77,11 +77,5 @@ export const intervalOptions = [ defaultMessage: 'Yearly', }), val: 'y' - }, - { - display: i18n.translate('common.ui.aggTypes.buckets.intervalOptions.customDisplayName', { - defaultMessage: 'Custom', - }), - val: 'custom' } ]; diff --git a/src/legacy/ui/public/agg_types/buckets/date_histogram.js b/src/legacy/ui/public/agg_types/buckets/date_histogram.js index 6e557d951c8c6..5a59b11274030 100644 --- a/src/legacy/ui/public/agg_types/buckets/date_histogram.js +++ b/src/legacy/ui/public/agg_types/buckets/date_histogram.js @@ -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' }, diff --git a/src/legacy/ui/public/agg_types/buckets/histogram.js b/src/legacy/ui/public/agg_types/buckets/histogram.js index 4f16d294eb381..5f5b1d2e9e95b 100644 --- a/src/legacy/ui/public/agg_types/buckets/histogram.js +++ b/src/legacy/ui/public/agg_types/buckets/histogram.js @@ -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'; diff --git a/src/legacy/ui/public/agg_types/controls/time_interval.html b/src/legacy/ui/public/agg_types/controls/time_interval.html deleted file mode 100644 index 4a28b2df8b8c5..0000000000000 --- a/src/legacy/ui/public/agg_types/controls/time_interval.html +++ /dev/null @@ -1,58 +0,0 @@ -
- - - -
- {{editorConfig.customInterval.help}} -
-
diff --git a/src/legacy/ui/public/agg_types/controls/time_interval.tsx b/src/legacy/ui/public/agg_types/controls/time_interval.tsx new file mode 100644 index 0000000000000..e4246d8ab5aae --- /dev/null +++ b/src/legacy/ui/public/agg_types/controls/time_interval.tsx @@ -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) { + 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 ? ( + + {' '} + + + ) : 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 ( + + + + ); +} + +const tooManyBucketsTooltip = ( + +); +const tooLargeBucketsTooltip = ( + +); +const selectOptionHelpText = ( + +); + +export { TimeIntervalParamEditor }; diff --git a/src/legacy/ui/public/agg_types/directives/validate_date_interval.js b/src/legacy/ui/public/agg_types/directives/validate_date_interval.js deleted file mode 100644 index 26a53a5a459fe..0000000000000 --- a/src/legacy/ui/public/agg_types/directives/validate_date_interval.js +++ /dev/null @@ -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; - } - } - } - }; - }); - diff --git a/src/legacy/ui/public/agg_types/index.d.ts b/src/legacy/ui/public/agg_types/index.d.ts index 6832c4ebcbd4a..848ab790803b6 100644 --- a/src/legacy/ui/public/agg_types/index.d.ts +++ b/src/legacy/ui/public/agg_types/index.d.ts @@ -17,5 +17,5 @@ * under the License. */ -export { AggParam } from './agg_param'; +export { AggParam, AggParamOption } from './agg_param'; export { AggType } from './agg_type'; diff --git a/src/legacy/ui/public/agg_types/utils.ts b/src/legacy/ui/public/agg_types/utils.ts new file mode 100644 index 0000000000000..66fc5d85d7ab7 --- /dev/null +++ b/src/legacy/ui/public/agg_types/utils.ts @@ -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 }; diff --git a/src/legacy/ui/public/agg_types/utils.tsx b/src/legacy/ui/public/utils/parse_interval.d.ts similarity index 65% rename from src/legacy/ui/public/agg_types/utils.tsx rename to src/legacy/ui/public/utils/parse_interval.d.ts index 497709c9ad2f9..9d78b4ef6cddf 100644 --- a/src/legacy/ui/public/agg_types/utils.tsx +++ b/src/legacy/ui/public/utils/parse_interval.d.ts @@ -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; diff --git a/src/legacy/ui/public/vis/editors/default/agg_param.js b/src/legacy/ui/public/vis/editors/default/agg_param.js index afaf52e2c8cd8..2bb6f4087580d 100644 --- a/src/legacy/ui/public/vis/editors/default/agg_param.js +++ b/src/legacy/ui/public/vis/editors/default/agg_param.js @@ -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(); }; diff --git a/src/legacy/ui/public/vis/editors/default/agg_param_editor_props.ts b/src/legacy/ui/public/vis/editors/default/agg_param_editor_props.ts index 1f78d2463ae23..43e3edbc6bf13 100644 --- a/src/legacy/ui/public/vis/editors/default/agg_param_editor_props.ts +++ b/src/legacy/ui/public/vis/editors/default/agg_param_editor_props.ts @@ -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 { agg: AggConfig; aggParam: AggParam; editorConfig: EditorConfig; - indexedFields?: any[]; + indexedFields?: FieldParamType[]; showValidation: boolean; value: T; setValidity(isValid: boolean): void; diff --git a/src/legacy/ui/public/vis/editors/default/agg_params.js b/src/legacy/ui/public/vis/editors/default/agg_params.js index d09fb3b860aca..fe0207e74233f 100644 --- a/src/legacy/ui/public/vis/editors/default/agg_params.js +++ b/src/legacy/ui/public/vis/editors/default/agg_params.js @@ -217,7 +217,7 @@ uiModules } function normalizeModelName(modelName = '') { - return modelName.replace('-', '_'); + return modelName.replace(/-/g, '_'); } } }; diff --git a/test/functional/apps/visualize/_area_chart.js b/test/functional/apps/visualize/_area_chart.js index e4f815f8c7ac5..51a6b55e4777f 100644 --- a/test/functional/apps/visualize/_area_chart.js +++ b/test/functional/apps/visualize/_area_chart.js @@ -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(); }; diff --git a/test/functional/apps/visualize/_vertical_bar_chart_nontimeindex.js b/test/functional/apps/visualize/_vertical_bar_chart_nontimeindex.js index e845742036e80..1eaab913e8ffb 100644 --- a/test/functional/apps/visualize/_vertical_bar_chart_nontimeindex.js +++ b/test/functional/apps/visualize/_vertical_bar_chart_nontimeindex.js @@ -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(); }; diff --git a/test/functional/page_objects/visualize_page.js b/test/functional/page_objects/visualize_page.js index 0296cea74c248..589e2bd4ee3fc 100644 --- a/test/functional/page_objects/visualize_page.js +++ b/test/functional/page_objects/visualize_page.js @@ -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) { diff --git a/test/functional/services/combo_box.js b/test/functional/services/combo_box.js index 1164eb88d374e..776fb3af494c2 100644 --- a/test/functional/services/combo_box.js +++ b/test/functional/services/combo_box.js @@ -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); diff --git a/x-pack/plugins/rollup/public/visualize/editor_config.js b/x-pack/plugins/rollup/public/visualize/editor_config.js index 39f554bc2f439..4eb8e2b70b129 100644 --- a/x-pack/plugins/rollup/public/visualize/editor_config.js +++ b/x-pack/plugins/rollup/public/visualize/editor_config.js @@ -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 } + }) } }; } diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 8e3203b00582c..3e70d7918d5d6 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -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": "高级设置",