[Vis: Default editor] EUIficate filters control (#35464) (#36536)

EUIfication of filters control for Filters aggregation parameter.
This commit is contained in:
Daniil Suleiman 2019-05-14 12:52:19 +03:00 committed by GitHub
parent bba50a8839
commit 688892e13c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 258 additions and 183 deletions

View file

@ -23,9 +23,7 @@ import angular from 'angular';
import { BucketAggType } from './_bucket_agg_type';
import { createFilterFilters } from './create_filter/filters';
import { decorateQuery, luceneStringToDsl } from '@kbn/es-query';
import '../directives/click_focus';
import '../directives/parse_query';
import filtersTemplate from '../controls/filters.html';
import { FiltersParamEditor } from '../controls/filters';
import { i18n } from '@kbn/i18n';
import chrome from 'ui/chrome';
@ -41,7 +39,7 @@ export const filtersBucketAgg = new BucketAggType({
params: [
{
name: 'filters',
editor: filtersTemplate,
editorComponent: FiltersParamEditor,
default: [ { input: {}, label: '' } ],
write: function (aggConfig, output) {
const inFilters = aggConfig.params.filters;

View file

@ -0,0 +1,136 @@
/*
* 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, { useState } from 'react';
import {
EuiForm,
EuiFlexGroup,
EuiFlexItem,
EuiButtonIcon,
EuiFieldText,
EuiFormRow,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
interface FilterRowProps {
id: string;
arrayIndex: number;
customLabel: string;
value: string;
autoFocus: boolean;
disableRemove: boolean;
dataTestSubj: string;
onChangeValue(id: string, query: string, label: string): void;
onRemoveFilter(id: string): void;
}
function FilterRow({
id,
arrayIndex,
customLabel,
value,
autoFocus,
disableRemove,
dataTestSubj,
onChangeValue,
onRemoveFilter,
}: FilterRowProps) {
const [showCustomLabel, setShowCustomLabel] = useState(false);
const filterLabel = i18n.translate('common.ui.aggTypes.filters.filterLabel', {
defaultMessage: 'Filter {index}',
values: {
index: arrayIndex + 1,
},
});
const FilterControl = (
<EuiFlexGroup gutterSize="s" responsive={false}>
<EuiFlexItem>
<EuiButtonIcon
iconType="tag"
aria-label={i18n.translate('common.ui.aggTypes.filters.toggleFilterButtonAriaLabel', {
defaultMessage: 'Toggle filter label',
})}
aria-expanded={showCustomLabel}
aria-controls={`visEditorFilterLabel${arrayIndex}`}
onClick={() => setShowCustomLabel(!showCustomLabel)}
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiButtonIcon
iconType="trash"
color="danger"
disabled={disableRemove}
aria-label={i18n.translate('common.ui.aggTypes.filters.removeFilterButtonAriaLabel', {
defaultMessage: 'Remove this filter',
})}
onClick={() => onRemoveFilter(id)}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
return (
<EuiForm>
<EuiFormRow
label={`${filterLabel}${customLabel ? ` - ${customLabel}` : ''}`}
labelAppend={FilterControl}
fullWidth={true}
className="visEditorSidebar__aggParamFormRow"
>
<EuiFieldText
value={value}
placeholder={i18n.translate('common.ui.aggTypes.filters.filterPlaceholder', {
defaultMessage: 'Lucene or Query DSL',
})}
data-test-subj={dataTestSubj}
onChange={ev => onChangeValue(id, ev.target.value, customLabel)}
fullWidth={true}
autoFocus={autoFocus}
/>
</EuiFormRow>
{showCustomLabel ? (
<EuiFormRow
id={`visEditorFilterLabel${arrayIndex}`}
label={i18n.translate('common.ui.aggTypes.filters.definiteFilterLabel', {
defaultMessage: 'Filter {index} label',
description:
"'Filter {index}' represents the name of the filter as a noun, similar to 'label for filter 1'.",
values: {
index: arrayIndex + 1,
},
})}
fullWidth={true}
className="visEditorSidebar__aggParamFormRow"
>
<EuiFieldText
value={customLabel}
placeholder={i18n.translate('common.ui.aggTypes.filters.labelPlaceholder', {
defaultMessage: 'Label',
})}
onChange={ev => onChangeValue(id, value, ev.target.value)}
fullWidth={true}
/>
</EuiFormRow>
) : null}
</EuiForm>
);
}
export { FilterRow };

View file

@ -1,90 +0,0 @@
<div class="form-group">
<div ng-repeat="filter in agg.params.filters">
<div class="visEditorSidebar__collapsibleTitle">
<label
for="visEditorFilterInput{{agg.id}}"
>
<span
i18n-id="common.ui.aggTypes.filters.filterLabel"
i18n-default-message="Filter {index}"
i18n-values="{ index: $index + 1 }"
></span>
<span ng-if="filter.label">- {{ filter.label }}</span>
</label>
<div class="kuiButtonGroup kuiButtonGroup--united">
<button
ng-click="showConfig = !showConfig"
type="button"
aria-label="{{ ::'common.ui.aggTypes.filters.toggleFilterButtonAriaLabel' | i18n: { defaultMessage: 'Toggle filter label' } }}"
aria-expanded="{{!!showConfig}}"
aria-controls="visEditorFilterLabel{{agg.id}}"
class="kuiButton kuiButton--basic kuiButton--small">
<i class="fa fa-tag"></i>
</button>
<button
type="button"
aria-label="{{ ::'common.ui.aggTypes.filters.removeFilterButtonAriaLabel' | i18n: { defaultMessage: 'Remove this filter' } }}"
ng-click="agg.params.filters.splice($index, 1)"
class="kuiButton kuiButton--danger kuiButton--small">
<i class="fa fa-times"></i>
</button>
</div>
</div>
<div class="form-group">
<input
placeholder="{{ ::'common.ui.aggTypes.filters.filterPlaceholder' | i18n: { defaultMessage: 'Lucene or Query DSL' } }}"
data-test-subj="visEditorFilterInput_{{agg.id}}_{{$index}}"
id="visEditorFilterInput{{agg.id}}"
parse-query
ng-model="filter.input.query"
type="text"
class="form-control"
name="filter{{$index}}">
</div>
<div class="form-group" ng-show="showConfig" id="visEditorFilterLabel{{agg.id}}">
<label
for="visEditorFilterLabelInput{{agg.id}}"
i18n-id="common.ui.aggTypes.filters.definiteFilterLabel"
i18n-default-message="Filter {index} label"
i18n-values="{ index: $index + 1 }"
i18n-description="'Filter {index}' represents the name of the filter as a noun, similar to 'label for filter 1'."
></label>
<input
id="visEditorFilterLabelInput{{agg.id}}"
ng-model="filter.label"
placeholder="{{ ::'common.ui.aggTypes.filters.labelPlaceholder' | i18n: { defaultMessage: 'Label' } }}"
type="text"
class="form-control"
name="label{{$index}}">
</div>
</div>
</div>
<input ng-model="agg.params.filters.length" name="filterLength" required min="1" type="number" class="ng-hide" />
<div class="hintbox" ng-show="aggForm.filterLength.$invalid">
<p>
<i class="fa fa-danger text-danger"></i>
<strong
i18n-id="common.ui.aggTypes.filters.requiredFilterLabel"
i18n-default-message="Required:"
></strong>
<span
i18n-id="common.ui.aggTypes.filters.requiredFilterDescription"
i18n-default-message="You must specify at least one filter."
></span>
</p>
</div>
<button
data-test-subj="visEditorAddFilterButton"
click-focus="'filter'+(agg.params.filters.length-1)"
ng-click="agg.params.filters.push({input:{}})"
class="kuiButton kuiButton--primary kuiButton--fullWidth"
>
{{ ::'common.ui.aggTypes.filters.addFilterButtonLabel' | i18n: { defaultMessage: 'Add Filter' } }}
</button>
<div class="euiSpacer euiSpacer--s"></div>

View file

@ -0,0 +1,120 @@
/*
* 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, { useState, useEffect } from 'react';
import { omit, isEqual } from 'lodash';
import { htmlIdGenerator, EuiButton, EuiSpacer } from '@elastic/eui';
import { AggParamEditorProps } from 'ui/vis/editors/default';
import { FormattedMessage } from '@kbn/i18n/react';
import { data } from 'plugins/data';
import { FilterRow } from './filter';
const { toUser, fromUser } = data.query.helpers;
const generateId = htmlIdGenerator();
interface FilterValue {
input: any;
label: string;
id: string;
}
function FiltersParamEditor({ agg, value, setValue }: AggParamEditorProps<FilterValue[]>) {
const [filters, setFilters] = useState(() =>
value.map(filter => ({ ...filter, id: generateId() }))
);
useEffect(() => {
// set parsed values into model after initialization
setValue(
filters.map(filter =>
omit({ ...filter, input: { query: fromUser(filter.input.query) } }, 'id')
)
);
}, []);
useEffect(
() => {
// responsible for discarding changes
if (
value.length !== filters.length ||
value.some((filter, index) => !isEqual(filter, omit(filters[index], 'id')))
) {
setFilters(value.map(filter => ({ ...filter, id: generateId() })));
}
},
[value]
);
const updateFilters = (updatedFilters: FilterValue[]) => {
// do not set internal id parameter into saved object
setValue(updatedFilters.map(filter => omit(filter, 'id')));
setFilters(updatedFilters);
};
const onAddFilter = () =>
updateFilters([...filters, { input: { query: '' }, label: '', id: generateId() }]);
const onRemoveFilter = (id: string) => updateFilters(filters.filter(filter => filter.id !== id));
const onChangeValue = (id: string, query: string, label: string) =>
updateFilters(
filters.map(filter =>
filter.id === id
? {
...filter,
input: { query: fromUser(query) },
label,
}
: filter
)
);
return (
<>
{filters.map(({ input, label, id }, arrayIndex) => (
<FilterRow
key={id}
id={id}
arrayIndex={arrayIndex}
customLabel={label}
value={toUser(input.query)}
autoFocus={arrayIndex === filters.length - 1}
disableRemove={arrayIndex === 0 && filters.length === 1}
dataTestSubj={`visEditorFilterInput_${agg.id}_${arrayIndex}`}
onChangeValue={onChangeValue}
onRemoveFilter={onRemoveFilter}
/>
))}
<EuiButton
iconType="plusInCircle"
fill={true}
fullWidth={true}
onClick={onAddFilter}
size="s"
data-test-subj="visEditorAddFilterButton"
>
<FormattedMessage
id="common.ui.aggTypes.filters.addFilterButtonLabel"
defaultMessage="Add filter"
/>
</EuiButton>
<EuiSpacer size="m" />
</>
);
}
export { FiltersParamEditor };

View file

@ -1,41 +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 _ from 'lodash';
import $ from 'jquery';
import { uiModules } from '../../modules';
const module = uiModules.get('kibana');
module.directive('clickFocus', function () {
return {
scope: {
clickFocus: '='
},
restrict: 'A',
link: function ($scope, $elem) {
function handler() {
const focusElem = $.find('input[name=' + $scope.clickFocus + ']');
if (focusElem[0]) focusElem[0].focus();
}
$elem.bind('click', handler);
$scope.$on('$destroy', _.bindKey($elem, 'unbind', 'click', handler));
}
};
});

View file

@ -1,45 +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 { data } from 'plugins/data';
const { toUser, fromUser } = data.query.helpers;
import { uiModules } from '../../modules';
uiModules
.get('kibana')
.directive('parseQuery', function () {
return {
restrict: 'A',
require: 'ngModel',
scope: {
'ngModel': '='
},
link: function ($scope, elem, attr, ngModel) {
const init = function () {
$scope.ngModel = fromUser($scope.ngModel);
};
ngModel.$parsers.push(fromUser);
ngModel.$formatters.push(toUser);
init();
}
};
});

View file

@ -124,13 +124,10 @@
"common.ui.aggTypes.extendedBounds.maxLabel": "最大值",
"common.ui.aggTypes.extendedBounds.minLabel": "最小值",
"common.ui.aggTypes.field.fieldLabel": "字段",
"common.ui.aggTypes.filters.addFilterButtonLabel": "添加筛选",
"common.ui.aggTypes.filters.definiteFilterLabel": "筛选 {index} 标签",
"common.ui.aggTypes.filters.filterLabel": "筛选 {index}",
"common.ui.aggTypes.filters.labelPlaceholder": "标签",
"common.ui.aggTypes.filters.removeFilterButtonAriaLabel": "移除此筛选",
"common.ui.aggTypes.filters.requiredFilterDescription": "必须指定至少一个筛选。",
"common.ui.aggTypes.filters.requiredFilterLabel": "必需:",
"common.ui.aggTypes.filters.toggleFilterButtonAriaLabel": "切换筛选标签",
"common.ui.aggTypes.histogram.missingMaxMinValuesWarning": "无法检索最大值和最小值以自动缩放直方图存储桶。这可能会导致可视化性能低下。",
"common.ui.aggTypes.ipRanges.cidrMask.addRangeButtonLabel": "添加范围",