[Vis: Default editor] EUIficate Sub agg control (#37979)

* EUIficate metric agg control

* Fix translation errors

* Display agg error underneath the last bucket agg form control

* Update functional test

* Update error message

* Update parent_pipeline_agg_controller.js

* Fix validation when metricAgg is invalid

* Show error message when a filed is selected

* Delete _terms_helper.tsx

* Remove extra empty line

* Update parent_pipeline_agg_helper.js

* Update selector for test
This commit is contained in:
Maryia Lapata 2019-06-13 11:18:26 +03:00 committed by GitHub
parent f2dd96b50a
commit 386619e577
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 198 additions and 197 deletions

View file

@ -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 };

View file

@ -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';

View file

@ -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<string>) {
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 (
<EuiFormRow label={label} fullWidth={true} isInvalid={showValidation ? !isValid : false}>
<EuiSelect
options={options}
value={value || EMPTY_VALUE}
onChange={ev => setValue(ev.target.value)}
fullWidth={true}
isInvalid={showValidation ? !isValid : false}
onBlur={setTouched}
data-test-subj={`visEditorSubAggMetric${agg.id}`}
/>
</EuiFormRow>
);
}
export { MetricAggParamEditor };

View file

@ -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 };

View file

@ -1,43 +1,8 @@
<div ng-controller="aggParam.controller">
<div class="form-group">
<label
for="visEditorSubAggMetric{{agg.id}}"
i18n-id="common.ui.aggTypes.metricLabel"
i18n-default-message="Metric"
></label>
<select
id="visEditorSubAggMetric{{agg.id}}"
name="metricAgg"
ng-model="agg.params.metricAgg"
agg="agg"
required
validate-agg
class="form-control">
<option
ng-repeat="respAgg in responseValueAggs track by respAgg.id"
value="{{respAgg.id}}"
ng-if="respAgg.type.name !== agg.type.name"
ng-disabled="isDisabledAgg(respAgg)"
ng-selected="agg.params.metricAgg === respAgg.id"
i18n-id="common.ui.aggTypes.definiteMetricLabel"
i18n-default-message="metric: {safeMakeLabel}"
i18n-values="{ safeMakeLabel: safeMakeLabel(respAgg) }"
></option>
<option
value="custom"
ng-selected="agg.params.metricAgg === 'custom'"
i18n-id="common.ui.aggTypes.customMetricLabel"
i18n-default-message="Custom Metric"
></option>
</select>
</div>
<div ng-if="agg.params.metricAgg === 'custom'" class="visEditorAgg__subAgg">
<ng-form name="customMetricForm">
<vis-editor-agg-params
index-pattern="agg.getIndexPattern()"
agg="agg.params.customMetric"
group-name="'metrics'">
</vis-editor-agg-params>
</ng-form>
</div>
<div ng-controller="aggParam.controller" ng-show="agg.params.metricAgg === 'custom'" class="visEditorAgg__subAgg">
<vis-editor-agg-params
index-pattern="agg.getIndexPattern()"
agg="agg.params.customMetric"
ng-if="agg.params.metricAgg === 'custom'"
group-name="'metrics'">
</vis-editor-agg-params>
</div>

View file

@ -28,7 +28,7 @@ function TopFieldParamEditor(props: AggParamEditorProps<FieldParamType>) {
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.',
});

View file

@ -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);
}
};
});

View file

@ -17,7 +17,6 @@
* under the License.
*/
import './directives/validate_agg';
import './agg_params';
import { IndexedArray } from '../indexed_array';
import { countMetricAgg } from './metrics/count';

View file

@ -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;
}

View file

@ -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
}
];
},

View file

@ -13,12 +13,6 @@
style="display: none;">
</div>
<div ng-if="agg.error" class="form-group">
<p class="visEditorAggParam__error ng-binding">
{{agg.error}}
</p>
</div>
<div ng-if="agg.schema.deprecate" class="form-group">
<p ng-show="agg.schema.deprecateMessage" class="visEditorAggParam__error">
{{ agg.schema.deprecateMessage }}

View file

@ -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;
}
}
}
};

View file

@ -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 (
<EuiFormRow
label={label}
labelAppend={helpLink}
error={errors}
isInvalid={isSelectInvalid}
isInvalid={showValidation ? !isValid : false}
fullWidth={true}
className="visEditorAggSelect__formRow"
>
@ -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}
/>
</EuiFormRow>

View file

@ -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;

View file

@ -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": "間隔を入力",

View file

@ -194,7 +194,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.onlyRequestDataAroundMapExtentLabel": "仅请求地图范围的数据",
"common.ui.aggTypes.onlyRequestDataAroundMapExtentTooltip": "应用 geo_bounding_box 筛选聚合以使用领口将主题区域缩小到地图视图框",
"common.ui.aggTypes.orderAgg.alphabeticalLabel": "按字母顺序",