diff --git a/src/legacy/ui/public/agg_types/buckets/_terms_helper.tsx b/src/legacy/ui/public/agg_types/agg_utils.ts similarity index 64% rename from src/legacy/ui/public/agg_types/buckets/_terms_helper.tsx rename to src/legacy/ui/public/agg_types/agg_utils.ts index f613673b0484a..14412903bc1e2 100644 --- a/src/legacy/ui/public/agg_types/buckets/_terms_helper.tsx +++ b/src/legacy/ui/public/agg_types/agg_utils.ts @@ -17,37 +17,23 @@ * under the License. */ -import { AggConfig } from 'ui/vis'; import { i18n } from '@kbn/i18n'; - -const aggFilter = [ - '!top_hits', - '!percentiles', - '!median', - '!std_dev', - '!derivative', - '!moving_avg', - '!serial_diff', - '!cumulative_sum', - '!avg_bucket', - '!max_bucket', - '!min_bucket', - '!sum_bucket', -]; - -// Returns true if the agg is compatible with the terms bucket -function isCompatibleAgg(agg: AggConfig) { - return !aggFilter.includes(`!${agg.type.name}`); -} +import { AggConfig } from '../vis/agg_config'; function safeMakeLabel(agg: AggConfig) { try { return agg.makeLabel(); } catch (e) { - return i18n.translate('common.ui.aggTypes.buckets.terms.aggNotValidLabel', { + return i18n.translate('common.ui.aggTypes.aggNotValidLabel', { defaultMessage: '- agg not valid -', }); } } -export { aggFilter, isCompatibleAgg, safeMakeLabel }; +function isCompatibleAggregation(aggFilter: string[]) { + return (agg: AggConfig) => { + return !aggFilter.includes(`!${agg.type.name}`); + }; +} + +export { safeMakeLabel, isCompatibleAggregation }; diff --git a/src/legacy/ui/public/agg_types/buckets/terms.js b/src/legacy/ui/public/agg_types/buckets/terms.js index 4eee239730007..e035dd4883c0f 100644 --- a/src/legacy/ui/public/agg_types/buckets/terms.js +++ b/src/legacy/ui/public/agg_types/buckets/terms.js @@ -27,10 +27,9 @@ import { createFilterTerms } from './create_filter/terms'; import { wrapWithInlineComp } from './_inline_comp_wrapper'; import { buildOtherBucketAgg, mergeOtherBucketAggResponse, updateMissingBucket } from './_terms_other_bucket_helper'; import { isStringType, migrateIncludeExcludeFormat } from './migrate_include_exclude_format'; -import { aggFilter } from './_terms_helper'; import orderAggTemplate from '../controls/order_agg.html'; import { OrderParamEditor } from '../controls/order'; -import { OrderAggParamEditor } from '../controls/order_agg'; +import { OrderAggParamEditor, aggFilter } from '../controls/order_agg'; import { SizeParamEditor } from '../controls/size'; import { MissingBucketParamEditor } from '../controls/missing_bucket'; import { OtherBucketParamEditor } from '../controls/other_bucket'; diff --git a/src/legacy/ui/public/agg_types/controls/metric_agg.tsx b/src/legacy/ui/public/agg_types/controls/metric_agg.tsx new file mode 100644 index 0000000000000..15176bcaa0ce8 --- /dev/null +++ b/src/legacy/ui/public/agg_types/controls/metric_agg.tsx @@ -0,0 +1,109 @@ +/* + * 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 React, { useEffect } from 'react'; +import { EuiFormRow, EuiSelect } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { AggParamEditorProps } from 'ui/vis/editors/default'; +import { safeMakeLabel, isCompatibleAggregation } from '../agg_utils'; + +const aggFilter = ['!top_hits', '!percentiles', '!percentile_ranks', '!median', '!std_dev']; +const isCompatibleAgg = isCompatibleAggregation(aggFilter); +const EMPTY_VALUE = 'EMPTY_VALUE'; + +function MetricAggParamEditor({ + agg, + value, + showValidation, + setValue, + setValidity, + setTouched, + responseValueAggs, +}: AggParamEditorProps) { + const label = i18n.translate('common.ui.aggTypes.metricLabel', { + defaultMessage: 'Metric', + }); + const isValid = !!value; + + useEffect( + () => { + setValidity(isValid); + }, + [isValid] + ); + + useEffect( + () => { + if (responseValueAggs && value && value !== 'custom') { + // ensure that metricAgg is set to a valid agg + const respAgg = responseValueAggs + .filter(isCompatibleAgg) + .find(aggregation => aggregation.id === value); + + if (!respAgg) { + setValue(); + } + } + }, + [responseValueAggs] + ); + + const options = responseValueAggs + ? responseValueAggs + .filter(respAgg => respAgg.type.name !== agg.type.name) + .map(respAgg => ({ + text: i18n.translate('common.ui.aggTypes.definiteMetricLabel', { + defaultMessage: 'Metric: {safeMakeLabel}', + values: { + safeMakeLabel: safeMakeLabel(respAgg), + }, + }), + value: respAgg.id, + disabled: !isCompatibleAgg(respAgg), + })) + : []; + + options.push({ + text: i18n.translate('common.ui.aggTypes.customMetricLabel', { + defaultMessage: 'Custom metric', + }), + value: 'custom', + disabled: false, + }); + + if (!value) { + options.unshift({ text: '', value: EMPTY_VALUE, disabled: false }); + } + + return ( + + setValue(ev.target.value)} + fullWidth={true} + isInvalid={showValidation ? !isValid : false} + onBlur={setTouched} + data-test-subj={`visEditorSubAggMetric${agg.id}`} + /> + + ); +} + +export { MetricAggParamEditor }; diff --git a/src/legacy/ui/public/agg_types/controls/order_agg.tsx b/src/legacy/ui/public/agg_types/controls/order_agg.tsx index 569fb4602e9f1..a546ac6f5d9cf 100644 --- a/src/legacy/ui/public/agg_types/controls/order_agg.tsx +++ b/src/legacy/ui/public/agg_types/controls/order_agg.tsx @@ -21,7 +21,23 @@ import React, { useEffect } from 'react'; import { EuiFormRow, EuiSelect } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { AggParamEditorProps } from 'ui/vis/editors/default'; -import { safeMakeLabel, isCompatibleAgg } from '../buckets/_terms_helper'; +import { safeMakeLabel, isCompatibleAggregation } from '../agg_utils'; + +const aggFilter = [ + '!top_hits', + '!percentiles', + '!median', + '!std_dev', + '!derivative', + '!moving_avg', + '!serial_diff', + '!cumulative_sum', + '!avg_bucket', + '!max_bucket', + '!min_bucket', + '!sum_bucket', +]; +const isCompatibleAgg = isCompatibleAggregation(aggFilter); function OrderAggParamEditor({ agg, @@ -116,4 +132,4 @@ function OrderAggParamEditor({ ); } -export { OrderAggParamEditor }; +export { OrderAggParamEditor, aggFilter }; diff --git a/src/legacy/ui/public/agg_types/controls/sub_agg.html b/src/legacy/ui/public/agg_types/controls/sub_agg.html index bcd3bc53690fd..71e3ea1eb0573 100644 --- a/src/legacy/ui/public/agg_types/controls/sub_agg.html +++ b/src/legacy/ui/public/agg_types/controls/sub_agg.html @@ -1,43 +1,8 @@ -
-
- - -
-
- - - - -
+
+ +
diff --git a/src/legacy/ui/public/agg_types/controls/top_field.tsx b/src/legacy/ui/public/agg_types/controls/top_field.tsx index 9069f081ccf99..be6ff91a36a93 100644 --- a/src/legacy/ui/public/agg_types/controls/top_field.tsx +++ b/src/legacy/ui/public/agg_types/controls/top_field.tsx @@ -28,7 +28,7 @@ function TopFieldParamEditor(props: AggParamEditorProps) { const compatibleAggs = getCompatibleAggs(props.agg, props.visName); let customError; - if (!compatibleAggs.length) { + if (props.value && !compatibleAggs.length) { customError = i18n.translate('common.ui.aggTypes.aggregateWith.noAggsErrorTooltip', { defaultMessage: 'The chosen field has no compatible aggregations.', }); diff --git a/src/legacy/ui/public/agg_types/directives/validate_agg.js b/src/legacy/ui/public/agg_types/directives/validate_agg.js deleted file mode 100644 index 39f3881a550d7..0000000000000 --- a/src/legacy/ui/public/agg_types/directives/validate_agg.js +++ /dev/null @@ -1,58 +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 { uiModules } from '../../modules'; - -uiModules - .get('kibana') - .directive('validateAgg', function () { - return { - restrict: 'A', - require: 'ngModel', - scope: { - 'ngModel': '=', - 'agg': '=' - }, - link: function ($scope, elem, attr, ngModel) { - function validateAgg(aggValue) { - if (aggValue == null || aggValue === 'custom') { - ngModel.$setValidity('aggInput', true); - return aggValue; - } - - try { - $scope.agg.params.customMetric = null; - $scope.agg.params.metricAgg = aggValue; - $scope.agg.makeLabel(); - ngModel.$setValidity('aggInput', true); - } catch (e) { - ngModel.$setValidity('aggInput', false); - } - - return aggValue; - } - - // From User - ngModel.$parsers.unshift(validateAgg); - - // To user - ngModel.$formatters.unshift(validateAgg); - } - }; - }); diff --git a/src/legacy/ui/public/agg_types/index.js b/src/legacy/ui/public/agg_types/index.js index 2a0db0a5eab18..2fcc40d540e29 100644 --- a/src/legacy/ui/public/agg_types/index.js +++ b/src/legacy/ui/public/agg_types/index.js @@ -17,7 +17,6 @@ * under the License. */ -import './directives/validate_agg'; import './agg_params'; import { IndexedArray } from '../indexed_array'; import { countMetricAgg } from './metrics/count'; diff --git a/src/legacy/ui/public/agg_types/metrics/lib/parent_pipeline_agg_controller.js b/src/legacy/ui/public/agg_types/metrics/lib/parent_pipeline_agg_controller.js index 963d473dad876..9ca01116f6ad0 100644 --- a/src/legacy/ui/public/agg_types/metrics/lib/parent_pipeline_agg_controller.js +++ b/src/legacy/ui/public/agg_types/metrics/lib/parent_pipeline_agg_controller.js @@ -18,33 +18,22 @@ */ import _ from 'lodash'; -import { safeMakeLabel } from './safe_make_label'; import { i18n } from '@kbn/i18n'; const parentPipelineAggController = function ($scope) { - - $scope.safeMakeLabel = safeMakeLabel; - $scope.$watch('responseValueAggs', updateOrderAgg); $scope.$watch('agg.params.metricAgg', updateOrderAgg); $scope.$on('$destroy', function () { - const lastBucket = _.findLast($scope.state.aggs, agg => agg.type.type === 'buckets'); - if ($scope.aggForm && $scope.aggForm.agg) { - $scope.aggForm.agg.$setValidity('bucket', true); - } + const lastBucket = _.findLast($scope.state.aggs, agg => agg.type && agg.type.type === 'buckets'); + if (lastBucket && lastBucket.error) { delete lastBucket.error; } }); - $scope.isDisabledAgg = function (agg) { - const invalidAggs = ['top_hits', 'percentiles', 'percentile_ranks', 'median', 'std_dev']; - return Boolean(invalidAggs.find(invalidAgg => invalidAgg === agg.type.name)); - }; - function checkBuckets() { - const lastBucket = _.findLast($scope.state.aggs, agg => agg.type.type === 'buckets'); + const lastBucket = _.findLast($scope.state.aggs, agg => agg.type && agg.type.type === 'buckets'); const bucketHasType = lastBucket && lastBucket.type; const bucketIsHistogram = bucketHasType && ['date_histogram', 'histogram'].includes(lastBucket.type.name); const canUseAggregation = lastBucket && bucketIsHistogram; @@ -52,16 +41,13 @@ const parentPipelineAggController = function ($scope) { // remove errors on all buckets _.each($scope.state.aggs, agg => { if (agg.error) delete agg.error; }); - if ($scope.aggForm.agg) { - $scope.aggForm.agg.$setValidity('bucket', canUseAggregation); - } if (canUseAggregation) { lastBucket.params.min_doc_count = (lastBucket.type.name === 'histogram') ? 1 : 0; } else { if (lastBucket) { const type = $scope.agg.type.title; lastBucket.error = i18n.translate('common.ui.aggTypes.metrics.wrongLastBucketTypeErrorMessage', { - defaultMessage: 'Last bucket aggregation must be "Date Histogram" or "Histogram" when using "{type}" metric aggregation!', + defaultMessage: 'Last bucket aggregation must be "Date Histogram" or "Histogram" when using "{type}" metric aggregation.', values: { type }, description: 'Date Histogram and Histogram should not be translated' }); @@ -79,9 +65,6 @@ const parentPipelineAggController = function ($scope) { // we aren't creating a custom aggConfig if (metricAgg !== 'custom') { - if (!$scope.state.aggs.find(agg => agg.id === metricAgg)) { - params.metricAgg = null; - } params.customMetric = null; return; } diff --git a/src/legacy/ui/public/agg_types/metrics/lib/parent_pipeline_agg_helper.js b/src/legacy/ui/public/agg_types/metrics/lib/parent_pipeline_agg_helper.js index 0d3fb7996bc02..7b2ab167f579b 100644 --- a/src/legacy/ui/public/agg_types/metrics/lib/parent_pipeline_agg_helper.js +++ b/src/legacy/ui/public/agg_types/metrics/lib/parent_pipeline_agg_helper.js @@ -18,6 +18,7 @@ */ import metricAggTemplate from '../../controls/sub_agg.html'; +import { MetricAggParamEditor } from '../../controls/metric_agg'; import _ from 'lodash'; import { AggConfig } from '../../../vis/agg_config'; import { Schemas } from '../../../vis/editors/default/schemas'; @@ -46,8 +47,15 @@ const parentPipelineAggHelper = { }), params: function () { return [ + { + name: 'metricAgg', + editorComponent: MetricAggParamEditor, + default: 'custom', + write: parentPipelineAggWriter + }, { name: 'customMetric', + editor: metricAggTemplate, type: AggConfig, default: null, serialize: function (customMetric) { @@ -64,18 +72,12 @@ const parentPipelineAggHelper = { return metricAgg; }, modifyAggConfigOnSearchRequestStart: forwardModifyAggConfigOnSearchRequestStart('customMetric'), - write: _.noop + write: _.noop, + controller: parentPipelineAggController }, { name: 'buckets_path', write: _.noop - }, - { - name: 'metricAgg', - editor: metricAggTemplate, - default: 'custom', - controller: parentPipelineAggController, - write: parentPipelineAggWriter } ]; }, diff --git a/src/legacy/ui/public/vis/editors/default/agg_params.html b/src/legacy/ui/public/vis/editors/default/agg_params.html index 411357dbfa78e..7c4ae575fe086 100644 --- a/src/legacy/ui/public/vis/editors/default/agg_params.html +++ b/src/legacy/ui/public/vis/editors/default/agg_params.html @@ -13,12 +13,6 @@ style="display: none;">
-
-

- {{agg.error}} -

-
-

{{ agg.schema.deprecateMessage }} diff --git a/src/legacy/ui/public/vis/editors/default/agg_select.js b/src/legacy/ui/public/vis/editors/default/agg_select.js index 0a1864e141e05..7461eec17d795 100644 --- a/src/legacy/ui/public/vis/editors/default/agg_select.js +++ b/src/legacy/ui/public/vis/editors/default/agg_select.js @@ -31,7 +31,7 @@ uiModules ['setValidity', { watchDepth: 'reference' }], ['setValue', { watchDepth: 'reference' }], 'aggHelpLink', - 'isSelectInvalid', + 'showValidation', 'isSubAggregation', 'value', ])) @@ -46,7 +46,7 @@ uiModules agg="agg" agg-help-link="aggHelpLink" agg-type-options="aggTypeOptions" - is-select-invalid="isSelectInvalid" + show-validation="showValidation" is-sub-aggregation="isSubAggregation" value="paramValue" set-validity="setValidity" @@ -61,51 +61,40 @@ uiModules $scope.$bind('isSubAggregation', attr.isSubAggregation); }, post: function ($scope, $el, attr, ngModelCtrl) { - let _isSelectInvalid = false; + $scope.showValidation = false; $scope.$watch('agg.type', (value) => { // Whenever the value of the parameter changed (e.g. by a reset or actually by calling) // we store the new value in $scope.paramValue, which will be passed as a new value to the react component. $scope.paramValue = value; - - $scope.setValidity(true); - $scope.isSelectInvalid = false; }); $scope.$watch(() => { // The model can become touched either onBlur event or when the form is submitted. return ngModelCtrl.$touched; }, (value) => { - if (value === true) { - showValidation(); + if (value) { + $scope.showValidation = true; } }, true); $scope.onChange = (value) => { - if (!value) { - // We prevent to make the field empty. - return; - } + $scope.paramValue = 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.onAggTypeChange($scope.agg, value); - + $scope.showValidation = true; ngModelCtrl.$setDirty(); }; $scope.setTouched = () => { ngModelCtrl.$setTouched(); - showValidation(); + $scope.showValidation = true; }; $scope.setValidity = (isValid) => { - _isSelectInvalid = !isValid; ngModelCtrl.$setValidity(`agg${$scope.agg.id}`, isValid); }; - - function showValidation() { - $scope.isSelectInvalid = _isSelectInvalid; - } } } }; diff --git a/src/legacy/ui/public/vis/editors/default/components/default_editor_agg_select.tsx b/src/legacy/ui/public/vis/editors/default/components/default_editor_agg_select.tsx index 1f32f6d52c5b3..c1bcc9387d07a 100644 --- a/src/legacy/ui/public/vis/editors/default/components/default_editor_agg_select.tsx +++ b/src/legacy/ui/public/vis/editors/default/components/default_editor_agg_select.tsx @@ -19,7 +19,7 @@ import { get, has } from 'lodash'; import React, { useEffect } from 'react'; -import { EuiComboBox, EuiFormRow, EuiLink } from '@elastic/eui'; +import { EuiComboBox, EuiComboBoxOptionProps, EuiFormRow, EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { AggType } from 'ui/agg_types'; @@ -30,7 +30,7 @@ import { ComboBoxGroupedOption } from '../default_editor_utils'; interface DefaultEditorAggSelectProps { agg: AggConfig; aggTypeOptions: AggType[]; - isSelectInvalid: boolean; + showValidation: boolean; isSubAggregation: boolean; value: AggType; setValidity: (isValid: boolean) => void; @@ -43,7 +43,7 @@ function DefaultEditorAggSelect({ value, setValue, aggTypeOptions, - isSelectInvalid, + showValidation, isSubAggregation, setTouched, setValidity, @@ -93,23 +93,43 @@ function DefaultEditorAggSelect({ }, }) ); - setTouched(); } + if (agg.error) { + errors.push(agg.error); + } + + const isValid = !!value && !errors.length && !agg.error; + useEffect( () => { - // The selector will be invalid when the value is empty. - setValidity(!!value); + setValidity(isValid); }, - [value] + [isValid] ); + useEffect( + () => { + if (errors.length) { + setTouched(); + } + }, + [errors.length] + ); + + const onChange = (options: EuiComboBoxOptionProps[]) => { + const selectedOption = get(options, '0.value'); + if (selectedOption) { + setValue(selectedOption); + } + }; + return ( @@ -123,10 +143,10 @@ function DefaultEditorAggSelect({ selectedOptions={selectedOptions} singleSelection={{ asPlainText: true }} onBlur={setTouched} - onChange={options => setValue(get(options, '0.value'))} + onChange={onChange} data-test-subj="defaultEditorAggSelect" isClearable={false} - isInvalid={isSelectInvalid} + isInvalid={showValidation ? !isValid : false} fullWidth={true} /> diff --git a/test/functional/page_objects/visualize_page.js b/test/functional/page_objects/visualize_page.js index ca10c144d6e4a..9b6e59dcdc212 100644 --- a/test/functional/page_objects/visualize_page.js +++ b/test/functional/page_objects/visualize_page.js @@ -1226,7 +1226,7 @@ export function VisualizePageProvider({ getService, getPageObjects, updateBaseli } async getBucketErrorMessage() { - const error = await find.byCssSelector('.visEditorAggParam__error'); + const error = await find.byCssSelector('[group-name="buckets"] [data-test-subj="defaultEditorAggSelect"] + .euiFormErrorText'); const errorMessage = await error.getProperty('innerText'); log.debug(errorMessage); return errorMessage; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 16f8c7befd47b..f0a349e22e43d 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -107,7 +107,6 @@ "common.ui.aggTypes.buckets.significantTerms.includeLabel": "含める", "common.ui.aggTypes.buckets.significantTermsLabel": "{fieldName} のトップ {size} の珍しいアイテム", "common.ui.aggTypes.buckets.significantTermsTitle": "Significant Terms", - "common.ui.aggTypes.buckets.terms.aggNotValidLabel": "- 無効な集約 -", "common.ui.aggTypes.buckets.terms.excludeLabel": "除外", "common.ui.aggTypes.buckets.terms.includeLabel": "含める", "common.ui.aggTypes.buckets.terms.missingBucketLabel": "欠測値", @@ -210,7 +209,6 @@ "common.ui.aggTypes.metrics.topHitTitle": "トップヒット", "common.ui.aggTypes.metrics.uniqueCountLabel": "{field} のユニークカウント", "common.ui.aggTypes.metrics.uniqueCountTitle": "ユニークカウント", - "common.ui.aggTypes.metrics.wrongLastBucketTypeErrorMessage": "「{type}」メトリック集約を使用する場合、最後のバケットアグリゲーションは「Date Histogram」または「Histogram」でなければなりません!", "common.ui.aggTypes.numberInterval.minimumIntervalLabel": "最低間隔", "common.ui.aggTypes.numberInterval.minimumIntervalTooltip": "入力された値により高度な設定の {histogramMaxBars} で指定されたよりも多くのバケットが作成される場合、間隔は自動的にスケーリングされます。", "common.ui.aggTypes.numberInterval.selectIntervalPlaceholder": "間隔を入力", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 7a922f39fe0fa..43180fcc11496 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -209,7 +209,6 @@ "common.ui.aggTypes.metrics.topHitTitle": "最高命中结果", "common.ui.aggTypes.metrics.uniqueCountLabel": "“{field}” 的唯一计数", "common.ui.aggTypes.metrics.uniqueCountTitle": "唯一计数", - "common.ui.aggTypes.metrics.wrongLastBucketTypeErrorMessage": "使用 “{type}” 指标聚合时,上一存储桶聚合必须是“Date Histogram”或“Histogram”!", "common.ui.aggTypes.numberInterval.minimumIntervalLabel": "最小时间间隔", "common.ui.aggTypes.numberInterval.minimumIntervalTooltip": "提供的值创建的存储桶数目大于“高级设置”的 {histogramMaxBars} 指定的数目时,将自动缩放时间间隔", "common.ui.aggTypes.numberInterval.selectIntervalPlaceholder": "输入时间间隔",