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 @@
-
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": "高级设置",