[Vis: Default editor] EUIficate field selection control and apply form validation (#34548)

* EUIficate field selection control

* Refactoring

* Remove unused commaList filter

* Remove field.html, update functional tests

* Update functional tests

* Remove unused translation

* Update unit test since wrapped React component haven't compiled yet

* Move setValidity invocation to the component

* Update functional test

* Fix type export

* Wrap setValidity into useEffect due to react warning on init load with empty value

* Update types

* Remove extra tag

* Removed changed translations

* Update functional test

* Add help and error message

* Update error message

* Remove unused dependency

* Remove helpText

* Remove unused dependencies

* Remove unused translation

* Refactoring

* Refactoring

* Update form validation; remove setTouched

* Update from validation

* Update form validation

* Update agg select validation

* Refactoring

* Add ariaLabel

* Revert changes

* Update comments

* Remove unnecessary aria-label

* Disable selector with no options

* Add 'required' support for string control

* Update messages

* Fix merge conflict

* Update message

* Fix eslint
This commit is contained in:
Maryia Lapata 2019-04-11 18:13:46 +03:00 committed by GitHub
parent c1ce349275
commit 870552dd78
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 302 additions and 218 deletions

View file

@ -108,7 +108,7 @@ describe('editor', function () {
expect(params).to.have.property('field');
expect(params.field).to.have.property('$el');
expect(params.field.modelValue()).to.be(field);
expect($scope.agg.params.field).to.be(field);
});
it('renders the interval editor', function () {

View file

@ -17,12 +17,14 @@
* under the License.
*/
import { AggConfig } from '../vis/agg_config';
import { AggConfig } from '../vis';
interface AggParam {
type: string;
name: string;
required?: boolean;
displayName?: string;
onChange?(agg: AggConfig): void;
disabled?(agg: AggConfig): boolean;
}

View file

@ -1,52 +0,0 @@
<div class="form-group">
<label
i18n-id="common.ui.aggTypes.field.fieldLabel"
i18n-default-message="Field"
></label>
<ui-select
title="{{ ::'common.ui.aggTypes.field.aggregationFieldTitle' | i18n: { defaultMessage: 'Aggregation Field' } }}"
name="field"
required
class="visEditorAggSelect__select field-select"
ng-show="indexedFields.length"
ng-model="agg.params.field"
on-select="aggParam.onChange(agg)"
uis-open-close="limit = 100"
>
<ui-select-match
placeholder="{{ ::'common.ui.aggTypes.field.selectFieldPlaceholder' | i18n: { defaultMessage: 'Select a field' } }}"
>
{{$select.selected.displayName}}
</ui-select-match>
<ui-select-choices
group-by="'type'"
kbn-scroll-bottom="limit = limit + 100"
repeat="field in indexedFields | filter: { displayName: $select.search } | sortPrefixFirst:$select.search:'name' | limitTo: limit"
>
<div
data-test-subj="{{field.displayName}}"
class="eui-textTruncate"
ng-bind-html="field.displayName | highlight: $select.search"
title="{{field.displayName}}"
></div>
</ui-select-choices>
</ui-select>
<div class="hintbox" ng-if="!indexedFields.length">
<p>
<i class="fa fa-danger text-danger"></i>
<strong
i18n-id="common.ui.aggTypes.dateRanges.noCompatibleFieldsLabel"
i18n-default-message="No Compatible Fields:"
></strong>
<span
i18n-id="common.ui.aggTypes.dateRanges.noCompatibleFieldsDescription"
i18n-default-message="The {indexPatternTitle} index pattern does not contain any of the following field types:"
i18n-values="{ indexPatternTitle: agg.getIndexPattern().title }"
></span>
{{ agg.type.params.byName.field.filterFieldTypes | commaList:false }}
</p>
</div>
</div>

View file

@ -0,0 +1,114 @@
/*
* 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 } from 'lodash';
import React, { useEffect } from 'react';
import { EuiComboBox, EuiComboBoxOptionProps, EuiFormRow } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { AggConfig } from 'ui/vis';
import { formatListAsProse, parseCommaSeparatedList } from '../../../../utils';
import { AggParamEditorProps } from '../../vis/editors/default';
import { ComboBoxGroupedOption } from '../../vis/editors/default/default_editor_utils';
import { FieldParamType } from '../param_types';
const label = i18n.translate('common.ui.aggTypes.field.fieldLabel', { defaultMessage: 'Field' });
function FieldParamEditor({
agg,
aggParam,
indexedFields = [],
isInvalid,
value,
setTouched,
setValidity,
setValue,
}: AggParamEditorProps<FieldParamType>) {
const selectedOptions: ComboBoxGroupedOption[] = value
? [{ label: value.displayName, value }]
: [];
const onChange = (options: EuiComboBoxOptionProps[]) => {
const selectedOption = get(options, '0.value');
if (!(aggParam.required && !selectedOption)) {
setValue(selectedOption);
}
if (aggParam.onChange) {
aggParam.onChange(agg);
}
};
const errors = [];
if (!indexedFields.length) {
errors.push(
i18n.translate('common.ui.aggTypes.field.noCompatibleFieldsDescription', {
defaultMessage:
'The index pattern {indexPatternTitle} does not contain any of the following compatible field types: {fieldTypes}',
values: {
indexPatternTitle: agg.getIndexPattern && agg.getIndexPattern().title,
fieldTypes: getFieldTypesString(agg),
},
})
);
setTouched();
}
useEffect(
() => {
setValidity(!!value);
},
[value]
);
return (
<EuiFormRow
label={label}
isInvalid={isInvalid}
fullWidth={true}
error={errors}
className="visEditorSidebar__aggParamFormRow"
>
<EuiComboBox
placeholder={i18n.translate('common.ui.aggTypes.field.selectFieldPlaceholder', {
defaultMessage: 'Select a field',
})}
options={indexedFields}
isDisabled={!indexedFields.length}
selectedOptions={selectedOptions}
singleSelection={{ asPlainText: true }}
isClearable={false}
isInvalid={isInvalid}
onChange={onChange}
onBlur={setTouched}
data-test-subj="visDefaultEditorField"
fullWidth={true}
/>
</EuiFormRow>
);
}
function getFieldTypesString(agg: AggConfig) {
return formatListAsProse(
parseCommaSeparatedList(get(agg, 'type.params.byName.field.filterFieldTypes')),
{ inclusive: false }
);
}
export { FieldParamEditor };

View file

@ -27,10 +27,11 @@ import { isValidJson } from '../utils';
function RawJsonParamEditor({
agg,
value,
setValue,
isInvalid,
value,
setValidity,
setValue,
setTouched,
}: AggParamEditorProps<string>) {
const label = (
<>
@ -68,6 +69,7 @@ function RawJsonParamEditor({
onChange={onChange}
rows={2}
fullWidth={true}
onBlur={setTouched}
/>
</EuiFormRow>
);

View file

@ -17,29 +17,49 @@
* under the License.
*/
import React from 'react';
import React, { useEffect } from 'react';
import { EuiFieldText, EuiFormRow } from '@elastic/eui';
import { AggParamEditorProps } from '../../vis/editors/default';
function StringParamEditor({ agg, aggParam, value, setValue }: AggParamEditorProps<string>) {
function StringParamEditor({
agg,
aggParam,
isInvalid,
value,
setValidity,
setValue,
setTouched,
}: AggParamEditorProps<string>) {
if (aggParam.disabled && aggParam.disabled(agg)) {
// reset model value
setValue();
return null;
}
useEffect(
() => {
if (aggParam.required) {
setValidity(!!value);
}
},
[value]
);
return (
<EuiFormRow
label={aggParam.displayName || aggParam.name}
fullWidth={true}
className="visEditorSidebar__aggParamFormRow"
isInvalid={isInvalid}
>
<EuiFieldText
value={value || ''}
data-test-subj={`visEditorStringInput${agg.id}${aggParam.name}`}
onChange={ev => setValue(ev.target.value)}
fullWidth={true}
onBlur={setTouched}
isInvalid={isInvalid}
/>
</EuiFormRow>
);

View file

@ -1,50 +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 expect from '@kbn/expect';
import ngMock from 'ng_mock';
import '../comma_list';
describe('Comma-List filter', function () {
let commaList;
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function ($injector) {
commaList = $injector.get('commaListFilter');
}));
it('converts a string to a pretty list', function () {
expect(commaList('john,jaine,jim', true)).to.be('john, jaine, and jim');
expect(commaList('john,jaine,jim', false)).to.be('john, jaine, or jim');
});
it('can accept an array too', function () {
expect(commaList(['john', 'jaine', 'jim'])).to.be('john, jaine, or jim');
});
it('handles undefined ok', function () {
expect(commaList()).to.be('');
});
it('handles single values ok', function () {
expect(commaList(['john'])).to.be('john');
});
});

View file

@ -19,9 +19,8 @@
import { sortBy } from 'lodash';
import { SavedObjectNotFound } from '../../errors';
import { FieldParamEditor } from '../controls/field';
import '../directives/scroll_bottom';
import '../filter/comma_list';
import editorHtml from '../controls/field.html';
import { BaseParamType } from './base';
import '../filters/sort_prefix_first';
import '../../filters/field_type';
@ -39,7 +38,8 @@ export function FieldParamType(config) {
createLegacyClass(FieldParamType).inherits(BaseParamType);
FieldParamType.prototype.editor = editorHtml;
FieldParamType.prototype.editorComponent = FieldParamEditor;
FieldParamType.prototype.required = true;
FieldParamType.prototype.scriptable = true;
FieldParamType.prototype.filterFieldTypes = '*';
// retain only the fields with the aggregatable property if the onlyAggregatable option is true

View file

@ -24,6 +24,10 @@ function isValidJson(value: string): boolean {
const trimmedValue = value.trim();
if (trimmedValue.length === 0) {
return true;
}
if (trimmedValue[0] === '{' || trimmedValue[0] === '[') {
try {
JSON.parse(trimmedValue);

View file

@ -23,12 +23,12 @@
</button>
<!-- description -->
<span ng-if="!editorOpen && aggForm.$valid" class="visEditorSidebar__collapsibleTitleDescription" title="{{describe()}}">
<span ng-if="!editorOpen && aggForm.softErrorCount() < 1" class="visEditorSidebar__collapsibleTitleDescription" title="{{describe()}}">
{{ describe() }}
</span>
<!-- error -->
<span ng-if="!editorOpen && aggForm.$invalid" class="visEditorSidebar__collapsibleTitleDescription visEditorSidebar__collapsibleTitleDescription--danger" title="{{aggForm.describeErrors()}}">
<span ng-if="!editorOpen && aggForm.softErrorCount() > 0" class="visEditorSidebar__collapsibleTitleDescription visEditorSidebar__collapsibleTitleDescription--danger" title="{{aggForm.describeErrors()}}">
{{ aggForm.describeErrors() }}
</span>

View file

@ -27,12 +27,14 @@ uiModules
.directive('visAggParamReactWrapper', reactDirective => reactDirective(wrapInI18nContext(AggParamReactWrapper), [
['agg', { watchDepth: 'collection' }],
['aggParam', { watchDepth: 'reference' }],
['indexedFields', { watchDepth: 'collection' }],
['paramEditor', { wrapApply: false }],
['onChange', { watchDepth: 'reference' }],
['setTouched', { watchDepth: 'reference' }],
['setValidity', { watchDepth: 'reference' }],
'value',
'field',
'isInvalid',
'field'
'value',
]))
.directive('visAggParamEditor', function (config) {
return {
@ -55,11 +57,13 @@ uiModules
param-editor="editorComponent"
agg="agg"
agg-param="aggParam"
on-change="onChange"
value="paramValue"
is-invalid="isInvalid"
set-validity="setValidity"
field="agg.params.field"
indexed-fields="indexedFields"
is-invalid="isInvalid"
value="paramValue"
on-change="onChange"
set-touched="setTouched"
set-validity="setValidity"
></vis-agg-param-react-wrapper>`;
}
@ -70,8 +74,10 @@ uiModules
$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) {
let _isInvalid = false;
$scope.config = config;
$scope.optionEnabled = function (option) {
@ -87,6 +93,18 @@ uiModules
// 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);
showValidation();
}, true);
$scope.$watch(() => {
// The model can become touched either onBlur event or when the form is submitted.
return ngModelCtrl.$touched;
}, (value) => {
if (value) {
showValidation();
}
}, true);
$scope.paramValue = $scope.agg.params[$scope.aggParam.name];
}
@ -96,17 +114,22 @@ uiModules
// 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);
if(ngModelCtrl) {
ngModelCtrl.$setDirty();
}
ngModelCtrl.$setDirty();
};
$scope.setTouched = () => {
ngModelCtrl.$setTouched();
showValidation();
};
$scope.setValidity = (isValid) => {
if(ngModelCtrl) {
$scope.isInvalid = !isValid;
ngModelCtrl.$setValidity(`agg${$scope.agg.id}${$scope.aggParam.name}`, isValid);
}
_isInvalid = !isValid;
ngModelCtrl.$setValidity(`agg${$scope.agg.id}${$scope.aggParam.name}`, isValid);
};
function showValidation() {
$scope.isInvalid = _isInvalid;
}
}
}
};

View file

@ -27,8 +27,10 @@ import { AggConfig } from '../../agg_config';
export interface AggParamEditorProps<T> {
agg: AggConfig;
aggParam: AggParam;
value: T;
indexedFields?: any[];
isInvalid: boolean;
setValue(value?: T): void;
value: T;
setValidity(isValid: boolean): void;
setValue(value?: T): void;
setTouched(): void;
}

View file

@ -20,16 +20,19 @@
import React from 'react';
import { AggParam } from '../../../agg_types';
import { FieldParamType } from '../../../agg_types/param_types';
import { AggConfig } from '../../agg_config';
import { AggParamEditorProps } from './agg_param_editor_props';
interface AggParamReactWrapperProps<T> {
agg: AggConfig;
aggParam: AggParam;
indexedFields: FieldParamType[];
isInvalid: boolean;
paramEditor: React.FunctionComponent<AggParamEditorProps<T>>;
value: T;
isInvalid: boolean;
onChange(value: T): void;
setTouched(): void;
setValidity(isValid: boolean): void;
}
@ -37,20 +40,24 @@ function AggParamReactWrapper<T>(props: AggParamReactWrapperProps<T>) {
const {
agg,
aggParam,
paramEditor: ParamEditor,
onChange,
value,
indexedFields,
isInvalid,
paramEditor: ParamEditor,
value,
onChange,
setValidity,
setTouched,
} = props;
return (
<ParamEditor
value={value}
setValue={onChange}
aggParam={aggParam}
agg={agg}
aggParam={aggParam}
indexedFields={indexedFields}
isInvalid={isInvalid}
value={value}
setTouched={setTouched}
setValidity={setValidity}
setValue={onChange}
/>
);
}

View file

@ -150,8 +150,9 @@ uiModules
// if field param exists, compute allowed fields
if (param.type === 'field') {
const availableFields = param.getAvailableFields($scope.agg.getIndexPattern().fields);
fields = $scope.indexedFields = $aggParamEditorsScope[`${param.name}Options`] =
fields = $aggParamEditorsScope[`${param.name}Options`] =
aggTypeFieldFilters.filter(availableFields, param.type, $scope.agg, $scope.vis);
$scope.indexedFields = groupAggregationsBy(fields, 'type', 'displayName');
}
if (fields) {
@ -202,6 +203,7 @@ uiModules
if (param.editorComponent) {
attrs['editor-component'] = `agg.type.params[${idx}].editorComponent`;
attrs['indexed-fields'] = 'indexedFields';
// The form should interact with reactified components as well.
// So we set the ng-model (using a random ng-model variable) to have the method to set dirty
// inside the agg_param.js directive, which can get access to the ngModelController to manipulate it.

View file

@ -27,13 +27,13 @@ uiModules
.directive('visAggSelectReactWrapper', reactDirective => reactDirective(wrapInI18nContext(DefaultEditorAggSelect), [
['agg', { watchDepth: 'collection' }],
['aggTypeOptions', { watchDepth: 'collection' }],
['setValue', { watchDepth: 'reference' }],
['setTouched', { watchDepth: 'reference' }],
['setValidity', { watchDepth: 'reference' }],
'value',
'isSubAggregation',
['setValue', { watchDepth: 'reference' }],
'aggHelpLink',
'isSelectInvalid'
'isSelectInvalid',
'isSubAggregation',
'value',
]))
.directive('visAggSelect', function () {
return {
@ -42,31 +42,50 @@ uiModules
require: '^ngModel',
template: function () {
return `<vis-agg-select-react-wrapper
ng-if="setValidity"
agg="agg"
value="paramValue"
set-value="onChange"
is-sub-aggregation="isSubAggregation"
agg-help-link="aggHelpLink"
agg-type-options="aggTypeOptions"
is-select-invalid="isSelectInvalid"
set-touched="setTouched"
is-sub-aggregation="isSubAggregation"
value="paramValue"
set-validity="setValidity"
set-value="onChange"
set-touched="setTouched"
></vis-agg-select-react-wrapper>`;
},
link: {
pre: function ($scope, $el, attr) {
$scope.$bind('agg', attr.agg);
$scope.$bind('isSubAggregation', attr.isSubAggregation);
$scope.$bind('aggTypeOptions', attr.aggTypeOptions);
$scope.$bind('isSubAggregation', attr.isSubAggregation);
},
post: function ($scope, $el, attr, ngModelCtrl) {
let _isSelectInvalid = 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();
}
}, true);
$scope.onChange = (value) => {
if (!value) {
// We prevent to make the field empty.
return;
}
// 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);
@ -76,15 +95,17 @@ uiModules
$scope.setTouched = () => {
ngModelCtrl.$setTouched();
$scope.isSelectInvalid = !$scope.paramValue;
showValidation();
};
$scope.setValidity = (isValid) => {
// The field will be marked as invalid when the value is empty and the field is touched.
$scope.isSelectInvalid = ngModelCtrl.$touched ? !isValid : false;
// Since aggType is required field, the form should become invalid when the aggregation field is set to empty.
_isSelectInvalid = !isValid;
ngModelCtrl.$setValidity(`agg${$scope.agg.id}`, isValid);
};
function showValidation() {
$scope.isSelectInvalid = _isSelectInvalid;
}
}
}
};

View file

@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { get, has, isFunction } from 'lodash';
import { get, has } from 'lodash';
import React, { useEffect } from 'react';
import { EuiComboBox, EuiFormRow, EuiLink } from '@elastic/eui';
@ -29,29 +29,26 @@ import { ComboBoxGroupedOption } from '../default_editor_utils';
interface DefaultEditorAggSelectProps {
agg: AggConfig;
value: AggType;
setValue: (aggType: AggType) => void;
aggTypeOptions: AggType[];
isSubAggregation: boolean;
isSelectInvalid: boolean;
setTouched: () => void;
isSubAggregation: boolean;
value: AggType;
setValidity: (isValid: boolean) => void;
setValue: (aggType: AggType) => void;
setTouched: () => void;
}
function DefaultEditorAggSelect({
agg = {},
value = { title: '' },
agg,
value,
setValue,
aggTypeOptions = [],
aggTypeOptions,
isSelectInvalid,
isSubAggregation,
setTouched,
setValidity,
}: DefaultEditorAggSelectProps) {
const isAggTypeDefined = value && Boolean(value.title);
const selectedOptions: ComboBoxGroupedOption[] = isAggTypeDefined
? [{ label: value.title, value }]
: [];
const selectedOptions: ComboBoxGroupedOption[] = value ? [{ label: value.title, value }] : [];
const label = isSubAggregation ? (
<FormattedMessage
@ -70,7 +67,7 @@ function DefaultEditorAggSelect({
aggHelpLink = get(documentationLinks, ['aggs', agg.type.name]);
}
const helpLink = isAggTypeDefined && aggHelpLink && (
const helpLink = value && aggHelpLink && (
<EuiLink
href={aggHelpLink}
target="_blank"
@ -80,42 +77,57 @@ function DefaultEditorAggSelect({
<FormattedMessage
id="common.ui.vis.defaultEditor.aggSelect.helpLinkLabel"
defaultMessage="{aggTitle} help"
values={{ aggTitle: isAggTypeDefined ? value.title : '' }}
values={{ aggTitle: value ? value.title : '' }}
/>
</EuiLink>
);
const errors = [];
if (!aggTypeOptions.length) {
errors.push(
i18n.translate('common.ui.vis.defaultEditor.aggSelect.noCompatibleAggsDescription', {
defaultMessage: 'The index pattern {indexPatternTitle} does not contain any aggregations.',
values: {
indexPatternTitle: agg.getIndexPattern && agg.getIndexPattern().title,
},
})
);
setTouched();
}
useEffect(
() => {
if (isFunction(setValidity)) {
setValidity(isAggTypeDefined);
}
// The selector will be invalid when the value is empty.
setValidity(!!value);
},
[isAggTypeDefined]
[value]
);
return (
<EuiFormRow
label={label}
labelAppend={helpLink}
error={errors}
isInvalid={isSelectInvalid}
fullWidth={true}
className="visEditorAggSelect__formRow"
>
<EuiComboBox
placeholder={i18n.translate('common.ui.vis.defaultEditor.aggSelect.selectAggPlaceholder', {
defaultMessage: 'Select an aggregation',
defaultMessage: 'Select an aggregation',
})}
id={`visDefaultEditorAggSelect${agg.id}`}
isDisabled={!aggTypeOptions.length}
options={aggTypeOptions}
selectedOptions={selectedOptions}
singleSelection={{ asPlainText: true }}
onBlur={setTouched}
onChange={options => setValue(get(options, '0.value'))}
data-test-subj="defaultEditorAggSelect"
isClearable={false}
isInvalid={isSelectInvalid}
fullWidth={true}
onBlur={() => setTouched()}
/>
</EuiFormRow>
);

View file

@ -20,7 +20,12 @@
import { EuiComboBoxOptionProps } from '@elastic/eui';
import { AggType } from 'ui/agg_types';
// NOTE: we cannot export the interface with export { InterfaceName }
// as there is currently a bug on babel typescript transform plugin for it
// https://github.com/babel/babel/issues/7641
//
export type ComboBoxGroupedOption = EuiComboBoxOptionProps & {
label: string;
value?: AggType;
options?: ComboBoxGroupedOption[];
};
@ -30,12 +35,14 @@ export type ComboBoxGroupedOption = EuiComboBoxOptionProps & {
*
* @param aggs An array of aggregations that will be grouped.
* @param groupBy A field name which aggregations is grouped by.
* @param labelName A name of a property which value will be displayed.
*
* @returns An array of grouped and sorted alphabetically `aggs` that are compatible with EuiComboBox options. If `aggs` is not an array, the function returns an ampry array.
*/
function groupAggregationsBy(
aggs: AggType[],
groupBy: string = 'type'
groupBy: string = 'type',
labelName = 'title'
): ComboBoxGroupedOption[] | [] {
if (!Array.isArray(aggs)) {
return [];
@ -44,7 +51,7 @@ function groupAggregationsBy(
const groupedOptions: ComboBoxGroupedOption[] = aggs.reduce((array: AggType[], type: AggType) => {
const group = array.find(element => element.label === type[groupBy]);
const option = {
label: type.title,
label: type[labelName],
value: type,
};
@ -72,7 +79,7 @@ function groupAggregationsBy(
return groupedOptions;
}
function sortByLabel(a: { label: string }, b: { label: string }) {
function sortByLabel(a: ComboBoxGroupedOption, b: ComboBoxGroupedOption) {
return a.label.toLowerCase().localeCompare(b.label.toLowerCase());
}

View file

@ -1,7 +1,7 @@
<div class="visEditorSidebar__container">
<form
class="visEditorSidebar__form"
ng-submit="visualizeEditor.$invalid ? stageEditableVis(false) : stageEditableVis()"
ng-submit="visualizeEditor.$valid && stageEditableVis()"
name="visualizeEditor"
novalidate
ng-keydown="submitEditorWithKeyboard($event)"
@ -69,7 +69,7 @@
<!-- controls -->
<ul class="nav navbar-nav navbar-right">
<li
ng-if="visualizeEditor.softErrorCount() > 0"
ng-if="visualizeEditor.errorCount() > 0 && visualizeEditor.errorCount() === visualizeEditor.softErrorCount()"
disabled
tooltip="{{ visualizeEditor.describeErrors() }}"
tooltip-placement="bottom"
@ -115,13 +115,13 @@
tooltip="{{::'common.ui.vis.editors.sidebar.applyChangesTooltip' | i18n: { defaultMessage: 'Apply changes' } }}"
tooltip-placement="bottom"
tooltip-popup-delay="400" tooltip-append-to-body="1"
ng-hide="autoApplyEnabled || visualizeEditor.softErrorCount() > 0"
ng-hide="autoApplyEnabled || (visualizeEditor.errorCount() > 0 && visualizeEditor.errorCount() === visualizeEditor.softErrorCount())"
>
<button
data-test-subj="visualizeEditorRenderButton"
class="kuiButton kuiButton--primary navbar-btn-link visEditorSidebar__navButtonLink"
type="submit"
ng-disabled="!vis.dirty || visualizeEditor.errorCount() > 0 || autoApplyEnabled"
ng-disabled="!vis.dirty || autoApplyEnabled"
aria-label="{{::'common.ui.vis.editors.sidebar.applyChangesAriaLabel' | i18n: { defaultMessage: 'Update the visualization with your changes' } }}"
>
<icon aria-hidden="true" type="'play'"></icon>

View file

@ -17,25 +17,6 @@
* under the License.
*/
import { uiModules } from 'ui/modules';
export function parseCommaSeparatedList(input: string | string[]): string[];
import {
parseCommaSeparatedList,
formatListAsProse,
} from '../../../../utils';
uiModules
.get('kibana')
.filter('commaList', function () {
/**
* Angular filter that accepts either an array or a comma-separated string
* and outputs a comma-separated string for presentation.
*
* @param {String|Array} input - The comma-separated list or array
* @param {Boolean} inclusive - Should the list be joined with an "and"?
* @return {String}
*/
return function (input, inclusive = false) {
return formatListAsProse(parseCommaSeparatedList(input), { inclusive });
};
});
export function formatListAsProse(list: string[], options?: { inclusive?: boolean }): string;

View file

@ -45,9 +45,9 @@ export default function ({ getService, getPageObjects }) {
log.debug('Click Date Histogram');
await PageObjects.visualize.selectAggregation('Date Histogram');
log.debug('Check field value');
const fieldValue = await PageObjects.visualize.getField();
log.debug('fieldValue = ' + fieldValue);
expect(fieldValue).to.be('@timestamp');
const fieldValues = await PageObjects.visualize.getField();
log.debug('fieldValue = ' + fieldValues);
expect(fieldValues[0]).to.be('@timestamp');
const intervalValue = await PageObjects.visualize.getInterval();
log.debug('intervalValue = ' + intervalValue);
expect(intervalValue).to.be('Auto');

View file

@ -252,7 +252,7 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }: FtrPro
return await PageObjects.header.waitUntilLoadingHasFinished();
}
public async fillInVariable(name = 'test', metric = 'count', nth = 0) {
public async fillInVariable(name = 'test', metric = 'Count', nth = 0) {
const elements = await testSubjects.findAll('varRow');
const varNameInput = await elements[nth].findByCssSelector('.tvbAggs__varName');
await varNameInput.type(name);

View file

@ -488,12 +488,9 @@ export function VisualizePageProvider({ getService, getPageObjects, updateBaseli
.byCssSelector(`#visAggEditorParams${index} [data-test-subj="defaultEditorAggSelect"]`);
await comboBox.setElement(aggSelect, agg);
const fieldSelect = await find
.byCssSelector(`#visAggEditorParams${index} > [agg-param="agg.type.params[0]"] > div > div > div.ui-select-match > span`);
// open field selection list
await fieldSelect.click();
const fieldSelect = await find.byCssSelector(`#visAggEditorParams${index} [data-test-subj="visDefaultEditorField"]`);
// select our field
await testSubjects.click(field);
await comboBox.setElement(fieldSelect, field);
// enter custom label
await this.setCustomLabel(label, index);
}
@ -535,9 +532,7 @@ export function VisualizePageProvider({ getService, getPageObjects, updateBaseli
}
async getField() {
const field = await retry.try(
async () => await find.byCssSelector('.ng-valid-required[name="field"] .ui-select-match-text'));
return await field.getVisibleText();
return await comboBox.getComboBoxSelectedOptions('visDefaultEditorField');
}
async selectField(fieldValue, groupName = 'buckets', childAggregationType = null) {
@ -546,12 +541,10 @@ export function VisualizePageProvider({ getService, getPageObjects, updateBaseli
[group-name="${groupName}"]
vis-editor-agg-params:not(.ng-hide)
${childAggregationType ? `vis-editor-agg-params[group-name="'${childAggregationType}'"]:not(.ng-hide)` : ''}
.field-select
[data-test-subj="visDefaultEditorField"]
`;
await find.clickByCssSelector(selector);
await find.setValue(`${selector} input.ui-select-search`, fieldValue);
const input = await find.byCssSelector(`${selector} input.ui-select-search`);
await input.pressKeys(browser.keys.RETURN);
const fieldEl = await find.byCssSelector(selector);
await comboBox.setElement(fieldEl, fieldValue);
}
async selectFieldById(fieldValue, id) {

View file

@ -116,8 +116,6 @@
"common.ui.aggTypes.dateRanges.acceptedDateFormatsLinkText": "已接受日期格式",
"common.ui.aggTypes.dateRanges.addRangeButtonLabel": "添加范围",
"common.ui.aggTypes.dateRanges.fromColumnLabel": "从",
"common.ui.aggTypes.dateRanges.noCompatibleFieldsDescription": "“{indexPatternTitle}” 索引模式不包含任何以下字段类型:",
"common.ui.aggTypes.dateRanges.noCompatibleFieldsLabel": "无兼容字段:",
"common.ui.aggTypes.dateRanges.removeRangeButtonAriaLabel": "移除此范围",
"common.ui.aggTypes.dateRanges.requiredDateRangeDescription": "必须至少指定一个日期范围。",
"common.ui.aggTypes.dateRanges.requiredDateRangeLabel": "必需:",
@ -131,9 +129,7 @@
"common.ui.aggTypes.extendedBounds.minLabel.optionalText": "(可选)",
"common.ui.aggTypes.extendedBoundsLabel": "扩展的边界",
"common.ui.aggTypes.extendedBoundsTooltip": "“最小值”和“最大值”不会筛选结果,而会扩展结果集的边界",
"common.ui.aggTypes.field.aggregationFieldTitle": "聚合字段",
"common.ui.aggTypes.field.fieldLabel": "字段",
"common.ui.aggTypes.field.selectFieldPlaceholder": "选择字段",
"common.ui.aggTypes.filters.addFilterButtonLabel": "添加筛选",
"common.ui.aggTypes.filters.definiteFilterLabel": "筛选 {index} 标签",
"common.ui.aggTypes.filters.filterLabel": "筛选 {index}",