mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
EUIfication of filters control for Filters aggregation parameter.
This commit is contained in:
parent
bba50a8839
commit
688892e13c
7 changed files with 258 additions and 183 deletions
|
@ -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;
|
||||
|
|
136
src/legacy/ui/public/agg_types/controls/filter.tsx
Normal file
136
src/legacy/ui/public/agg_types/controls/filter.tsx
Normal 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 };
|
|
@ -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>
|
120
src/legacy/ui/public/agg_types/controls/filters.tsx
Normal file
120
src/legacy/ui/public/agg_types/controls/filters.tsx
Normal 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 };
|
|
@ -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));
|
||||
}
|
||||
};
|
||||
});
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
});
|
|
@ -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": "添加范围",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue