mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Unified Search] Create a filter builder component which works with nested OR/AND filters (#136815)
* init: add filters editor component * apply some changes * add storybook * push some changes * push some logic * push some logic * push some logic * add reducer * add more complext filters * feat: add moveFilter action * feat: add input form for filters editor filter item * feat: add filter UI * fix: storybook * fix UI: edit filter component * style: add panel to filter * feat: update style * style: add style for dilimeter * refact: filter item * style: added style for odd filter group * feat: added add filter action * feat: update add filter in filter group * feat: added remove filter * refact: updated add filter and remove filter * feat: added addFilterGroup * feat: updated addFilterGroup with empty filter * refact: utils * refact: seperte filters editor filter (field input) * refact: seperte filters editor filter (operator input) * refact: filters editor * refact: filters editor (params field * push some changes * push some cleanup * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * move filters to __mock__ folder * feat: update handle filter item * add getFilterByPath method * remove addFilterGroupWithFilter * remove addFilterGroupWithFilter * add some logic * add addFilter support * update styles * add removeFilter * fix types * refact: cleanup any * refact: filter item * fix: data for show filter item * refact: cleanup useState in filters editor * refact: filter edit * style: fix UI in filter group * add disableOR/AND/DELETE params * add preparation for d2d * fix: show filter item after add filter * update filterGroup * fix: drag and drop * do cleanup * feat: add logic for drag&drop * style: filter item * apply new structure * push some logic * fix some cases * feat: udpated updateFilter * feat: added update field in filter * feat: added update params in filter * feat: added update operator in filter * feat: added update operator in filter--amend * style: add uiPanel into filterItem * feat: added translate * refact: filter item * feat: updated filter item for exists type * feat: added unique key for filter Item * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * fix: update params * fix: update params * style: params input * fix: disabled for phrase value input * feat: added filter params update * refact: change type * feat: index for EuiDraggable * refact: rename properties * feat: added useCallback * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * refact: cleanup code * refact: minor changes * style: change show label for filter phrase(s) * dandd preparation * feat: add ref to panel * fix merge configct * refact: destination => dropTarget and added it into context * [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' * feat: udpate d&d logic * feat: added chagne cursor for OR filter * feat: add change cursor in d&d * feat: add change cursor in d&d * style: changed cursor view svg * feat: added disable or conditional in d&d * feat: moved filter_editor -> shared_component * style: used emotion react * move filter_label * rename filters editor -> filters builder * feat: added hideOr * feat: added hideOr * feat: added sisabled and drop if depth = maxDepth * feat: update disable isCombine * feat: rename filtersEditor->filtersBulder * feat: added new storybook * feat: remove autocomplete service * refact: updated filterBuilder * feat: updated css style * fix: jest test * refact: move snapshot for filter label * feat: update search bar * refact: filter builder utils * fix: update filter * refact: update filter builder * fix: autocomplete test * fix: test * fix: jest test * fix: d&d * test: update test * test: update test * fix: functional tests: * fix: check type * refact: move filter * refact: minor * feat: move filter editor from shared components to filter bar * minor update * fix: check type * fix: type check * fix: d&d * fix: d&d * feat: resolve comment * minor fix * fix: d&d * fix: type check test * fix: check type test * feat: covered new case with d&d * feat: covered new case with d&d * feat: covered new case with d&d * refact: move filter * fix: bug with storybook * refact: remove todo comment * fix normalizeFilters method * refact: ref to FilterEditor * refact: moved filters_builder to up level * fix: update operator * refact: key for list * refact: did FiltersBuilder lazy * feat: move cvg to under component and rename * feat: resolve comment * feat: added validation for filter build item into params enter uncorrect date * test: added test for normalizeFilters * feat: update test * style: fix UI * refact: utils files in filters builder * style: filter group background * style: filter group * doc: added comment to methods in utils * style: fix border * style: fix border * fix: UI * feat: added backgroud for gray filter group * temp changes * fix some styles * feat: added devider and margin * style: change style -> css * style: change style -> css * refact: style * refact: style * fix: test * refact: style * feat: added rule for show border * style: UI * style: show OR delimiter * style: margin for AND delimiter * feat: update show border for first level * feat: update styles * style: fix padding for panel
This commit is contained in:
parent
efbf9e4d31
commit
1fcd75a706
62 changed files with 2791 additions and 183 deletions
|
@ -291,7 +291,6 @@ export function plugin(initializerContext: PluginInitializerContext<ConfigSchema
|
|||
export type {
|
||||
DataPublicPluginSetup,
|
||||
DataPublicPluginStart,
|
||||
IDataPluginServices,
|
||||
DataPublicPluginStartActions,
|
||||
} from './types';
|
||||
|
||||
|
|
|
@ -6,14 +6,12 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { CoreStart } from '@kbn/core/public';
|
||||
import { BfetchPublicSetup } from '@kbn/bfetch-plugin/public';
|
||||
import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public';
|
||||
import { ExpressionsSetup } from '@kbn/expressions-plugin/public';
|
||||
import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
|
||||
import { UiActionsSetup, UiActionsStart } from '@kbn/ui-actions-plugin/public';
|
||||
import { FieldFormatsSetup, FieldFormatsStart } from '@kbn/field-formats-plugin/public';
|
||||
import { UsageCollectionSetup, UsageCollectionStart } from '@kbn/usage-collection-plugin/public';
|
||||
import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public';
|
||||
import { Setup as InspectorSetup } from '@kbn/inspector-plugin/public';
|
||||
import { ScreenshotModePluginStart } from '@kbn/screenshot-mode-plugin/public';
|
||||
import { SharePluginStart } from '@kbn/share-plugin/public';
|
||||
|
@ -102,15 +100,3 @@ export interface DataPublicPluginStart {
|
|||
|
||||
nowProvider: NowProviderPublicContract;
|
||||
}
|
||||
|
||||
export interface IDataPluginServices extends Partial<CoreStart> {
|
||||
appName: string;
|
||||
uiSettings: CoreStart['uiSettings'];
|
||||
savedObjects: CoreStart['savedObjects'];
|
||||
notifications: CoreStart['notifications'];
|
||||
application: CoreStart['application'];
|
||||
http: CoreStart['http'];
|
||||
storage: IStorageWrapper;
|
||||
data: DataPublicPluginStart;
|
||||
usageCollection?: UsageCollectionStart;
|
||||
}
|
||||
|
|
|
@ -26,8 +26,8 @@ import {
|
|||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import type { DataViewListItem } from '@kbn/data-views-plugin/public';
|
||||
import { IDataPluginServices } from '@kbn/data-plugin/public';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import type { IUnifiedSearchPluginServices } from '../types';
|
||||
import type { DataViewPickerPropsExtended } from '.';
|
||||
import { DataViewsList } from './dataview_list';
|
||||
import type { TextBasedLanguagesListProps } from './text_languages_list';
|
||||
|
@ -82,7 +82,7 @@ export function ChangeDataView({
|
|||
const [isTextLangTransitionModalVisible, setIsTextLangTransitionModalVisible] = useState(false);
|
||||
const [selectedDataViewId, setSelectedDataViewId] = useState(currentDataViewId);
|
||||
|
||||
const kibana = useKibana<IDataPluginServices>();
|
||||
const kibana = useKibana<IUnifiedSearchPluginServices>();
|
||||
const { application, data, storage } = kibana.services;
|
||||
const styles = changeDataViewStyles({ fullWidth: trigger.fullWidth });
|
||||
const [isTextLangTransitionModalDismissed, setIsTextLangTransitionModalDismissed] = useState(() =>
|
||||
|
|
|
@ -7,7 +7,8 @@
|
|||
*/
|
||||
|
||||
import { registerTestBed, TestBed } from '@kbn/test-jest-helpers';
|
||||
import { FilterEditor, Props } from '.';
|
||||
import type { FilterEditorProps } from '.';
|
||||
import { FilterEditor } from '.';
|
||||
import React from 'react';
|
||||
|
||||
jest.mock('@kbn/kibana-react-plugin/public', () => {
|
||||
|
@ -32,7 +33,7 @@ describe('<FilterEditor />', () => {
|
|||
let testBed: TestBed;
|
||||
|
||||
beforeEach(async () => {
|
||||
const defaultProps: Omit<Props, 'intl'> = {
|
||||
const defaultProps: Omit<FilterEditorProps, 'intl'> = {
|
||||
filter: {
|
||||
meta: {
|
||||
type: 'phase',
|
||||
|
|
|
@ -48,8 +48,9 @@ import { Operator } from './lib/filter_operators';
|
|||
import { PhraseValueInput } from './phrase_value_input';
|
||||
import { PhrasesValuesInput } from './phrases_values_input';
|
||||
import { RangeValueInput } from './range_value_input';
|
||||
import { getFieldValidityAndErrorMessage } from './lib/helpers';
|
||||
|
||||
export interface Props {
|
||||
export interface FilterEditorProps {
|
||||
filter: Filter;
|
||||
indexPatterns: DataView[];
|
||||
onSubmit: (filter: Filter) => void;
|
||||
|
@ -84,8 +85,8 @@ const updateButtonLabel = i18n.translate('unifiedSearch.filter.filterEditor.upda
|
|||
defaultMessage: 'Update filter',
|
||||
});
|
||||
|
||||
class FilterEditorUI extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
class FilterEditorUI extends Component<FilterEditorProps, State> {
|
||||
constructor(props: FilterEditorProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
selectedIndexPattern: this.getIndexPatternFromFilter(),
|
||||
|
@ -356,32 +357,55 @@ class FilterEditorUI extends Component<Props, State> {
|
|||
return '';
|
||||
}
|
||||
|
||||
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(
|
||||
this.state.selectedField,
|
||||
this.state.params
|
||||
);
|
||||
|
||||
switch (this.state.selectedOperator.type) {
|
||||
case 'exists':
|
||||
return '';
|
||||
case 'phrase':
|
||||
return (
|
||||
<PhraseValueInput
|
||||
indexPattern={indexPattern}
|
||||
field={this.state.selectedField}
|
||||
value={this.state.params}
|
||||
onChange={this.onParamsChange}
|
||||
data-test-subj="phraseValueInput"
|
||||
timeRangeForSuggestionsOverride={this.props.timeRangeForSuggestionsOverride}
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
/>
|
||||
label={this.props.intl.formatMessage({
|
||||
id: 'unifiedSearch.filter.filterEditor.valueInputLabel',
|
||||
defaultMessage: 'Value',
|
||||
})}
|
||||
isInvalid={isInvalid}
|
||||
error={errorMessage}
|
||||
>
|
||||
<PhraseValueInput
|
||||
indexPattern={indexPattern}
|
||||
field={this.state.selectedField}
|
||||
value={this.state.params}
|
||||
onChange={this.onParamsChange}
|
||||
data-test-subj="phraseValueInput"
|
||||
timeRangeForSuggestionsOverride={this.props.timeRangeForSuggestionsOverride}
|
||||
fullWidth
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
case 'phrases':
|
||||
return (
|
||||
<PhrasesValuesInput
|
||||
indexPattern={indexPattern}
|
||||
field={this.state.selectedField}
|
||||
values={this.state.params}
|
||||
onChange={this.onParamsChange}
|
||||
onParamsUpdate={this.onParamsUpdate}
|
||||
timeRangeForSuggestionsOverride={this.props.timeRangeForSuggestionsOverride}
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
/>
|
||||
label={this.props.intl.formatMessage({
|
||||
id: 'unifiedSearch.filter.filterEditor.valuesSelectLabel',
|
||||
defaultMessage: 'Values',
|
||||
})}
|
||||
>
|
||||
<PhrasesValuesInput
|
||||
indexPattern={indexPattern}
|
||||
field={this.state.selectedField}
|
||||
values={this.state.params}
|
||||
onChange={this.onParamsChange}
|
||||
onParamsUpdate={this.onParamsUpdate}
|
||||
timeRangeForSuggestionsOverride={this.props.timeRangeForSuggestionsOverride}
|
||||
fullWidth
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
case 'range':
|
||||
return (
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export type { Operator } from './lib';
|
||||
|
||||
export {
|
||||
getFieldFromFilter,
|
||||
getOperatorFromFilter,
|
||||
getFilterableFields,
|
||||
getOperatorOptions,
|
||||
validateParams,
|
||||
isFilterValid,
|
||||
isOperator,
|
||||
isNotOperator,
|
||||
isOneOfOperator,
|
||||
isNotOneOfOperator,
|
||||
isBetweenOperator,
|
||||
isNotBetweenOperator,
|
||||
existsOperator,
|
||||
doesNotExistOperator,
|
||||
FILTER_OPERATORS,
|
||||
} from './lib';
|
||||
|
||||
export type { GenericComboBoxProps } from './generic_combo_box';
|
||||
export type { PhraseSuggestorProps } from './phrase_suggestor';
|
||||
export type { PhrasesSuggestorProps } from './phrases_values_input';
|
||||
|
||||
export { GenericComboBox } from './generic_combo_box';
|
||||
export { PhraseSuggestor } from './phrase_suggestor';
|
||||
export { PhrasesValuesInput } from './phrases_values_input';
|
||||
export { PhraseValueInput } from './phrase_value_input';
|
||||
export { RangeValueInput, isRangeParams } from './range_value_input';
|
||||
export { ValueInputType } from './value_input_type';
|
||||
|
||||
export { FilterEditor } from './filter_editor';
|
||||
export type { FilterEditorProps } from './filter_editor';
|
|
@ -37,7 +37,7 @@ export function getOperatorOptions(field: DataViewField) {
|
|||
}
|
||||
|
||||
export function validateParams(params: any, field: DataViewField) {
|
||||
switch (field.type) {
|
||||
switch (field?.type) {
|
||||
case 'date':
|
||||
const moment = typeof params === 'string' ? dateMath.parse(params) : null;
|
||||
return Boolean(typeof params === 'string' && moment && moment.isValid());
|
||||
|
|
|
@ -10,13 +10,13 @@ import type { DataViewField } from '@kbn/data-views-plugin/common';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { KBN_FIELD_TYPES } from '@kbn/data-plugin/public';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { validateParams } from '../filter_bar/filter_editor/lib/filter_editor_utils';
|
||||
import { validateParams } from './filter_editor_utils';
|
||||
|
||||
export const getFieldValidityAndErrorMessage = (
|
||||
field: DataViewField,
|
||||
value?: string | undefined
|
||||
): { isInvalid: boolean; errorMessage?: string } => {
|
||||
const type = field.type;
|
||||
const type = field?.type;
|
||||
switch (type) {
|
||||
case KBN_FIELD_TYPES.DATE:
|
||||
case KBN_FIELD_TYPES.DATE_RANGE:
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export { getFieldValidityAndErrorMessage } from './helpers';
|
||||
export type { Operator } from './filter_operators';
|
||||
export {
|
||||
isOperator,
|
||||
isNotOperator,
|
||||
isOneOfOperator,
|
||||
isNotOneOfOperator,
|
||||
isBetweenOperator,
|
||||
isNotBetweenOperator,
|
||||
existsOperator,
|
||||
doesNotExistOperator,
|
||||
FILTER_OPERATORS,
|
||||
} from './filter_operators';
|
||||
export {
|
||||
getFieldFromFilter,
|
||||
getOperatorFromFilter,
|
||||
getFilterableFields,
|
||||
getOperatorOptions,
|
||||
validateParams,
|
||||
isFilterValid,
|
||||
} from './filter_editor_utils';
|
|
@ -10,13 +10,12 @@ import React from 'react';
|
|||
import { withKibana, KibanaReactContextValue } from '@kbn/kibana-react-plugin/public';
|
||||
import { UI_SETTINGS } from '@kbn/data-plugin/common';
|
||||
import { DataView, DataViewField } from '@kbn/data-views-plugin/common';
|
||||
import { IDataPluginServices } from '@kbn/data-plugin/public';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
import { getAutocomplete } from '../../services';
|
||||
import { IUnifiedSearchPluginServices } from '../../types';
|
||||
|
||||
export interface PhraseSuggestorProps {
|
||||
kibana: KibanaReactContextValue<IDataPluginServices>;
|
||||
kibana: KibanaReactContextValue<IUnifiedSearchPluginServices>;
|
||||
indexPattern: DataView;
|
||||
field: DataViewField;
|
||||
timeRangeForSuggestionsOverride?: boolean;
|
||||
|
@ -80,7 +79,7 @@ export class PhraseSuggestorUI<T extends PhraseSuggestorProps> extends React.Com
|
|||
return;
|
||||
}
|
||||
this.setState({ isLoading: true });
|
||||
const suggestions = await getAutocomplete().getValueSuggestions({
|
||||
const suggestions = await this.services.unifiedSearch.autocomplete.getValueSuggestions({
|
||||
indexPattern,
|
||||
field,
|
||||
query,
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { EuiFormRow } from '@elastic/eui';
|
||||
import { InjectedIntl, injectI18n } from '@kbn/i18n-react';
|
||||
import { uniq } from 'lodash';
|
||||
import React from 'react';
|
||||
|
@ -14,36 +13,27 @@ import { withKibana } from '@kbn/kibana-react-plugin/public';
|
|||
import { GenericComboBox, GenericComboBoxProps } from './generic_combo_box';
|
||||
import { PhraseSuggestorUI, PhraseSuggestorProps } from './phrase_suggestor';
|
||||
import { ValueInputType } from './value_input_type';
|
||||
import { getFieldValidityAndErrorMessage } from '../../utils/helpers';
|
||||
|
||||
interface Props extends PhraseSuggestorProps {
|
||||
interface PhraseValueInputProps extends PhraseSuggestorProps {
|
||||
value?: string;
|
||||
onChange: (value: string | number | boolean) => void;
|
||||
intl: InjectedIntl;
|
||||
fullWidth?: boolean;
|
||||
compressed?: boolean;
|
||||
disabled?: boolean;
|
||||
isInvalid?: boolean;
|
||||
}
|
||||
|
||||
class PhraseValueInputUI extends PhraseSuggestorUI<Props> {
|
||||
class PhraseValueInputUI extends PhraseSuggestorUI<PhraseValueInputProps> {
|
||||
public render() {
|
||||
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(
|
||||
this.props.field,
|
||||
this.props.value
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
fullWidth={this.props.fullWidth}
|
||||
label={this.props.intl.formatMessage({
|
||||
id: 'unifiedSearch.filter.filterEditor.valueInputLabel',
|
||||
defaultMessage: 'Value',
|
||||
})}
|
||||
isInvalid={isInvalid}
|
||||
error={errorMessage}
|
||||
>
|
||||
<>
|
||||
{this.isSuggestingValues() ? (
|
||||
this.renderWithSuggestions()
|
||||
) : (
|
||||
<ValueInputType
|
||||
disabled={this.props.disabled}
|
||||
compressed={this.props.compressed}
|
||||
fullWidth={this.props.fullWidth}
|
||||
placeholder={this.props.intl.formatMessage({
|
||||
id: 'unifiedSearch.filter.filterEditor.valueInputPlaceholder',
|
||||
|
@ -52,10 +42,10 @@ class PhraseValueInputUI extends PhraseSuggestorUI<Props> {
|
|||
value={this.props.value}
|
||||
onChange={this.props.onChange}
|
||||
field={this.props.field}
|
||||
isInvalid={isInvalid}
|
||||
isInvalid={this.props.isInvalid}
|
||||
/>
|
||||
)}
|
||||
</EuiFormRow>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -67,7 +57,9 @@ class PhraseValueInputUI extends PhraseSuggestorUI<Props> {
|
|||
const options = value ? uniq([valueAsStr, ...suggestions]) : suggestions;
|
||||
return (
|
||||
<StringComboBox
|
||||
isDisabled={this.props.disabled}
|
||||
fullWidth={fullWidth}
|
||||
compressed={this.props.compressed}
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'unifiedSearch.filter.filterEditor.valueSelectPlaceholder',
|
||||
defaultMessage: 'Select a value',
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { EuiFormRow } from '@elastic/eui';
|
||||
import { InjectedIntl, injectI18n } from '@kbn/i18n-react';
|
||||
import { uniq } from 'lodash';
|
||||
import React from 'react';
|
||||
|
@ -14,46 +13,40 @@ import { withKibana } from '@kbn/kibana-react-plugin/public';
|
|||
import { GenericComboBox, GenericComboBoxProps } from './generic_combo_box';
|
||||
import { PhraseSuggestorUI, PhraseSuggestorProps } from './phrase_suggestor';
|
||||
|
||||
interface Props extends PhraseSuggestorProps {
|
||||
export interface PhrasesSuggestorProps extends PhraseSuggestorProps {
|
||||
values?: string[];
|
||||
onChange: (values: string[]) => void;
|
||||
onParamsUpdate: (value: string) => void;
|
||||
intl: InjectedIntl;
|
||||
fullWidth?: boolean;
|
||||
compressed?: boolean;
|
||||
}
|
||||
|
||||
class PhrasesValuesInputUI extends PhraseSuggestorUI<Props> {
|
||||
class PhrasesValuesInputUI extends PhraseSuggestorUI<PhrasesSuggestorProps> {
|
||||
public render() {
|
||||
const { suggestions } = this.state;
|
||||
const { values, intl, onChange, fullWidth, onParamsUpdate } = this.props;
|
||||
const { values, intl, onChange, fullWidth, onParamsUpdate, compressed } = this.props;
|
||||
const options = values ? uniq([...values, ...suggestions]) : suggestions;
|
||||
return (
|
||||
<EuiFormRow
|
||||
<StringComboBox
|
||||
fullWidth={fullWidth}
|
||||
label={intl.formatMessage({
|
||||
id: 'unifiedSearch.filter.filterEditor.valuesSelectLabel',
|
||||
defaultMessage: 'Values',
|
||||
compressed={compressed}
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'unifiedSearch.filter.filterEditor.valuesSelectPlaceholder',
|
||||
defaultMessage: 'Select values',
|
||||
})}
|
||||
>
|
||||
<StringComboBox
|
||||
fullWidth={fullWidth}
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'unifiedSearch.filter.filterEditor.valuesSelectPlaceholder',
|
||||
defaultMessage: 'Select values',
|
||||
})}
|
||||
delimiter=","
|
||||
options={options}
|
||||
getLabel={(option) => option}
|
||||
selectedOptions={values || []}
|
||||
onSearchChange={this.onSearchChange}
|
||||
onCreateOption={(option: string) => {
|
||||
onParamsUpdate(option.trim());
|
||||
}}
|
||||
onChange={onChange}
|
||||
isClearable={false}
|
||||
data-test-subj="filterParamsComboBox phrasesParamsComboxBox"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
delimiter=","
|
||||
options={options}
|
||||
getLabel={(option) => option}
|
||||
selectedOptions={values || []}
|
||||
onSearchChange={this.onSearchChange}
|
||||
onCreateOption={(option: string) => {
|
||||
onParamsUpdate(option.trim());
|
||||
}}
|
||||
onChange={onChange}
|
||||
isClearable={false}
|
||||
data-test-subj="filterParamsComboBox phrasesParamsComboxBox"
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,6 +28,11 @@ interface Props {
|
|||
onChange: (params: RangeParamsPartial) => void;
|
||||
intl: InjectedIntl;
|
||||
fullWidth?: boolean;
|
||||
compressed?: boolean;
|
||||
}
|
||||
|
||||
export function isRangeParams(params: any): params is RangeParams {
|
||||
return Boolean(params && 'from' in params && 'to' in params);
|
||||
}
|
||||
|
||||
function RangeValueInputUI(props: Props) {
|
||||
|
@ -68,6 +73,7 @@ function RangeValueInputUI(props: Props) {
|
|||
startControl={
|
||||
<ValueInputType
|
||||
controlOnly
|
||||
compressed={props.compressed}
|
||||
field={props.field}
|
||||
value={props.value ? props.value.from : undefined}
|
||||
onChange={onFromChange}
|
||||
|
@ -83,6 +89,7 @@ function RangeValueInputUI(props: Props) {
|
|||
endControl={
|
||||
<ValueInputType
|
||||
controlOnly
|
||||
compressed={props.compressed}
|
||||
field={props.field}
|
||||
value={props.value ? props.value.to : undefined}
|
||||
onChange={onToChange}
|
||||
|
|
|
@ -24,6 +24,8 @@ interface Props {
|
|||
className?: string;
|
||||
fullWidth?: boolean;
|
||||
isInvalid?: boolean;
|
||||
compressed?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
class ValueInputTypeUI extends Component<Props> {
|
||||
|
@ -37,12 +39,14 @@ class ValueInputTypeUI extends Component<Props> {
|
|||
|
||||
public render() {
|
||||
const value = this.props.value;
|
||||
const type = this.props.field.type;
|
||||
const type = this.props.field?.type ?? 'string';
|
||||
let inputElement: React.ReactNode;
|
||||
switch (type) {
|
||||
case 'string':
|
||||
inputElement = (
|
||||
<EuiFieldText
|
||||
compressed={this.props.compressed}
|
||||
disabled={this.props.disabled}
|
||||
fullWidth={this.props.fullWidth}
|
||||
placeholder={this.props.placeholder}
|
||||
value={value}
|
||||
|
@ -57,6 +61,8 @@ class ValueInputTypeUI extends Component<Props> {
|
|||
case 'number_range':
|
||||
inputElement = (
|
||||
<EuiFieldNumber
|
||||
compressed={this.props.compressed}
|
||||
disabled={this.props.disabled}
|
||||
fullWidth={this.props.fullWidth}
|
||||
placeholder={this.props.placeholder}
|
||||
value={this.getValueForNumberField(value)}
|
||||
|
@ -70,6 +76,8 @@ class ValueInputTypeUI extends Component<Props> {
|
|||
case 'date_range':
|
||||
inputElement = (
|
||||
<EuiFieldText
|
||||
compressed={this.props.compressed}
|
||||
disabled={this.props.disabled}
|
||||
fullWidth={this.props.fullWidth}
|
||||
placeholder={this.props.placeholder}
|
||||
value={value}
|
||||
|
@ -86,12 +94,14 @@ class ValueInputTypeUI extends Component<Props> {
|
|||
inputElement = (
|
||||
<EuiFieldText
|
||||
fullWidth={this.props.fullWidth}
|
||||
disabled={this.props.disabled}
|
||||
placeholder={this.props.placeholder}
|
||||
value={value}
|
||||
onChange={this.onChange}
|
||||
isInvalid={!isEmpty(value) && !validateParams(value, this.props.field)}
|
||||
controlOnly={this.props.controlOnly}
|
||||
className={this.props.className}
|
||||
compressed={this.props.compressed}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
|
@ -119,6 +129,7 @@ class ValueInputTypeUI extends Component<Props> {
|
|||
onChange={this.onBoolChange}
|
||||
className={this.props.className}
|
||||
fullWidth={this.props.fullWidth}
|
||||
compressed={this.props.compressed}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
|
|
|
@ -27,7 +27,7 @@ import {
|
|||
getDisplayValueFromFilter,
|
||||
getFieldDisplayValueFromFilter,
|
||||
} from '@kbn/data-plugin/public';
|
||||
import { FilterEditor } from '../filter_editor';
|
||||
import { FilterEditor } from '../filter_editor/filter_editor';
|
||||
import { FilterView } from '../filter_view';
|
||||
import { getIndexPatterns } from '../../services';
|
||||
import { FilterPanelOption } from '../../types';
|
||||
|
|
|
@ -11,11 +11,11 @@ import { css } from '@emotion/react';
|
|||
import { EuiFlexItem } from '@elastic/eui';
|
||||
import { InjectedIntl, injectI18n } from '@kbn/i18n-react';
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
import { IDataPluginServices } from '@kbn/data-plugin/public';
|
||||
import { METRIC_TYPE } from '@kbn/analytics';
|
||||
import { DataView } from '@kbn/data-views-plugin/public';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { FilterItem, FilterItemProps } from './filter_item';
|
||||
import type { IUnifiedSearchPluginServices } from '../../types';
|
||||
|
||||
/**
|
||||
* Properties for the filter items component, which will render a single filter pill for every filter that is sent in
|
||||
|
@ -41,7 +41,7 @@ export interface FilterItemsProps {
|
|||
|
||||
const FilterItemsUI = React.memo(function FilterItemsUI(props: FilterItemsProps) {
|
||||
const groupRef = useRef<HTMLDivElement>(null);
|
||||
const kibana = useKibana<IDataPluginServices>();
|
||||
const kibana = useKibana<IUnifiedSearchPluginServices>();
|
||||
const { appName, usageCollection, uiSettings } = kibana.services;
|
||||
const { readOnly = false } = props;
|
||||
|
||||
|
|
|
@ -10,8 +10,8 @@ import React, { Fragment } from 'react';
|
|||
import { EuiTextColor } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Filter, FILTERS } from '@kbn/es-query';
|
||||
import { existsOperator, isOneOfOperator } from './filter_operators';
|
||||
import type { FilterLabelStatus } from '../../filter_item/filter_item';
|
||||
import type { FilterLabelStatus } from '../filter_item/filter_item';
|
||||
import { existsOperator, isOneOfOperator } from '../filter_editor';
|
||||
|
||||
export interface FilterLabelProps {
|
||||
filter: Filter;
|
|
@ -29,7 +29,7 @@ export const FilterItems = (props: React.ComponentProps<typeof LazyFilterItems>)
|
|||
</React.Suspense>
|
||||
);
|
||||
|
||||
const LazyFilterLabel = React.lazy(() => import('./filter_editor/lib/filter_label'));
|
||||
const LazyFilterLabel = React.lazy(() => import('./filter_label/filter_label'));
|
||||
/**
|
||||
* Renders the label for a single filter pill
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,630 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
|
||||
export const getFiltersMock = () =>
|
||||
[
|
||||
{
|
||||
meta: {
|
||||
index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
|
||||
alias: null,
|
||||
negate: false,
|
||||
disabled: false,
|
||||
type: 'phrase',
|
||||
key: 'category.keyword',
|
||||
params: {
|
||||
query: "Men's Accessories 1",
|
||||
},
|
||||
},
|
||||
query: {
|
||||
match_phrase: {
|
||||
'category.keyword': "Men's Accessories 1",
|
||||
},
|
||||
},
|
||||
$state: {
|
||||
store: 'appState',
|
||||
},
|
||||
},
|
||||
{
|
||||
meta: {
|
||||
type: 'OR',
|
||||
params: [
|
||||
{
|
||||
meta: {
|
||||
index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
|
||||
alias: null,
|
||||
negate: false,
|
||||
disabled: false,
|
||||
type: 'phrase',
|
||||
key: 'category.keyword',
|
||||
params: {
|
||||
query: "Men's Accessories 2",
|
||||
},
|
||||
},
|
||||
query: {
|
||||
match_phrase: {
|
||||
'category.keyword': "Men's Accessories 2",
|
||||
},
|
||||
},
|
||||
$state: {
|
||||
store: 'appState',
|
||||
},
|
||||
},
|
||||
[
|
||||
{
|
||||
meta: {
|
||||
index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
|
||||
alias: null,
|
||||
negate: false,
|
||||
disabled: false,
|
||||
type: 'phrase',
|
||||
key: 'category.keyword',
|
||||
params: {
|
||||
query: "Men's Accessories 3",
|
||||
},
|
||||
},
|
||||
query: {
|
||||
match_phrase: {
|
||||
'category.keyword': "Men's Accessories 3",
|
||||
},
|
||||
},
|
||||
$state: {
|
||||
store: 'appState',
|
||||
},
|
||||
},
|
||||
{
|
||||
meta: {
|
||||
index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
|
||||
alias: null,
|
||||
negate: false,
|
||||
disabled: false,
|
||||
type: 'phrase',
|
||||
key: 'category.keyword',
|
||||
params: {
|
||||
query: "Men's Accessories 4",
|
||||
},
|
||||
},
|
||||
query: {
|
||||
match_phrase: {
|
||||
'category.keyword': "Men's Accessories 4",
|
||||
},
|
||||
},
|
||||
$state: {
|
||||
store: 'appState',
|
||||
},
|
||||
},
|
||||
],
|
||||
{
|
||||
meta: {
|
||||
index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
|
||||
alias: null,
|
||||
negate: false,
|
||||
disabled: false,
|
||||
type: 'phrase',
|
||||
key: 'category.keyword',
|
||||
params: {
|
||||
query: "Men's Accessories 5",
|
||||
},
|
||||
},
|
||||
query: {
|
||||
match_phrase: {
|
||||
'category.keyword': "Men's Accessories 5",
|
||||
},
|
||||
},
|
||||
$state: {
|
||||
store: 'appState',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
meta: {
|
||||
index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
|
||||
alias: null,
|
||||
negate: false,
|
||||
disabled: false,
|
||||
type: 'phrase',
|
||||
key: 'category.keyword',
|
||||
params: {
|
||||
query: "Men's Accessories 6",
|
||||
},
|
||||
},
|
||||
query: {
|
||||
match_phrase: {
|
||||
'category.keyword': "Men's Accessories 6",
|
||||
},
|
||||
},
|
||||
$state: {
|
||||
store: 'appState',
|
||||
},
|
||||
},
|
||||
] as Filter[];
|
||||
|
||||
export const getFiltersMockOrHide = () =>
|
||||
[
|
||||
{
|
||||
meta: {
|
||||
index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
|
||||
alias: null,
|
||||
negate: false,
|
||||
disabled: false,
|
||||
type: 'phrase',
|
||||
key: 'category.keyword',
|
||||
params: {
|
||||
query: "Men's Accessories 1",
|
||||
},
|
||||
},
|
||||
query: {
|
||||
match_phrase: {
|
||||
'category.keyword': "Men's Accessories 1",
|
||||
},
|
||||
},
|
||||
$state: {
|
||||
store: 'appState',
|
||||
},
|
||||
},
|
||||
{
|
||||
meta: {
|
||||
index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
|
||||
alias: null,
|
||||
negate: false,
|
||||
disabled: false,
|
||||
type: 'phrase',
|
||||
key: 'category.keyword',
|
||||
params: {
|
||||
query: "Men's Accessories 2",
|
||||
},
|
||||
},
|
||||
query: {
|
||||
match_phrase: {
|
||||
'category.keyword': "Men's Accessories 2",
|
||||
},
|
||||
},
|
||||
$state: {
|
||||
store: 'appState',
|
||||
},
|
||||
},
|
||||
{
|
||||
meta: {
|
||||
index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
|
||||
alias: null,
|
||||
negate: false,
|
||||
disabled: false,
|
||||
type: 'phrase',
|
||||
key: 'category.keyword',
|
||||
params: {
|
||||
query: "Men's Accessories 3",
|
||||
},
|
||||
},
|
||||
query: {
|
||||
match_phrase: {
|
||||
'category.keyword': "Men's Accessories 3",
|
||||
},
|
||||
},
|
||||
$state: {
|
||||
store: 'appState',
|
||||
},
|
||||
},
|
||||
{
|
||||
meta: {
|
||||
index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
|
||||
alias: null,
|
||||
negate: false,
|
||||
disabled: false,
|
||||
type: 'phrase',
|
||||
key: 'category.keyword',
|
||||
params: {
|
||||
query: "Men's Accessories 4",
|
||||
},
|
||||
},
|
||||
query: {
|
||||
match_phrase: {
|
||||
'category.keyword': "Men's Accessories 4",
|
||||
},
|
||||
},
|
||||
$state: {
|
||||
store: 'appState',
|
||||
},
|
||||
},
|
||||
{
|
||||
meta: {
|
||||
index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
|
||||
alias: null,
|
||||
negate: false,
|
||||
disabled: false,
|
||||
type: 'phrase',
|
||||
key: 'category.keyword',
|
||||
params: {
|
||||
query: "Men's Accessories 5",
|
||||
},
|
||||
},
|
||||
query: {
|
||||
match_phrase: {
|
||||
'category.keyword': "Men's Accessories 5",
|
||||
},
|
||||
},
|
||||
$state: {
|
||||
store: 'appState',
|
||||
},
|
||||
},
|
||||
{
|
||||
meta: {
|
||||
index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
|
||||
alias: null,
|
||||
negate: false,
|
||||
disabled: false,
|
||||
type: 'phrase',
|
||||
key: 'category.keyword',
|
||||
params: {
|
||||
query: "Men's Accessories 6",
|
||||
},
|
||||
},
|
||||
query: {
|
||||
match_phrase: {
|
||||
'category.keyword': "Men's Accessories 6",
|
||||
},
|
||||
},
|
||||
$state: {
|
||||
store: 'appState',
|
||||
},
|
||||
},
|
||||
] as Filter[];
|
||||
|
||||
export const getDataThatNeedsNormalized = () =>
|
||||
[
|
||||
{
|
||||
meta: {
|
||||
index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
|
||||
alias: null,
|
||||
negate: false,
|
||||
disabled: false,
|
||||
type: 'phrase',
|
||||
key: 'category.keyword',
|
||||
params: {
|
||||
query: "Men's Accessories 1",
|
||||
},
|
||||
},
|
||||
query: {
|
||||
match_phrase: {
|
||||
'category.keyword': "Men's Accessories 1",
|
||||
},
|
||||
},
|
||||
$state: {
|
||||
store: 'appState',
|
||||
},
|
||||
},
|
||||
{
|
||||
meta: {
|
||||
type: 'OR',
|
||||
params: [
|
||||
{
|
||||
meta: {
|
||||
index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
|
||||
alias: null,
|
||||
negate: false,
|
||||
disabled: false,
|
||||
type: 'phrase',
|
||||
key: 'category.keyword',
|
||||
params: {
|
||||
query: "Men's Accessories 2",
|
||||
},
|
||||
},
|
||||
query: {
|
||||
match_phrase: {
|
||||
'category.keyword': "Men's Accessories 2",
|
||||
},
|
||||
},
|
||||
$state: {
|
||||
store: 'appState',
|
||||
},
|
||||
},
|
||||
[
|
||||
{
|
||||
meta: {
|
||||
index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
|
||||
alias: null,
|
||||
negate: false,
|
||||
disabled: false,
|
||||
type: 'phrase',
|
||||
key: 'category.keyword',
|
||||
params: {
|
||||
query: "Men's Accessories 3",
|
||||
},
|
||||
},
|
||||
query: {
|
||||
match_phrase: {
|
||||
'category.keyword': "Men's Accessories 3",
|
||||
},
|
||||
},
|
||||
$state: {
|
||||
store: 'appState',
|
||||
},
|
||||
},
|
||||
],
|
||||
{
|
||||
meta: {
|
||||
index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
|
||||
alias: null,
|
||||
negate: false,
|
||||
disabled: false,
|
||||
type: 'phrase',
|
||||
key: 'category.keyword',
|
||||
params: {
|
||||
query: "Men's Accessories 5",
|
||||
},
|
||||
},
|
||||
query: {
|
||||
match_phrase: {
|
||||
'category.keyword': "Men's Accessories 5",
|
||||
},
|
||||
},
|
||||
$state: {
|
||||
store: 'appState',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
meta: {
|
||||
index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
|
||||
alias: null,
|
||||
negate: false,
|
||||
disabled: false,
|
||||
type: 'phrase',
|
||||
key: 'category.keyword',
|
||||
params: {
|
||||
query: "Men's Accessories 6",
|
||||
},
|
||||
},
|
||||
query: {
|
||||
match_phrase: {
|
||||
'category.keyword': "Men's Accessories 6",
|
||||
},
|
||||
},
|
||||
$state: {
|
||||
store: 'appState',
|
||||
},
|
||||
},
|
||||
] as Filter[];
|
||||
|
||||
export const getDataAfterNormalized = () =>
|
||||
[
|
||||
{
|
||||
meta: {
|
||||
index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
|
||||
alias: null,
|
||||
negate: false,
|
||||
disabled: false,
|
||||
type: 'phrase',
|
||||
key: 'category.keyword',
|
||||
params: {
|
||||
query: "Men's Accessories 1",
|
||||
},
|
||||
},
|
||||
query: {
|
||||
match_phrase: {
|
||||
'category.keyword': "Men's Accessories 1",
|
||||
},
|
||||
},
|
||||
$state: {
|
||||
store: 'appState',
|
||||
},
|
||||
},
|
||||
{
|
||||
meta: {
|
||||
type: 'OR',
|
||||
params: [
|
||||
{
|
||||
meta: {
|
||||
index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
|
||||
alias: null,
|
||||
negate: false,
|
||||
disabled: false,
|
||||
type: 'phrase',
|
||||
key: 'category.keyword',
|
||||
params: {
|
||||
query: "Men's Accessories 2",
|
||||
},
|
||||
},
|
||||
query: {
|
||||
match_phrase: {
|
||||
'category.keyword': "Men's Accessories 2",
|
||||
},
|
||||
},
|
||||
$state: {
|
||||
store: 'appState',
|
||||
},
|
||||
},
|
||||
{
|
||||
meta: {
|
||||
index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
|
||||
alias: null,
|
||||
negate: false,
|
||||
disabled: false,
|
||||
type: 'phrase',
|
||||
key: 'category.keyword',
|
||||
params: {
|
||||
query: "Men's Accessories 3",
|
||||
},
|
||||
},
|
||||
query: {
|
||||
match_phrase: {
|
||||
'category.keyword': "Men's Accessories 3",
|
||||
},
|
||||
},
|
||||
$state: {
|
||||
store: 'appState',
|
||||
},
|
||||
},
|
||||
{
|
||||
meta: {
|
||||
index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
|
||||
alias: null,
|
||||
negate: false,
|
||||
disabled: false,
|
||||
type: 'phrase',
|
||||
key: 'category.keyword',
|
||||
params: {
|
||||
query: "Men's Accessories 5",
|
||||
},
|
||||
},
|
||||
query: {
|
||||
match_phrase: {
|
||||
'category.keyword': "Men's Accessories 5",
|
||||
},
|
||||
},
|
||||
$state: {
|
||||
store: 'appState',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
meta: {
|
||||
index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
|
||||
alias: null,
|
||||
negate: false,
|
||||
disabled: false,
|
||||
type: 'phrase',
|
||||
key: 'category.keyword',
|
||||
params: {
|
||||
query: "Men's Accessories 6",
|
||||
},
|
||||
},
|
||||
query: {
|
||||
match_phrase: {
|
||||
'category.keyword': "Men's Accessories 6",
|
||||
},
|
||||
},
|
||||
$state: {
|
||||
store: 'appState',
|
||||
},
|
||||
},
|
||||
] as Filter[];
|
||||
|
||||
export const getDataThatNeedNotNormalized = () =>
|
||||
[
|
||||
{
|
||||
meta: {
|
||||
type: 'OR',
|
||||
params: [
|
||||
{
|
||||
meta: {
|
||||
index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
|
||||
alias: null,
|
||||
negate: false,
|
||||
disabled: false,
|
||||
type: 'phrase',
|
||||
key: 'category.keyword',
|
||||
params: {
|
||||
query: "Men's Accessories 2",
|
||||
},
|
||||
},
|
||||
query: {
|
||||
match_phrase: {
|
||||
'category.keyword': "Men's Accessories 2",
|
||||
},
|
||||
},
|
||||
$state: {
|
||||
store: 'appState',
|
||||
},
|
||||
},
|
||||
[
|
||||
{
|
||||
meta: {
|
||||
index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
|
||||
alias: null,
|
||||
negate: false,
|
||||
disabled: false,
|
||||
type: 'phrase',
|
||||
key: 'category.keyword',
|
||||
params: {
|
||||
query: "Men's Accessories 3",
|
||||
},
|
||||
},
|
||||
query: {
|
||||
match_phrase: {
|
||||
'category.keyword': "Men's Accessories 3",
|
||||
},
|
||||
},
|
||||
$state: {
|
||||
store: 'appState',
|
||||
},
|
||||
},
|
||||
{
|
||||
meta: {
|
||||
index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
|
||||
alias: null,
|
||||
negate: false,
|
||||
disabled: false,
|
||||
type: 'phrase',
|
||||
key: 'category.keyword',
|
||||
params: {
|
||||
query: "Men's Accessories 4",
|
||||
},
|
||||
},
|
||||
query: {
|
||||
match_phrase: {
|
||||
'category.keyword': "Men's Accessories 4",
|
||||
},
|
||||
},
|
||||
$state: {
|
||||
store: 'appState',
|
||||
},
|
||||
},
|
||||
],
|
||||
{
|
||||
meta: {
|
||||
index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
|
||||
alias: null,
|
||||
negate: false,
|
||||
disabled: false,
|
||||
type: 'phrase',
|
||||
key: 'category.keyword',
|
||||
params: {
|
||||
query: "Men's Accessories 5",
|
||||
},
|
||||
},
|
||||
query: {
|
||||
match_phrase: {
|
||||
'category.keyword': "Men's Accessories 5",
|
||||
},
|
||||
},
|
||||
$state: {
|
||||
store: 'appState',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
meta: {
|
||||
index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
|
||||
alias: null,
|
||||
negate: false,
|
||||
disabled: false,
|
||||
type: 'phrase',
|
||||
key: 'category.keyword',
|
||||
params: {
|
||||
query: "Men's Accessories 6",
|
||||
},
|
||||
},
|
||||
query: {
|
||||
match_phrase: {
|
||||
'category.keyword': "Men's Accessories 6",
|
||||
},
|
||||
},
|
||||
$state: {
|
||||
store: 'appState',
|
||||
},
|
||||
},
|
||||
] as Filter[];
|
|
@ -0,0 +1,188 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import { ComponentStory } from '@storybook/react';
|
||||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
import { EuiForm } from '@elastic/eui';
|
||||
import type { DataView } from '@kbn/data-views-plugin/common';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
import { getFiltersMock, getFiltersMockOrHide } from '../__mock__/filters';
|
||||
import FiltersBuilder, { FiltersBuilderProps } from '../filters_builder';
|
||||
|
||||
export default {
|
||||
title: 'Filters Builder',
|
||||
component: FiltersBuilder,
|
||||
decorators: [(story: Function) => <EuiForm>{story()}</EuiForm>],
|
||||
};
|
||||
|
||||
const Template: ComponentStory<FC<FiltersBuilderProps>> = (args) => <FiltersBuilder {...args} />;
|
||||
|
||||
export const Default = Template.bind({});
|
||||
|
||||
Default.decorators = [
|
||||
(Story) => (
|
||||
<I18nProvider>
|
||||
<KibanaContextProvider services={services}>
|
||||
<Story />
|
||||
</KibanaContextProvider>
|
||||
</I18nProvider>
|
||||
),
|
||||
];
|
||||
|
||||
const mockedDataView = {
|
||||
id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
|
||||
title: 'logstash-*',
|
||||
fields: [
|
||||
{
|
||||
name: 'category.keyword',
|
||||
type: 'string',
|
||||
esTypes: ['integer'],
|
||||
aggregatable: true,
|
||||
filterable: true,
|
||||
searchable: true,
|
||||
},
|
||||
],
|
||||
} as DataView;
|
||||
|
||||
const filters = getFiltersMock();
|
||||
|
||||
Default.args = {
|
||||
filters,
|
||||
dataView: mockedDataView,
|
||||
onChange: (f: Filter[]) => {},
|
||||
hideOr: false,
|
||||
};
|
||||
|
||||
export const withoutOR = Template.bind({});
|
||||
withoutOR.args = { ...Default.args, filters: getFiltersMockOrHide(), hideOr: true };
|
||||
|
||||
withoutOR.decorators = [
|
||||
(Story) => (
|
||||
<I18nProvider>
|
||||
<KibanaContextProvider services={services}>
|
||||
<Story />
|
||||
</KibanaContextProvider>
|
||||
</I18nProvider>
|
||||
),
|
||||
];
|
||||
|
||||
const createMockWebStorage = () => ({
|
||||
clear: action('clear'),
|
||||
getItem: action('getItem'),
|
||||
key: action('key'),
|
||||
removeItem: action('removeItem'),
|
||||
setItem: action('setItem'),
|
||||
length: 0,
|
||||
});
|
||||
|
||||
const createMockStorage = () => ({
|
||||
storage: createMockWebStorage(),
|
||||
set: action('set'),
|
||||
remove: action('remove'),
|
||||
clear: action('clear'),
|
||||
get: () => true,
|
||||
});
|
||||
|
||||
const services = {
|
||||
uiSettings: {
|
||||
get: () => true,
|
||||
},
|
||||
savedObjects: action('savedObjects'),
|
||||
notifications: action('notifications'),
|
||||
http: {
|
||||
basePath: {
|
||||
prepend: () => 'http://test',
|
||||
},
|
||||
},
|
||||
docLinks: {
|
||||
links: {
|
||||
query: {
|
||||
kueryQuerySyntax: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
storage: createMockStorage(),
|
||||
data: {
|
||||
query: {
|
||||
savedQueries: {
|
||||
findSavedQueries: () =>
|
||||
Promise.resolve({
|
||||
queries: [
|
||||
{
|
||||
id: 'testwewe',
|
||||
attributes: {
|
||||
title: 'Saved query 1',
|
||||
description: '',
|
||||
query: {
|
||||
query: 'category.keyword : "Men\'s Shoes" ',
|
||||
language: 'kuery',
|
||||
},
|
||||
filters: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '0173d0d0-b19a-11ec-8323-837d6b231b82',
|
||||
attributes: {
|
||||
title: 'test',
|
||||
description: '',
|
||||
query: {
|
||||
query: '',
|
||||
language: 'kuery',
|
||||
},
|
||||
filters: [
|
||||
{
|
||||
meta: {
|
||||
index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
|
||||
alias: null,
|
||||
negate: false,
|
||||
disabled: false,
|
||||
type: 'phrase',
|
||||
key: 'category.keyword',
|
||||
params: {
|
||||
query: "Men's Accessories",
|
||||
},
|
||||
},
|
||||
query: {
|
||||
match_phrase: {
|
||||
'category.keyword': "Men's Accessories",
|
||||
},
|
||||
},
|
||||
$state: {
|
||||
store: 'appState',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
dataViews: {
|
||||
getIdsWithTitle: () => [
|
||||
{ id: '8a0b7cd0-b0c4-11ec-92b2-73d62e0d28a9', title: 'logstash-*' },
|
||||
{ id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', title: 'test-*' },
|
||||
],
|
||||
},
|
||||
},
|
||||
unifiedSearch: {
|
||||
autocomplete: {
|
||||
hasQuerySuggestions: () => Promise.resolve(false),
|
||||
getQuerySuggestions: () => [],
|
||||
getValueSuggestions: () =>
|
||||
new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve([]);
|
||||
}, 300);
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
|
@ -0,0 +1,33 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="27px" height="40px" viewBox="0 0 27 40" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>add</title>
|
||||
<defs>
|
||||
<filter x="-28.4%" y="-17.1%" width="156.8%" height="134.3%" filterUnits="objectBoundingBox" id="filter-1">
|
||||
<feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
|
||||
<feGaussianBlur stdDeviation="0.9" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.65 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
|
||||
<feMerge>
|
||||
<feMergeNode in="shadowMatrixOuter1"></feMergeNode>
|
||||
<feMergeNode in="SourceGraphic"></feMergeNode>
|
||||
</feMerge>
|
||||
</filter>
|
||||
<linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-2">
|
||||
<stop stop-color="#5CD331" offset="0%"></stop>
|
||||
<stop stop-color="#078C04" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="add" filter="url(#filter-1)" transform="translate(4.000000, 4.500000)">
|
||||
<g id="circle" transform="translate(1.000000, 13.500000)" fill="url(#linearGradient-2)">
|
||||
<path d="M0,9 C0,13.97 4.03,18 9,18 L9,18 C13.97,18 18,13.97 18,9 L18,9 C18,4.029 13.97,0 9,0 L9,0 C4.03,0 0,4.029 0,9"></path>
|
||||
</g>
|
||||
<g id="arrow">
|
||||
<polygon id="arrow-border" fill="#FFFFFF" points="0 16 0 0 11.6000004 11.6081174 4.55353306 11.6081174 4.40241581 11.7320012"></polygon>
|
||||
<polygon fill="#000000" points="1 2.29999995 1 13.4999998 3.969 10.6309258 4.129 10.4917767 9.165 10.5"></polygon>
|
||||
</g>
|
||||
<g transform="translate(5.000000, 17.500000)" fill="#FFFFFF" fill-rule="nonzero" id="Path">
|
||||
<path d="M5.25,0 C5.52614237,0 5.75,0.223857625 5.75,0.5 L5.75,4.25 L9.5,4.25 C9.77614237,4.25 10,4.47385763 10,4.75 L10,5.25 C10,5.52614237 9.77614237,5.75 9.5,5.75 L5.75,5.75 L5.75,9.5 C5.75,9.77614237 5.52614237,10 5.25,10 L4.75,10 C4.47385763,10 4.25,9.77614237 4.25,9.5 L4.25,5.75 L0.5,5.75 C0.223857625,5.75 0,5.52614237 0,5.25 L0,4.75 C0,4.47385763 0.223857625,4.25 0.5,4.25 L4.25,4.25 L4.25,0.5 C4.25,0.223857625 4.47385763,0 4.75,0 L5.25,0 Z"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.5 KiB |
|
@ -0,0 +1,33 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="27px" height="40px" viewBox="0 0 27 40" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>or</title>
|
||||
<defs>
|
||||
<filter x="-28.4%" y="-16.9%" width="156.8%" height="133.8%" filterUnits="objectBoundingBox" id="filter-1">
|
||||
<feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
|
||||
<feGaussianBlur stdDeviation="0.9" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.65 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
|
||||
<feMerge>
|
||||
<feMergeNode in="shadowMatrixOuter1"></feMergeNode>
|
||||
<feMergeNode in="SourceGraphic"></feMergeNode>
|
||||
</feMerge>
|
||||
</filter>
|
||||
<linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-2">
|
||||
<stop stop-color="#5CD331" offset="0%"></stop>
|
||||
<stop stop-color="#078C04" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="or" filter="url(#filter-1)" transform="translate(4.000000, 4.000000)">
|
||||
<g id="circle" transform="translate(1.000000, 14.000000)">
|
||||
<path d="M0,9 C0,13.97 4.03,18 9,18 L9,18 C13.97,18 18,13.97 18,9 L18,9 C18,4.029 13.97,0 9,0 L9,0 C4.03,0 0,4.029 0,9" fill="url(#linearGradient-2)"></path>
|
||||
<g id="or" transform="translate(4.000000, 5.000000)" fill="#FFFFFF" fill-rule="nonzero" stroke="#FFFFFF" stroke-width="0.5">
|
||||
<path d="M8.32844896,0 C9.21088789,0 9.93167133,0.681619492 9.99500028,1.54655963 L10,1.67155098 L10,3.32810299 C10.0002373,4.20282328 9.32574192,4.9296502 8.45344031,4.99465432 L8.33344861,4.99965397 L1.23727308,4.99965397 L2.82882959,6.51621568 C2.91929037,6.59834359 2.97087359,6.71484602 2.97087359,6.83702681 C2.97087359,6.9592076 2.91929037,7.07571003 2.82882959,7.15783794 C2.6330138,7.33595713 2.33385985,7.33595713 2.13804407,7.15783794 L0.429828959,5.5329504 C0.156296125,5.2877964 0,4.93783253 0,4.57051701 C0,4.20320149 0.156296125,3.85323761 0.429828959,3.60808362 L2.13804407,1.98319608 C2.33413487,1.80496239 2.63357206,1.80496239 2.82966287,1.98319608 C2.92012364,2.06532398 2.97170686,2.18182641 2.97170686,2.30400721 C2.97170686,2.426188 2.92012364,2.54269043 2.82966287,2.62481834 L1.2089417,4.16637831 L8.33344861,4.16637831 C8.7617523,4.16387848 9.11256136,3.8414008 9.16089135,3.42642952 L9.16672428,3.32810299 L9.16672428,1.67155098 C9.16672428,1.24158074 8.84341332,0.88743858 8.42677549,0.838275316 L8.32844896,0.833275662 L6.25025946,0.833275662 C6.03445016,0.833672946 5.85405825,0.66921065 5.83455883,0.454283726 C5.81505942,0.239356801 5.96290758,0.0451146606 6.17526465,0.0066662053 L6.25025946,0 L8.32844896,0 Z" id="Path"></path>
|
||||
</g>
|
||||
</g>
|
||||
<g id="arrow">
|
||||
<polygon id="arrow-border" fill="#FFFFFF" points="0 16.5 0 0.5 11.6000004 12.1081174 4.55353306 12.1081174 4.40241581 12.2320012"></polygon>
|
||||
<polygon fill="#000000" points="1 2.79999995 1 13.9999998 3.969 11.1309258 4.129 10.9917767 9.165 11"></polygon>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 3.3 KiB |
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useReducer, useCallback, useState, useMemo } from 'react';
|
||||
import { EuiDragDropContext, DragDropContextProps, useEuiPaddingSize } from '@elastic/eui';
|
||||
import type { DataView } from '@kbn/data-views-plugin/common';
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
import { css } from '@emotion/css';
|
||||
import { FiltersBuilderContextType } from './filters_builder_context';
|
||||
import { ConditionTypes } from '../utils';
|
||||
import { FilterGroup } from './filters_builder_filter_group';
|
||||
import { FiltersBuilderReducer } from './filters_builder_reducer';
|
||||
|
||||
export interface FiltersBuilderProps {
|
||||
filters: Filter[];
|
||||
dataView: DataView;
|
||||
onChange: (filters: Filter[]) => void;
|
||||
timeRangeForSuggestionsOverride?: boolean;
|
||||
maxDepth?: number;
|
||||
hideOr?: boolean;
|
||||
}
|
||||
|
||||
const rootLevelConditionType = ConditionTypes.AND;
|
||||
const DEFAULT_MAX_DEPTH = 10;
|
||||
|
||||
function FiltersBuilder({
|
||||
onChange,
|
||||
dataView,
|
||||
filters,
|
||||
timeRangeForSuggestionsOverride,
|
||||
maxDepth = DEFAULT_MAX_DEPTH,
|
||||
hideOr = false,
|
||||
}: FiltersBuilderProps) {
|
||||
const [state, dispatch] = useReducer(FiltersBuilderReducer, { filters });
|
||||
const [dropTarget, setDropTarget] = useState('');
|
||||
const mPaddingSize = useEuiPaddingSize('m');
|
||||
|
||||
const filtersBuilderStyles = useMemo(
|
||||
() => css`
|
||||
.filter-builder__panel {
|
||||
&.filter-builder__panel-nested {
|
||||
padding: ${mPaddingSize} 0;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-builder__item {
|
||||
&.filter-builder__item-nested {
|
||||
padding: 0 ${mPaddingSize};
|
||||
}
|
||||
}
|
||||
`,
|
||||
[mPaddingSize]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (state.filters !== filters) {
|
||||
onChange(state.filters);
|
||||
}
|
||||
}, [filters, onChange, state.filters]);
|
||||
|
||||
const handleMoveFilter = useCallback(
|
||||
(pathFrom: string, pathTo: string, conditionalType: ConditionTypes) => {
|
||||
if (pathFrom === pathTo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: 'moveFilter',
|
||||
payload: {
|
||||
pathFrom,
|
||||
pathTo,
|
||||
conditionalType,
|
||||
},
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const onDragEnd: DragDropContextProps['onDragEnd'] = ({ combine, source, destination }) => {
|
||||
if (source && destination) {
|
||||
handleMoveFilter(source.droppableId, destination.droppableId, ConditionTypes.AND);
|
||||
}
|
||||
|
||||
if (source && combine) {
|
||||
handleMoveFilter(source.droppableId, combine.droppableId, ConditionTypes.OR);
|
||||
}
|
||||
setDropTarget('');
|
||||
};
|
||||
|
||||
const onDragActive: DragDropContextProps['onDragUpdate'] = ({ destination, combine }) => {
|
||||
if (destination) {
|
||||
setDropTarget(destination.droppableId);
|
||||
}
|
||||
|
||||
if (combine) {
|
||||
setDropTarget(combine.droppableId);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={filtersBuilderStyles}>
|
||||
<FiltersBuilderContextType.Provider
|
||||
value={{
|
||||
globalParams: { hideOr, maxDepth },
|
||||
dataView,
|
||||
dispatch,
|
||||
dropTarget,
|
||||
timeRangeForSuggestionsOverride,
|
||||
}}
|
||||
>
|
||||
<EuiDragDropContext onDragEnd={onDragEnd} onDragUpdate={onDragActive}>
|
||||
<FilterGroup filters={state.filters} conditionType={rootLevelConditionType} path={''} />
|
||||
</EuiDragDropContext>
|
||||
</FiltersBuilderContextType.Provider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// React.lazy support
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default FiltersBuilder;
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { Dispatch } from 'react';
|
||||
import type { DataView } from '@kbn/data-views-plugin/common';
|
||||
import type { FiltersBuilderActions } from './filters_builder_reducer';
|
||||
|
||||
interface FiltersBuilderContextType {
|
||||
dataView: DataView;
|
||||
dispatch: Dispatch<FiltersBuilderActions>;
|
||||
globalParams: {
|
||||
maxDepth: number;
|
||||
hideOr: boolean;
|
||||
};
|
||||
dropTarget: string;
|
||||
timeRangeForSuggestionsOverride?: boolean;
|
||||
}
|
||||
|
||||
export const FiltersBuilderContextType = React.createContext<FiltersBuilderContextType>(
|
||||
{} as FiltersBuilderContextType
|
||||
);
|
|
@ -0,0 +1,149 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useContext, useMemo } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiHorizontalRule,
|
||||
EuiPanel,
|
||||
EuiText,
|
||||
useEuiBackgroundColor,
|
||||
useEuiPaddingSize,
|
||||
} from '@elastic/eui';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import { css, cx } from '@emotion/css';
|
||||
import type { Path } from './filters_builder_types';
|
||||
import { ConditionTypes, getConditionalOperationType } from '../utils';
|
||||
import { FilterItem } from './filters_builder_filter_item';
|
||||
import { FiltersBuilderContextType } from './filters_builder_context';
|
||||
import { getPathInArray } from './filters_builder_utils';
|
||||
|
||||
export interface FilterGroupProps {
|
||||
filters: Filter[];
|
||||
conditionType: ConditionTypes;
|
||||
path: Path;
|
||||
|
||||
/** @internal used for recursive rendering **/
|
||||
renderedLevel?: number;
|
||||
reverseBackground?: boolean;
|
||||
}
|
||||
|
||||
/** @internal **/
|
||||
const Delimiter = ({
|
||||
color,
|
||||
conditionType,
|
||||
}: {
|
||||
color: 'subdued' | 'plain';
|
||||
conditionType: ConditionTypes;
|
||||
}) => {
|
||||
const xsPadding = useEuiPaddingSize('xs');
|
||||
const mPadding = useEuiPaddingSize('m');
|
||||
const backgroundColor = useEuiBackgroundColor(color);
|
||||
|
||||
const delimiterStyles = useMemo(
|
||||
() => css`
|
||||
position: relative;
|
||||
|
||||
.filter-builder__delimiter_text {
|
||||
position: absolute;
|
||||
display: block;
|
||||
padding: ${xsPadding};
|
||||
top: 0;
|
||||
left: ${mPadding};
|
||||
background: ${backgroundColor};
|
||||
}
|
||||
`,
|
||||
[backgroundColor, mPadding, xsPadding]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={delimiterStyles}>
|
||||
<EuiHorizontalRule margin="s" />
|
||||
<EuiText size="xs" className="filter-builder__delimiter_text">
|
||||
{i18n.translate('unifiedSearch.filter.filtersBuilder.delimiterLabel', {
|
||||
defaultMessage: '{conditionType}',
|
||||
values: {
|
||||
conditionType,
|
||||
},
|
||||
})}
|
||||
</EuiText>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const FilterGroup = ({
|
||||
filters,
|
||||
conditionType,
|
||||
path,
|
||||
reverseBackground = false,
|
||||
renderedLevel = 0,
|
||||
}: FilterGroupProps) => {
|
||||
const {
|
||||
globalParams: { maxDepth, hideOr },
|
||||
} = useContext(FiltersBuilderContextType);
|
||||
|
||||
const pathInArray = getPathInArray(path);
|
||||
const isDepthReached = maxDepth <= pathInArray.length;
|
||||
const orDisabled = hideOr || (isDepthReached && conditionType === ConditionTypes.AND);
|
||||
const andDisabled = isDepthReached && conditionType === ConditionTypes.OR;
|
||||
const removeDisabled = pathInArray.length <= 1 && filters.length === 1;
|
||||
const shouldNormalizeFirstLevel =
|
||||
!path && filters.length === 1 && getConditionalOperationType(filters[0]);
|
||||
|
||||
if (shouldNormalizeFirstLevel) {
|
||||
reverseBackground = true;
|
||||
renderedLevel -= 1;
|
||||
}
|
||||
|
||||
const color = reverseBackground ? 'plain' : 'subdued';
|
||||
|
||||
const renderedFilters = filters.map((filter, index, acc) => (
|
||||
<EuiFlexGroup direction="column" gutterSize="xs">
|
||||
<EuiFlexItem>
|
||||
<FilterItem
|
||||
filter={filter}
|
||||
path={`${path}${path ? '.' : ''}${index}`}
|
||||
reverseBackground={reverseBackground}
|
||||
disableOr={orDisabled}
|
||||
disableAnd={andDisabled}
|
||||
disableRemove={removeDisabled}
|
||||
color={color}
|
||||
index={index}
|
||||
renderedLevel={renderedLevel}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
{conditionType && index + 1 < acc.length ? (
|
||||
<EuiFlexItem>
|
||||
{conditionType === ConditionTypes.OR && (
|
||||
<Delimiter color={color} conditionType={conditionType} />
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
</EuiFlexGroup>
|
||||
));
|
||||
|
||||
return shouldNormalizeFirstLevel ? (
|
||||
<>{renderedFilters}</>
|
||||
) : (
|
||||
<EuiPanel
|
||||
color={color}
|
||||
hasShadow={false}
|
||||
paddingSize="none"
|
||||
hasBorder
|
||||
className={cx({
|
||||
'filter-builder__panel': true,
|
||||
'filter-builder__panel-nested': renderedLevel > 0,
|
||||
})}
|
||||
>
|
||||
{renderedFilters}
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,317 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useContext, useMemo } from 'react';
|
||||
import {
|
||||
EuiButtonIcon,
|
||||
EuiDraggable,
|
||||
EuiDroppable,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormRow,
|
||||
EuiIcon,
|
||||
EuiPanel,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import { buildEmptyFilter, FieldFilter, Filter, getFilterParams } from '@kbn/es-query';
|
||||
import { DataViewField } from '@kbn/data-views-plugin/common';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { cx, css } from '@emotion/css';
|
||||
|
||||
import add from '../assets/add.svg';
|
||||
import or from '../assets/or.svg';
|
||||
|
||||
import { FieldInput } from './filters_builder_filter_item_field_input';
|
||||
import { OperatorInput } from './filters_builder_filter_item_operator_input';
|
||||
import { ParamsEditor } from './filters_builder_filter_item_params_editor';
|
||||
import { ConditionTypes, getConditionalOperationType } from '../../utils';
|
||||
import { FiltersBuilderContextType } from '../filters_builder_context';
|
||||
import { FilterGroup } from '../filters_builder_filter_group';
|
||||
import type { Path } from '../filters_builder_types';
|
||||
import { getFieldFromFilter, getOperatorFromFilter } from '../../filter_bar/filter_editor';
|
||||
import { Operator } from '../../filter_bar/filter_editor';
|
||||
|
||||
export interface FilterItemProps {
|
||||
path: Path;
|
||||
filter: Filter;
|
||||
disableOr: boolean;
|
||||
disableAnd: boolean;
|
||||
disableRemove: boolean;
|
||||
color: 'plain' | 'subdued';
|
||||
index: number;
|
||||
|
||||
/** @internal used for recursive rendering **/
|
||||
renderedLevel: number;
|
||||
reverseBackground: boolean;
|
||||
}
|
||||
|
||||
const cursorAddStyles = css`
|
||||
cursor: url(${add}), auto;
|
||||
`;
|
||||
|
||||
const cursorOrStyles = css`
|
||||
cursor: url(${or}), auto;
|
||||
`;
|
||||
|
||||
export function FilterItem({
|
||||
filter,
|
||||
path,
|
||||
reverseBackground,
|
||||
disableOr,
|
||||
disableAnd,
|
||||
disableRemove,
|
||||
color,
|
||||
index,
|
||||
renderedLevel,
|
||||
}: FilterItemProps) {
|
||||
const {
|
||||
dispatch,
|
||||
dataView,
|
||||
dropTarget,
|
||||
globalParams: { hideOr },
|
||||
timeRangeForSuggestionsOverride,
|
||||
} = useContext(FiltersBuilderContextType);
|
||||
const conditionalOperationType = getConditionalOperationType(filter);
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const grabIconStyles = useMemo(
|
||||
() => css`
|
||||
margin: 0 ${euiTheme.size.xxs};
|
||||
`,
|
||||
[euiTheme.size.xxs]
|
||||
);
|
||||
|
||||
let field: DataViewField | undefined;
|
||||
let operator: Operator | undefined;
|
||||
let params: Filter['meta']['params'] | undefined;
|
||||
|
||||
if (!conditionalOperationType) {
|
||||
field = getFieldFromFilter(filter as FieldFilter, dataView);
|
||||
operator = getOperatorFromFilter(filter);
|
||||
params = getFilterParams(filter);
|
||||
}
|
||||
|
||||
const onHandleField = useCallback(
|
||||
(selectedField: DataViewField) => {
|
||||
dispatch({
|
||||
type: 'updateFilter',
|
||||
payload: { path, field: selectedField },
|
||||
});
|
||||
},
|
||||
[dispatch, path]
|
||||
);
|
||||
|
||||
const onHandleOperator = useCallback(
|
||||
(selectedOperator: Operator) => {
|
||||
dispatch({
|
||||
type: 'updateFilter',
|
||||
payload: { path, field, operator: selectedOperator },
|
||||
});
|
||||
},
|
||||
[dispatch, path, field]
|
||||
);
|
||||
|
||||
const onHandleParamsChange = useCallback(
|
||||
(selectedParams: string) => {
|
||||
dispatch({
|
||||
type: 'updateFilter',
|
||||
payload: { path, field, operator, params: selectedParams },
|
||||
});
|
||||
},
|
||||
[dispatch, path, field, operator]
|
||||
);
|
||||
|
||||
const onHandleParamsUpdate = useCallback(
|
||||
(value: Filter['meta']['params']) => {
|
||||
dispatch({
|
||||
type: 'updateFilter',
|
||||
payload: { path, params: [value, ...(params || [])] },
|
||||
});
|
||||
},
|
||||
[dispatch, path, params]
|
||||
);
|
||||
|
||||
const onRemoveFilter = useCallback(() => {
|
||||
dispatch({
|
||||
type: 'removeFilter',
|
||||
payload: {
|
||||
path,
|
||||
},
|
||||
});
|
||||
}, [dispatch, path]);
|
||||
|
||||
const onAddFilter = useCallback(
|
||||
(conditionalType: ConditionTypes) => {
|
||||
dispatch({
|
||||
type: 'addFilter',
|
||||
payload: {
|
||||
path,
|
||||
filter: buildEmptyFilter(false, dataView.id),
|
||||
conditionalType,
|
||||
},
|
||||
});
|
||||
},
|
||||
[dispatch, dataView.id, path]
|
||||
);
|
||||
|
||||
const onAddButtonClick = useCallback(() => onAddFilter(ConditionTypes.AND), [onAddFilter]);
|
||||
const onOrButtonClick = useCallback(() => onAddFilter(ConditionTypes.OR), [onAddFilter]);
|
||||
|
||||
if (!dataView) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx({
|
||||
'filter-builder__item': true,
|
||||
'filter-builder__item-nested': renderedLevel > 0,
|
||||
})}
|
||||
>
|
||||
{conditionalOperationType ? (
|
||||
<FilterGroup
|
||||
path={path}
|
||||
conditionType={conditionalOperationType}
|
||||
filters={Array.isArray(filter) ? filter : filter.meta?.params}
|
||||
reverseBackground={!reverseBackground}
|
||||
renderedLevel={renderedLevel + 1}
|
||||
/>
|
||||
) : (
|
||||
<EuiDroppable
|
||||
droppableId={path}
|
||||
spacing="none"
|
||||
isCombineEnabled={!disableOr || !hideOr}
|
||||
className={cx({ [cursorAddStyles]: dropTarget === path })}
|
||||
isDropDisabled={disableAnd}
|
||||
>
|
||||
<EuiDraggable
|
||||
spacing="s"
|
||||
key={JSON.stringify(filter)}
|
||||
index={index}
|
||||
draggableId={`${path}`}
|
||||
customDragHandle={true}
|
||||
hasInteractiveChildren={true}
|
||||
>
|
||||
{(provided) => (
|
||||
<EuiFlexGroup
|
||||
gutterSize="xs"
|
||||
responsive={false}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<EuiPanel color={color} paddingSize={'none'} hasShadow={false}>
|
||||
<EuiFlexGroup
|
||||
responsive={false}
|
||||
alignItems="baseline"
|
||||
gutterSize="s"
|
||||
justifyContent="center"
|
||||
className={cx({
|
||||
[cursorOrStyles]: dropTarget === path && !hideOr,
|
||||
})}
|
||||
>
|
||||
<EuiFlexItem grow={false} {...provided.dragHandleProps}>
|
||||
<EuiIcon type="grab" size="s" className={grabIconStyles} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={true}>
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center" justifyContent="center">
|
||||
<EuiFlexItem grow={4}>
|
||||
<EuiFormRow fullWidth>
|
||||
<FieldInput
|
||||
field={field}
|
||||
dataView={dataView}
|
||||
onHandleField={onHandleField}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={2}>
|
||||
<EuiFormRow fullWidth>
|
||||
<OperatorInput
|
||||
field={field}
|
||||
operator={operator}
|
||||
params={params}
|
||||
onHandleOperator={onHandleOperator}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={4}>
|
||||
<EuiFormRow fullWidth>
|
||||
<ParamsEditor
|
||||
dataView={dataView}
|
||||
field={field}
|
||||
operator={operator}
|
||||
params={params}
|
||||
onHandleParamsChange={onHandleParamsChange}
|
||||
onHandleParamsUpdate={onHandleParamsUpdate}
|
||||
timeRangeForSuggestionsOverride={timeRangeForSuggestionsOverride}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup justifyContent="flexEnd" alignItems="flexEnd" gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
onClick={onRemoveFilter}
|
||||
iconType="trash"
|
||||
isDisabled={disableRemove}
|
||||
size="s"
|
||||
color="danger"
|
||||
aria-label={i18n.translate(
|
||||
'unifiedSearch.filter.filtersBuilder.deleteFilterGroupButtonIcon',
|
||||
{
|
||||
defaultMessage: 'Delete filter group',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
{!hideOr ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
onClick={onOrButtonClick}
|
||||
isDisabled={disableOr}
|
||||
iconType="returnKey"
|
||||
size="s"
|
||||
aria-label={i18n.translate(
|
||||
'unifiedSearch.filter.filtersBuilder.addOrFilterGroupButtonIcon',
|
||||
{
|
||||
defaultMessage: 'Add filter group with OR',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
display="base"
|
||||
onClick={onAddButtonClick}
|
||||
isDisabled={disableAnd}
|
||||
iconType="plus"
|
||||
size="s"
|
||||
aria-label={i18n.translate(
|
||||
'unifiedSearch.filter.filtersBuilder.addAndFilterGroupButtonIcon',
|
||||
{
|
||||
defaultMessage: 'Add filter group with AND',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
</EuiDraggable>
|
||||
</EuiDroppable>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { DataView, DataViewField } from '@kbn/data-views-plugin/common';
|
||||
import { useGeneratedHtmlId } from '@elastic/eui';
|
||||
import { getFilterableFields, GenericComboBox } from '../../filter_bar/filter_editor';
|
||||
|
||||
interface FieldInputProps {
|
||||
dataView: DataView;
|
||||
onHandleField: (field: DataViewField) => void;
|
||||
field?: DataViewField;
|
||||
}
|
||||
|
||||
export function FieldInput({ field, dataView, onHandleField }: FieldInputProps) {
|
||||
const fields = dataView ? getFilterableFields(dataView) : [];
|
||||
const id = useGeneratedHtmlId({ prefix: 'fieldInput' });
|
||||
|
||||
const onFieldChange = useCallback(
|
||||
([selectedField]: DataViewField[]) => {
|
||||
onHandleField(selectedField);
|
||||
},
|
||||
[onHandleField]
|
||||
);
|
||||
|
||||
const getLabel = useCallback((view: DataViewField) => view.customLabel || view.name, []);
|
||||
|
||||
return (
|
||||
<GenericComboBox
|
||||
id={id}
|
||||
isDisabled={!dataView}
|
||||
placeholder={i18n.translate('unifiedSearch.filter.filtersBuilder.fieldSelectPlaceholder', {
|
||||
defaultMessage: 'Select a field first',
|
||||
})}
|
||||
options={fields}
|
||||
selectedOptions={field ? [field] : []}
|
||||
getLabel={getLabel}
|
||||
onChange={onFieldChange}
|
||||
singleSelection={{ asPlainText: true }}
|
||||
isClearable={false}
|
||||
compressed
|
||||
fullWidth
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { DataViewField } from '@kbn/data-views-plugin/common';
|
||||
import type { Operator } from '../../filter_bar/filter_editor';
|
||||
import { getOperatorOptions, GenericComboBox } from '../../filter_bar/filter_editor';
|
||||
|
||||
interface OperatorInputProps<TParams = unknown> {
|
||||
field: DataViewField | undefined;
|
||||
operator: Operator | undefined;
|
||||
params: TParams;
|
||||
onHandleOperator: (operator: Operator, params?: TParams) => void;
|
||||
}
|
||||
|
||||
export function OperatorInput<TParams = unknown>({
|
||||
field,
|
||||
operator,
|
||||
params,
|
||||
onHandleOperator,
|
||||
}: OperatorInputProps<TParams>) {
|
||||
const operators = field ? getOperatorOptions(field) : [];
|
||||
|
||||
const onOperatorChange = useCallback(
|
||||
([selectedOperator]: Operator[]) => {
|
||||
const selectedParams = selectedOperator === operator ? params : undefined;
|
||||
|
||||
onHandleOperator(selectedOperator, selectedParams);
|
||||
},
|
||||
[onHandleOperator, operator, params]
|
||||
);
|
||||
|
||||
return (
|
||||
<GenericComboBox
|
||||
fullWidth
|
||||
compressed
|
||||
isDisabled={!field}
|
||||
placeholder={
|
||||
field
|
||||
? i18n.translate('unifiedSearch.filter.filtersBuilder.operatorSelectPlaceholderSelect', {
|
||||
defaultMessage: 'Select',
|
||||
})
|
||||
: i18n.translate('unifiedSearch.filter.filtersBuilder.operatorSelectPlaceholderWaiting', {
|
||||
defaultMessage: 'Waiting',
|
||||
})
|
||||
}
|
||||
options={operators}
|
||||
selectedOptions={operator ? [operator] : []}
|
||||
getLabel={({ message }) => message}
|
||||
onChange={onOperatorChange}
|
||||
singleSelection={{ asPlainText: true }}
|
||||
isClearable={false}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import { DataView, DataViewField } from '@kbn/data-views-plugin/common';
|
||||
import { EuiFormRow } from '@elastic/eui';
|
||||
import type { Operator } from '../../filter_bar/filter_editor';
|
||||
import {
|
||||
PhraseValueInput,
|
||||
PhrasesValuesInput,
|
||||
RangeValueInput,
|
||||
isRangeParams,
|
||||
} from '../../filter_bar/filter_editor';
|
||||
import { getFieldValidityAndErrorMessage } from '../../filter_bar/filter_editor/lib';
|
||||
|
||||
interface ParamsEditorProps<TParams = unknown> {
|
||||
dataView: DataView;
|
||||
params: TParams;
|
||||
onHandleParamsChange: (params: TParams) => void;
|
||||
onHandleParamsUpdate: (value: TParams) => void;
|
||||
timeRangeForSuggestionsOverride?: boolean;
|
||||
field?: DataViewField;
|
||||
operator?: Operator;
|
||||
}
|
||||
|
||||
export function ParamsEditor<TParams = unknown>({
|
||||
dataView,
|
||||
field,
|
||||
operator,
|
||||
params,
|
||||
onHandleParamsChange,
|
||||
onHandleParamsUpdate,
|
||||
timeRangeForSuggestionsOverride,
|
||||
}: ParamsEditorProps<TParams>) {
|
||||
const onParamsChange = useCallback(
|
||||
(selectedParams) => {
|
||||
onHandleParamsChange(selectedParams);
|
||||
},
|
||||
[onHandleParamsChange]
|
||||
);
|
||||
|
||||
const onParamsUpdate = useCallback(
|
||||
(value) => {
|
||||
onHandleParamsUpdate(value);
|
||||
},
|
||||
[onHandleParamsUpdate]
|
||||
);
|
||||
|
||||
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(
|
||||
field!,
|
||||
typeof params === 'string' ? params : undefined
|
||||
);
|
||||
|
||||
switch (operator?.type) {
|
||||
case 'exists':
|
||||
return null;
|
||||
case 'phrase':
|
||||
return (
|
||||
<EuiFormRow fullWidth isInvalid={isInvalid} error={errorMessage}>
|
||||
<PhraseValueInput
|
||||
compressed
|
||||
indexPattern={dataView}
|
||||
field={field!}
|
||||
value={typeof params === 'string' ? params : undefined}
|
||||
onChange={onParamsChange}
|
||||
timeRangeForSuggestionsOverride={timeRangeForSuggestionsOverride}
|
||||
fullWidth
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
case 'phrases':
|
||||
return (
|
||||
<PhrasesValuesInput
|
||||
compressed
|
||||
indexPattern={dataView}
|
||||
field={field!}
|
||||
values={Array.isArray(params) ? params : undefined}
|
||||
onChange={onParamsChange}
|
||||
onParamsUpdate={onParamsUpdate}
|
||||
timeRangeForSuggestionsOverride={timeRangeForSuggestionsOverride}
|
||||
fullWidth
|
||||
/>
|
||||
);
|
||||
case 'range':
|
||||
return (
|
||||
<RangeValueInput
|
||||
compressed
|
||||
field={field!}
|
||||
value={isRangeParams(params) ? params : undefined}
|
||||
onChange={onParamsChange}
|
||||
fullWidth
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<PhraseValueInput
|
||||
disabled={!dataView || !operator}
|
||||
indexPattern={dataView}
|
||||
field={field!}
|
||||
value={typeof params === 'string' ? params : undefined}
|
||||
onChange={onParamsChange}
|
||||
timeRangeForSuggestionsOverride={timeRangeForSuggestionsOverride}
|
||||
fullWidth
|
||||
compressed
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export { FilterItem } from './filters_builder_filter_item';
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { Reducer } from 'react';
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
import type { DataViewField } from '@kbn/data-views-plugin/common';
|
||||
import type { Path } from './filters_builder_types';
|
||||
import type { ConditionTypes } from '../utils';
|
||||
import { addFilter, moveFilter, removeFilter, updateFilter } from './filters_builder_utils';
|
||||
import type { Operator } from '../filter_bar/filter_editor';
|
||||
|
||||
/** @internal **/
|
||||
export interface FiltersBuilderState {
|
||||
filters: Filter[];
|
||||
}
|
||||
|
||||
/** @internal **/
|
||||
export interface AddFilterPayload {
|
||||
path: Path;
|
||||
filter: Filter;
|
||||
conditionalType: ConditionTypes;
|
||||
}
|
||||
|
||||
/** @internal **/
|
||||
export interface UpdateFilterPayload {
|
||||
path: string;
|
||||
field?: DataViewField;
|
||||
operator?: Operator;
|
||||
params?: Filter['meta']['params'];
|
||||
}
|
||||
|
||||
/** @internal **/
|
||||
export interface RemoveFilterPayload {
|
||||
path: Path;
|
||||
}
|
||||
|
||||
/** @internal **/
|
||||
export interface MoveFilterPayload {
|
||||
pathFrom: Path;
|
||||
pathTo: Path;
|
||||
conditionalType: ConditionTypes;
|
||||
}
|
||||
|
||||
/** @internal **/
|
||||
export type FiltersBuilderActions =
|
||||
| { type: 'addFilter'; payload: AddFilterPayload }
|
||||
| { type: 'removeFilter'; payload: RemoveFilterPayload }
|
||||
| { type: 'moveFilter'; payload: MoveFilterPayload }
|
||||
| { type: 'updateFilter'; payload: UpdateFilterPayload };
|
||||
|
||||
export const FiltersBuilderReducer: Reducer<FiltersBuilderState, FiltersBuilderActions> = (
|
||||
state,
|
||||
action
|
||||
) => {
|
||||
switch (action.type) {
|
||||
case 'addFilter':
|
||||
return {
|
||||
filters: addFilter(
|
||||
state.filters,
|
||||
action.payload.filter,
|
||||
action.payload.path,
|
||||
action.payload.conditionalType
|
||||
),
|
||||
};
|
||||
case 'removeFilter':
|
||||
return {
|
||||
...state,
|
||||
filters: removeFilter(state.filters, action.payload.path),
|
||||
};
|
||||
case 'moveFilter':
|
||||
return {
|
||||
...state,
|
||||
filters: moveFilter(
|
||||
state.filters,
|
||||
action.payload.pathFrom,
|
||||
action.payload.pathTo,
|
||||
action.payload.conditionalType
|
||||
),
|
||||
};
|
||||
case 'updateFilter':
|
||||
return {
|
||||
...state,
|
||||
filters: updateFilter(
|
||||
state.filters,
|
||||
action.payload.path,
|
||||
action.payload.field,
|
||||
action.payload.operator,
|
||||
action.payload.params
|
||||
),
|
||||
};
|
||||
default:
|
||||
throw new Error('wrong action');
|
||||
}
|
||||
};
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
/** @internal **/
|
||||
export type Path = string;
|
|
@ -0,0 +1,261 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { buildEmptyFilter, Filter } from '@kbn/es-query';
|
||||
import { ConditionTypes } from '../utils';
|
||||
import {
|
||||
getFilterByPath,
|
||||
getPathInArray,
|
||||
addFilter,
|
||||
removeFilter,
|
||||
moveFilter,
|
||||
normalizeFilters,
|
||||
} from './filters_builder_utils';
|
||||
import type { FilterItem } from '../utils';
|
||||
import { getConditionalOperationType } from '../utils';
|
||||
|
||||
import {
|
||||
getDataAfterNormalized,
|
||||
getDataThatNeedNotNormalized,
|
||||
getDataThatNeedsNormalized,
|
||||
getFiltersMock,
|
||||
} from './__mock__/filters';
|
||||
|
||||
describe('filters_builder_utils', () => {
|
||||
let filters: Filter[];
|
||||
beforeAll(() => {
|
||||
filters = getFiltersMock();
|
||||
});
|
||||
|
||||
describe('getFilterByPath', () => {
|
||||
test('should return correct filterByPath', () => {
|
||||
expect(getFilterByPath(filters, '0')).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"$state": Object {
|
||||
"store": "appState",
|
||||
},
|
||||
"meta": Object {
|
||||
"alias": null,
|
||||
"disabled": false,
|
||||
"index": "ff959d40-b880-11e8-a6d9-e546fe2bba5f",
|
||||
"key": "category.keyword",
|
||||
"negate": false,
|
||||
"params": Object {
|
||||
"query": "Men's Accessories 1",
|
||||
},
|
||||
"type": "phrase",
|
||||
},
|
||||
"query": Object {
|
||||
"match_phrase": Object {
|
||||
"category.keyword": "Men's Accessories 1",
|
||||
},
|
||||
},
|
||||
}
|
||||
`);
|
||||
expect(getFilterByPath(filters, '2')).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"$state": Object {
|
||||
"store": "appState",
|
||||
},
|
||||
"meta": Object {
|
||||
"alias": null,
|
||||
"disabled": false,
|
||||
"index": "ff959d40-b880-11e8-a6d9-e546fe2bba5f",
|
||||
"key": "category.keyword",
|
||||
"negate": false,
|
||||
"params": Object {
|
||||
"query": "Men's Accessories 6",
|
||||
},
|
||||
"type": "phrase",
|
||||
},
|
||||
"query": Object {
|
||||
"match_phrase": Object {
|
||||
"category.keyword": "Men's Accessories 6",
|
||||
},
|
||||
},
|
||||
}
|
||||
`);
|
||||
expect(getFilterByPath(filters, '1.2')).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"$state": Object {
|
||||
"store": "appState",
|
||||
},
|
||||
"meta": Object {
|
||||
"alias": null,
|
||||
"disabled": false,
|
||||
"index": "ff959d40-b880-11e8-a6d9-e546fe2bba5f",
|
||||
"key": "category.keyword",
|
||||
"negate": false,
|
||||
"params": Object {
|
||||
"query": "Men's Accessories 5",
|
||||
},
|
||||
"type": "phrase",
|
||||
},
|
||||
"query": Object {
|
||||
"match_phrase": Object {
|
||||
"category.keyword": "Men's Accessories 5",
|
||||
},
|
||||
},
|
||||
}
|
||||
`);
|
||||
expect(getFilterByPath(filters, '1.1.1')).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"$state": Object {
|
||||
"store": "appState",
|
||||
},
|
||||
"meta": Object {
|
||||
"alias": null,
|
||||
"disabled": false,
|
||||
"index": "ff959d40-b880-11e8-a6d9-e546fe2bba5f",
|
||||
"key": "category.keyword",
|
||||
"negate": false,
|
||||
"params": Object {
|
||||
"query": "Men's Accessories 4",
|
||||
},
|
||||
"type": "phrase",
|
||||
},
|
||||
"query": Object {
|
||||
"match_phrase": Object {
|
||||
"category.keyword": "Men's Accessories 4",
|
||||
},
|
||||
},
|
||||
}
|
||||
`);
|
||||
expect(getFilterByPath(filters, '1.1')).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"$state": Object {
|
||||
"store": "appState",
|
||||
},
|
||||
"meta": Object {
|
||||
"alias": null,
|
||||
"disabled": false,
|
||||
"index": "ff959d40-b880-11e8-a6d9-e546fe2bba5f",
|
||||
"key": "category.keyword",
|
||||
"negate": false,
|
||||
"params": Object {
|
||||
"query": "Men's Accessories 3",
|
||||
},
|
||||
"type": "phrase",
|
||||
},
|
||||
"query": Object {
|
||||
"match_phrase": Object {
|
||||
"category.keyword": "Men's Accessories 3",
|
||||
},
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"$state": Object {
|
||||
"store": "appState",
|
||||
},
|
||||
"meta": Object {
|
||||
"alias": null,
|
||||
"disabled": false,
|
||||
"index": "ff959d40-b880-11e8-a6d9-e546fe2bba5f",
|
||||
"key": "category.keyword",
|
||||
"negate": false,
|
||||
"params": Object {
|
||||
"query": "Men's Accessories 4",
|
||||
},
|
||||
"type": "phrase",
|
||||
},
|
||||
"query": Object {
|
||||
"match_phrase": Object {
|
||||
"category.keyword": "Men's Accessories 4",
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getConditionalOperationType', () => {
|
||||
let filter: Filter;
|
||||
let filtersWithOrRelationships: FilterItem;
|
||||
let groupOfFilters: FilterItem;
|
||||
|
||||
beforeAll(() => {
|
||||
filter = filters[0];
|
||||
filtersWithOrRelationships = filters[1];
|
||||
groupOfFilters = filters[1].meta.params;
|
||||
});
|
||||
|
||||
test('should return correct ConditionalOperationType', () => {
|
||||
expect(getConditionalOperationType(filter)).toBeUndefined();
|
||||
expect(getConditionalOperationType(filtersWithOrRelationships)).toBe(ConditionTypes.OR);
|
||||
expect(getConditionalOperationType(groupOfFilters)).toBe(ConditionTypes.AND);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPathInArray', () => {
|
||||
test('should return correct path in array from path', () => {
|
||||
expect(getPathInArray('0')).toStrictEqual([0]);
|
||||
expect(getPathInArray('1.1')).toStrictEqual([1, 1]);
|
||||
expect(getPathInArray('1.0.2')).toStrictEqual([1, 0, 2]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addFilter', () => {
|
||||
const emptyFilter = buildEmptyFilter(false);
|
||||
|
||||
test('should add filter into filters after zero element', () => {
|
||||
const enlargedFilters = addFilter(filters, emptyFilter, '0', ConditionTypes.AND);
|
||||
expect(getFilterByPath(enlargedFilters, '1')).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"$state": Object {
|
||||
"store": "appState",
|
||||
},
|
||||
"meta": Object {
|
||||
"alias": null,
|
||||
"disabled": false,
|
||||
"index": undefined,
|
||||
"negate": false,
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeFilter', () => {
|
||||
test('should remove filter from filters', () => {
|
||||
const path = '1.1';
|
||||
const filterBeforeRemoved = getFilterByPath(filters, path);
|
||||
const filtersAfterRemoveFilter = removeFilter(filters, path);
|
||||
const filterObtainedAfterFilterRemovalFromFilters = getFilterByPath(
|
||||
filtersAfterRemoveFilter,
|
||||
path
|
||||
);
|
||||
|
||||
expect(filterBeforeRemoved).not.toBe(filterObtainedAfterFilterRemovalFromFilters);
|
||||
});
|
||||
});
|
||||
|
||||
describe('moveFilter', () => {
|
||||
test('should move filter from "0" path to "2" path into filters', () => {
|
||||
const filterBeforeMoving = getFilterByPath(filters, '0');
|
||||
const filtersAfterMovingFilter = moveFilter(filters, '0', '2', ConditionTypes.AND);
|
||||
const filterObtainedAfterFilterMovingFilters = getFilterByPath(filtersAfterMovingFilter, '2');
|
||||
expect(filterBeforeMoving).toEqual(filterObtainedAfterFilterMovingFilters);
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizeFilters', () => {
|
||||
test('should normalize filter after removed filter', () => {
|
||||
const dataNeedsNormalized = getDataThatNeedsNormalized();
|
||||
const dataAfterNormalized = getDataAfterNormalized();
|
||||
expect(normalizeFilters(dataNeedsNormalized)).toEqual(dataAfterNormalized);
|
||||
});
|
||||
|
||||
test('should not normalize filter after removed filter', () => {
|
||||
const dataNeedNotNormalized = getDataThatNeedNotNormalized();
|
||||
const dataAfterNormalized = getDataThatNeedNotNormalized();
|
||||
expect(normalizeFilters(dataNeedNotNormalized)).toEqual(dataAfterNormalized);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,360 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { DataViewField } from '@kbn/data-views-plugin/common';
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { ConditionTypes, getConditionalOperationType, isOrFilter, buildOrFilter } from '../utils';
|
||||
import type { FilterItem } from '../utils';
|
||||
import type { Operator } from '../filter_bar/filter_editor';
|
||||
|
||||
const PATH_SEPARATOR = '.';
|
||||
|
||||
/**
|
||||
* The method returns the filter nesting identification number as an array.
|
||||
* @param {string} path - variable is used to identify the filter and its nesting in the filter group.
|
||||
*/
|
||||
export const getPathInArray = (path: string) => path.split(PATH_SEPARATOR).map((i) => +i);
|
||||
|
||||
const getGroupedFilters = (filter: FilterItem) =>
|
||||
Array.isArray(filter) ? filter : filter?.meta?.params;
|
||||
|
||||
const doForFilterByPath = <T>(
|
||||
filters: FilterItem[],
|
||||
path: string,
|
||||
action: (filter: FilterItem) => T
|
||||
) => {
|
||||
const pathArray = getPathInArray(path);
|
||||
let f = filters[pathArray[0]];
|
||||
for (let i = 1, depth = pathArray.length; i < depth; i++) {
|
||||
f = getGroupedFilters(f)[+pathArray[i]];
|
||||
}
|
||||
return action(f);
|
||||
};
|
||||
|
||||
const getContainerMetaByPath = (filters: FilterItem[], pathInArray: number[]) => {
|
||||
let targetArray: FilterItem[] = filters;
|
||||
let parentFilter: FilterItem | undefined;
|
||||
let parentConditionType = ConditionTypes.AND;
|
||||
|
||||
if (pathInArray.length > 1) {
|
||||
parentFilter = getFilterByPath(filters, getParentFilterPath(pathInArray));
|
||||
parentConditionType = getConditionalOperationType(parentFilter) ?? parentConditionType;
|
||||
targetArray = getGroupedFilters(parentFilter);
|
||||
}
|
||||
|
||||
return {
|
||||
parentFilter,
|
||||
targetArray,
|
||||
parentConditionType,
|
||||
};
|
||||
};
|
||||
|
||||
const getParentFilterPath = (pathInArray: number[]) =>
|
||||
pathInArray.slice(0, -1).join(PATH_SEPARATOR);
|
||||
|
||||
/**
|
||||
* The method corrects the positions of the filters after removing some filter from the filters.
|
||||
* @param {FilterItem[]} filters - an array of filters that may contain filters that are incorrectly nested for later display in the UI.
|
||||
*/
|
||||
export const normalizeFilters = (filters: FilterItem[]) => {
|
||||
const doRecursive = (f: FilterItem, parent: FilterItem) => {
|
||||
if (Array.isArray(f)) {
|
||||
return normalizeArray(f, parent);
|
||||
} else if (isOrFilter(f)) {
|
||||
return normalizeOr(f);
|
||||
}
|
||||
return f;
|
||||
};
|
||||
|
||||
const normalizeArray = (filtersArray: FilterItem[], parent: FilterItem): FilterItem[] => {
|
||||
const partiallyNormalized = filtersArray
|
||||
.map((item) => {
|
||||
const normalized = doRecursive(item, filtersArray);
|
||||
|
||||
if (Array.isArray(normalized)) {
|
||||
if (normalized.length === 1) {
|
||||
return normalized[0];
|
||||
}
|
||||
if (normalized.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
return normalized;
|
||||
}, [])
|
||||
.filter(Boolean) as FilterItem[];
|
||||
|
||||
return Array.isArray(parent) ? partiallyNormalized.flat() : partiallyNormalized;
|
||||
};
|
||||
|
||||
const normalizeOr = (orFilter: Filter): FilterItem => {
|
||||
const orFilters = getGroupedFilters(orFilter);
|
||||
if (orFilters.length < 2) {
|
||||
return orFilters[0];
|
||||
}
|
||||
|
||||
return {
|
||||
...orFilter,
|
||||
meta: {
|
||||
...orFilter.meta,
|
||||
params: doRecursive(orFilters, orFilter),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
return normalizeArray(filters, filters) as Filter[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Find filter by path.
|
||||
* @param {FilterItem[]} filters - filters in which the search for the desired filter will occur.
|
||||
* @param {string} path - path to filter.
|
||||
*/
|
||||
export const getFilterByPath = (filters: FilterItem[], path: string) =>
|
||||
doForFilterByPath(filters, path, (f) => f);
|
||||
|
||||
/**
|
||||
* Method to add a filter to a specified location in a filter group.
|
||||
* @param {Filter[]} filters - array of filters where the new filter will be added.
|
||||
* @param {FilterItem} filter - new filter.
|
||||
* @param {string} path - path to filter.
|
||||
* @param {ConditionTypes} conditionalType - OR/AND relationships between filters.
|
||||
*/
|
||||
export const addFilter = (
|
||||
filters: Filter[],
|
||||
filter: FilterItem,
|
||||
path: string,
|
||||
conditionalType: ConditionTypes
|
||||
) => {
|
||||
const newFilters = cloneDeep(filters);
|
||||
const pathInArray = getPathInArray(path);
|
||||
const { targetArray, parentConditionType } = getContainerMetaByPath(newFilters, pathInArray);
|
||||
const selector = pathInArray[pathInArray.length - 1];
|
||||
|
||||
if (parentConditionType !== conditionalType) {
|
||||
if (conditionalType === ConditionTypes.OR) {
|
||||
targetArray.splice(selector, 1, buildOrFilter([targetArray[selector], filter]));
|
||||
}
|
||||
if (conditionalType === ConditionTypes.AND) {
|
||||
targetArray.splice(selector, 1, [targetArray[selector], filter]);
|
||||
}
|
||||
} else {
|
||||
targetArray.splice(selector + 1, 0, filter);
|
||||
}
|
||||
|
||||
return newFilters;
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove filter from specified location.
|
||||
* @param {Filter[]} filters - array of filters.
|
||||
* @param {string} path - path to filter.
|
||||
*/
|
||||
export const removeFilter = (filters: Filter[], path: string) => {
|
||||
const newFilters = cloneDeep(filters);
|
||||
const pathInArray = getPathInArray(path);
|
||||
const { targetArray } = getContainerMetaByPath(newFilters, pathInArray);
|
||||
const selector = pathInArray[pathInArray.length - 1];
|
||||
|
||||
targetArray.splice(selector, 1);
|
||||
|
||||
return normalizeFilters(newFilters);
|
||||
};
|
||||
|
||||
/**
|
||||
* Moving the filter on drag and drop.
|
||||
* @param {Filter[]} filters - array of filters.
|
||||
* @param {string} from - filter path before moving.
|
||||
* @param {string} to - filter path where the filter will be moved.
|
||||
* @param {ConditionTypes} conditionalType - OR/AND relationships between filters.
|
||||
*/
|
||||
export const moveFilter = (
|
||||
filters: Filter[],
|
||||
from: string,
|
||||
to: string,
|
||||
conditionalType: ConditionTypes
|
||||
) => {
|
||||
const addFilterThenRemoveFilter = (
|
||||
source: Filter[],
|
||||
addedFilter: FilterItem,
|
||||
pathFrom: string,
|
||||
pathTo: string,
|
||||
conditional: ConditionTypes
|
||||
) => {
|
||||
const newFiltersWithFilter = addFilter(source, addedFilter, pathTo, conditional);
|
||||
return removeFilter(newFiltersWithFilter, pathFrom);
|
||||
};
|
||||
|
||||
const removeFilterThenAddFilter = (
|
||||
source: Filter[],
|
||||
removableFilter: FilterItem,
|
||||
pathFrom: string,
|
||||
pathTo: string,
|
||||
conditional: ConditionTypes
|
||||
) => {
|
||||
const newFiltersWithoutFilter = removeFilter(source, pathFrom);
|
||||
return addFilter(newFiltersWithoutFilter, removableFilter, pathTo, conditional);
|
||||
};
|
||||
|
||||
const newFilters = cloneDeep(filters);
|
||||
const movingFilter = getFilterByPath(newFilters, from);
|
||||
|
||||
const pathInArrayTo = getPathInArray(to);
|
||||
const pathInArrayFrom = getPathInArray(from);
|
||||
|
||||
if (pathInArrayTo.length === pathInArrayFrom.length) {
|
||||
const filterPositionTo = pathInArrayTo.at(-1);
|
||||
const filterPositionFrom = pathInArrayFrom.at(-1);
|
||||
|
||||
const { parentConditionType } = getContainerMetaByPath(newFilters, pathInArrayTo);
|
||||
const filterMovementDirection = Number(filterPositionTo) - Number(filterPositionFrom);
|
||||
|
||||
if (filterMovementDirection === -1 && parentConditionType === conditionalType) {
|
||||
return filters;
|
||||
}
|
||||
|
||||
if (filterMovementDirection >= -1) {
|
||||
return addFilterThenRemoveFilter(newFilters, movingFilter, from, to, conditionalType);
|
||||
} else {
|
||||
return removeFilterThenAddFilter(newFilters, movingFilter, from, to, conditionalType);
|
||||
}
|
||||
}
|
||||
|
||||
if (pathInArrayTo.length > pathInArrayFrom.length) {
|
||||
return addFilterThenRemoveFilter(newFilters, movingFilter, from, to, conditionalType);
|
||||
} else {
|
||||
return removeFilterThenAddFilter(newFilters, movingFilter, from, to, conditionalType);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Method to update values inside filter.
|
||||
* @param {Filter[]} filters - filter array
|
||||
* @param {string} path - path to filter
|
||||
* @param {DataViewField} field - DataViewField property inside a filter
|
||||
* @param {Operator} operator - defines a relation by property and value
|
||||
* @param {Filter['meta']['params']} params - filter value
|
||||
*/
|
||||
export const updateFilter = (
|
||||
filters: Filter[],
|
||||
path: string,
|
||||
field?: DataViewField,
|
||||
operator?: Operator,
|
||||
params?: Filter['meta']['params']
|
||||
) => {
|
||||
const newFilters = [...filters];
|
||||
const changedFilter = getFilterByPath(newFilters, path) as Filter;
|
||||
let filter = Object.assign({}, changedFilter);
|
||||
|
||||
if (field && operator && params) {
|
||||
if (Array.isArray(params)) {
|
||||
filter = updateWithIsOneOfOperator(filter, operator, params);
|
||||
} else {
|
||||
filter = updateWithIsOperator(filter, operator, params);
|
||||
}
|
||||
} else if (field && operator) {
|
||||
if (operator.type === 'exists') {
|
||||
filter = updateWithExistsOperator(filter, operator);
|
||||
} else {
|
||||
filter = updateOperator(filter, operator);
|
||||
}
|
||||
} else {
|
||||
filter = updateField(filter, field);
|
||||
}
|
||||
|
||||
const pathInArray = getPathInArray(path);
|
||||
const { targetArray } = getContainerMetaByPath(newFilters, pathInArray);
|
||||
const selector = pathInArray[pathInArray.length - 1];
|
||||
targetArray.splice(selector, 1, filter);
|
||||
|
||||
return newFilters;
|
||||
};
|
||||
|
||||
function updateField(filter: Filter, field?: DataViewField) {
|
||||
return {
|
||||
...filter,
|
||||
meta: {
|
||||
...filter.meta,
|
||||
key: field?.name,
|
||||
params: { query: undefined },
|
||||
value: undefined,
|
||||
type: undefined,
|
||||
},
|
||||
query: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function updateOperator(filter: Filter, operator?: Operator) {
|
||||
return {
|
||||
...filter,
|
||||
meta: {
|
||||
...filter.meta,
|
||||
negate: operator?.negate,
|
||||
type: operator?.type,
|
||||
params: { ...filter.meta.params, query: undefined },
|
||||
value: undefined,
|
||||
},
|
||||
query: { match_phrase: { field: filter.meta.key } },
|
||||
};
|
||||
}
|
||||
|
||||
function updateWithExistsOperator(filter: Filter, operator?: Operator) {
|
||||
return {
|
||||
...filter,
|
||||
meta: {
|
||||
...filter.meta,
|
||||
negate: operator?.negate,
|
||||
type: operator?.type,
|
||||
params: undefined,
|
||||
value: 'exists',
|
||||
},
|
||||
query: { exists: { field: filter.meta.key } },
|
||||
};
|
||||
}
|
||||
|
||||
function updateWithIsOperator(
|
||||
filter: Filter,
|
||||
operator?: Operator,
|
||||
params?: Filter['meta']['params']
|
||||
) {
|
||||
return {
|
||||
...filter,
|
||||
meta: {
|
||||
...filter.meta,
|
||||
negate: operator?.negate,
|
||||
type: operator?.type,
|
||||
params: { ...filter.meta.params, query: params },
|
||||
},
|
||||
query: { match_phrase: { ...filter!.query?.match_phrase, [filter.meta.key!]: params } },
|
||||
};
|
||||
}
|
||||
|
||||
function updateWithIsOneOfOperator(
|
||||
filter: Filter,
|
||||
operator?: Operator,
|
||||
params?: Array<Filter['meta']['params']>
|
||||
) {
|
||||
return {
|
||||
...filter,
|
||||
meta: {
|
||||
...filter.meta,
|
||||
negate: operator?.negate,
|
||||
type: operator?.type,
|
||||
params,
|
||||
},
|
||||
query: {
|
||||
bool: {
|
||||
minimum_should_match: 1,
|
||||
...filter!.query?.should,
|
||||
should: params?.map((param) => {
|
||||
return { match_phrase: { [filter.meta.key!]: param } };
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
22
src/plugins/unified_search/public/filters_builder/index.ts
Normal file
22
src/plugins/unified_search/public/filters_builder/index.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { withSuspense } from '@kbn/shared-ux-utility';
|
||||
|
||||
/**
|
||||
* The Lazily-loaded `FiltersBuilder` component. Consumers should use `React.Suspense` or
|
||||
* the withSuspense` HOC to load this component.
|
||||
*/
|
||||
export const FiltersBuilderLazy = React.lazy(() => import('./filters_builder'));
|
||||
|
||||
/**
|
||||
* A `FiltersBuilder` component that is wrapped by the `withSuspense` HOC. This component can
|
||||
* be used directly by consumers and will load the `FiltersBuilderLazy` component lazily with
|
||||
* a predefined fallback and error boundary.
|
||||
*/
|
||||
export const FiltersBuilder = withSuspense(FiltersBuilderLazy);
|
|
@ -12,7 +12,11 @@ export type { IndexPatternSelectProps } from './index_pattern_select';
|
|||
export type { QueryStringInputProps } from './query_string_input';
|
||||
export { QueryStringInput } from './query_string_input';
|
||||
export type { StatefulSearchBarProps, SearchBarProps } from './search_bar';
|
||||
export type { UnifiedSearchPublicPluginStart, UnifiedSearchPluginSetup } from './types';
|
||||
export type {
|
||||
UnifiedSearchPublicPluginStart,
|
||||
UnifiedSearchPluginSetup,
|
||||
IUnifiedSearchPluginServices,
|
||||
} from './types';
|
||||
export { SearchBar } from './search_bar';
|
||||
export type { FilterItemsProps } from './filter_bar';
|
||||
export { FilterLabel, FilterItem, FilterItems } from './filter_bar';
|
||||
|
|
|
@ -11,7 +11,7 @@ import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public';
|
|||
import { APPLY_FILTER_TRIGGER } from '@kbn/data-plugin/public';
|
||||
import { UPDATE_FILTER_REFERENCES_TRIGGER, updateFilterReferencesTrigger } from './triggers';
|
||||
import { ConfigSchema } from '../config';
|
||||
import { setIndexPatterns, setTheme, setOverlays, setAutocomplete } from './services';
|
||||
import { setIndexPatterns, setTheme, setOverlays } from './services';
|
||||
import { AutocompleteService } from './autocomplete/autocomplete_service';
|
||||
import { createSearchBar } from './search_bar';
|
||||
import { createIndexPatternSelect } from './index_pattern_select';
|
||||
|
@ -70,7 +70,6 @@ export class UnifiedSearchPublicPlugin
|
|||
setOverlays(core.overlays);
|
||||
setIndexPatterns(dataViews);
|
||||
const autocompleteStart = this.autocomplete.start();
|
||||
setAutocomplete(autocompleteStart);
|
||||
|
||||
const SearchBar = createSearchBar({
|
||||
core,
|
||||
|
@ -78,6 +77,9 @@ export class UnifiedSearchPublicPlugin
|
|||
storage: this.storage,
|
||||
usageCollection: this.usageCollection,
|
||||
isScreenshotMode: Boolean(screenshotMode?.isScreenshotMode()),
|
||||
unifiedSearch: {
|
||||
autocomplete: autocompleteStart,
|
||||
},
|
||||
});
|
||||
|
||||
uiActions.addTriggerAction(
|
||||
|
|
|
@ -11,8 +11,8 @@ import { Filter, buildEmptyFilter } from '@kbn/es-query';
|
|||
import { METRIC_TYPE } from '@kbn/analytics';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { UI_SETTINGS } from '@kbn/data-plugin/common';
|
||||
import { IDataPluginServices } from '@kbn/data-plugin/public';
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import type { IUnifiedSearchPluginServices } from '../types';
|
||||
import { FILTER_EDITOR_WIDTH } from '../filter_bar/filter_item/filter_item';
|
||||
import { FilterEditor } from '../filter_bar/filter_editor';
|
||||
import { fetchIndexPatterns } from './fetch_index_patterns';
|
||||
|
@ -32,7 +32,7 @@ export const FilterEditorWrapper = React.memo(function FilterEditorWrapper({
|
|||
closePopover,
|
||||
onFiltersUpdated,
|
||||
}: FilterEditorWrapperProps) {
|
||||
const kibana = useKibana<IDataPluginServices>();
|
||||
const kibana = useKibana<IUnifiedSearchPluginServices>();
|
||||
const { uiSettings, data, usageCollection, appName } = kibana.services;
|
||||
const reportUiCounter = usageCollection?.reportUiCounter.bind(usageCollection, appName);
|
||||
const [dataViews, setDataviews] = useState<DataView[]>([]);
|
||||
|
|
|
@ -29,7 +29,8 @@ import {
|
|||
import { METRIC_TYPE } from '@kbn/analytics';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { KIBANA_USER_QUERY_LANGUAGE_KEY, UI_SETTINGS } from '@kbn/data-plugin/common';
|
||||
import type { IDataPluginServices, SavedQueryService, SavedQuery } from '@kbn/data-plugin/public';
|
||||
import type { SavedQueryService, SavedQuery } from '@kbn/data-plugin/public';
|
||||
import type { IUnifiedSearchPluginServices } from '../types';
|
||||
import { fromUser } from './from_user';
|
||||
import { QueryLanguageSwitcher } from './language_switcher';
|
||||
import { FilterPanelOption } from '../types';
|
||||
|
@ -88,7 +89,7 @@ export function QueryBarMenuPanels({
|
|||
onQueryChange,
|
||||
setRenderedComponent,
|
||||
}: QueryBarMenuPanelsProps) {
|
||||
const kibana = useKibana<IDataPluginServices>();
|
||||
const kibana = useKibana<IUnifiedSearchPluginServices>();
|
||||
const { appName, usageCollection, uiSettings, http, storage } = kibana.services;
|
||||
const reportUiCounter = usageCollection?.reportUiCounter.bind(usageCollection, appName);
|
||||
const cancelPendingListingRequest = useRef<() => void>(() => {});
|
||||
|
|
|
@ -20,7 +20,6 @@ import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
|||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
import { stubIndexPattern } from '@kbn/data-plugin/public/stubs';
|
||||
import { UI_SETTINGS } from '@kbn/data-plugin/common';
|
||||
import { setAutocomplete } from '../services';
|
||||
import { unifiedSearchPluginMock } from '../mocks';
|
||||
|
||||
const startMock = coreMock.createStart();
|
||||
|
@ -96,6 +95,7 @@ function wrapQueryBarTopRowInContext(testProps: any) {
|
|||
|
||||
const services = {
|
||||
...startMock,
|
||||
unifiedSearch: unifiedSearchPluginMock.createStartContract(),
|
||||
data: dataPluginMock.createStartContract(),
|
||||
appName: 'discover',
|
||||
storage: createMockStorage(),
|
||||
|
@ -120,11 +120,6 @@ describe('QueryBarTopRowTopRow', () => {
|
|||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
const autocompleteStart = unifiedSearchPluginMock.createStartContract();
|
||||
setAutocomplete(autocompleteStart.autocomplete);
|
||||
});
|
||||
|
||||
it('Should render query and time picker', () => {
|
||||
const { getByText, getByTestId } = render(
|
||||
wrapQueryBarTopRowInContext({
|
||||
|
|
|
@ -26,12 +26,13 @@ import {
|
|||
useIsWithinBreakpoints,
|
||||
EuiSuperUpdateButton,
|
||||
} from '@elastic/eui';
|
||||
import { IDataPluginServices, TimeHistoryContract, getQueryLog } from '@kbn/data-plugin/public';
|
||||
import { TimeHistoryContract, getQueryLog } from '@kbn/data-plugin/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { DataView } from '@kbn/data-views-plugin/public';
|
||||
import type { PersistedLog } from '@kbn/data-plugin/public';
|
||||
import { useKibana, withKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { UI_SETTINGS } from '@kbn/data-plugin/common';
|
||||
import type { IUnifiedSearchPluginServices } from '../types';
|
||||
import QueryStringInputUI from './query_string_input';
|
||||
import { NoDataPopover } from './no_data_popover';
|
||||
import { shallowEqual } from '../utils/shallow_equal';
|
||||
|
@ -164,7 +165,7 @@ export const QueryBarTopRow = React.memo(
|
|||
const [isDateRangeInvalid, setIsDateRangeInvalid] = useState(false);
|
||||
const [isQueryInputFocused, setIsQueryInputFocused] = useState(false);
|
||||
|
||||
const kibana = useKibana<IDataPluginServices>();
|
||||
const kibana = useKibana<IUnifiedSearchPluginServices>();
|
||||
const { uiSettings, storage, appName } = kibana.services;
|
||||
const isQueryLangSelected = props.query && !isOfQueryType(props.query);
|
||||
|
||||
|
|
|
@ -27,12 +27,9 @@ import { coreMock } from '@kbn/core/public/mocks';
|
|||
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
|
||||
import { stubIndexPattern } from '@kbn/data-plugin/public/stubs';
|
||||
import { KibanaContextProvider, withKibana } from '@kbn/kibana-react-plugin/public';
|
||||
|
||||
import { setAutocomplete } from '../services';
|
||||
import { unifiedSearchPluginMock } from '../mocks';
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
const startMock = coreMock.createStart();
|
||||
|
||||
const noop = () => {
|
||||
|
@ -71,6 +68,7 @@ const QueryStringInput = withKibana(QueryStringInputUI);
|
|||
function wrapQueryStringInputInContext(testProps: any, storage?: any) {
|
||||
const services = {
|
||||
...startMock,
|
||||
unifiedSearch: unifiedSearchPluginMock.createStartContract(),
|
||||
data: dataPluginMock.createStartContract(),
|
||||
appName: testProps.appName || 'test',
|
||||
storage: storage || createMockStorage(),
|
||||
|
@ -95,11 +93,6 @@ describe('QueryStringInput', () => {
|
|||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
const autocompleteStart = unifiedSearchPluginMock.createStartContract();
|
||||
setAutocomplete(autocompleteStart.autocomplete);
|
||||
});
|
||||
|
||||
it('Should render the given query', async () => {
|
||||
const { getByText } = render(
|
||||
wrapQueryStringInputInContext({
|
||||
|
|
|
@ -30,7 +30,7 @@ import { FormattedMessage } from '@kbn/i18n-react';
|
|||
import { compact, debounce, isEmpty, isEqual, isFunction } from 'lodash';
|
||||
import { Toast } from '@kbn/core/public';
|
||||
import type { Query } from '@kbn/es-query';
|
||||
import { IDataPluginServices, getQueryLog } from '@kbn/data-plugin/public';
|
||||
import { getQueryLog } from '@kbn/data-plugin/public';
|
||||
import { DataView } from '@kbn/data-views-plugin/public';
|
||||
import type { PersistedLog } from '@kbn/data-plugin/public';
|
||||
import { getFieldSubtypeNested, KIBANA_USER_QUERY_LANGUAGE_KEY } from '@kbn/data-plugin/common';
|
||||
|
@ -41,11 +41,12 @@ import { fromUser } from './from_user';
|
|||
import { fetchIndexPatterns } from './fetch_index_patterns';
|
||||
import { QueryLanguageSwitcher } from './language_switcher';
|
||||
import type { SuggestionsListSize } from '../typeahead/suggestions_component';
|
||||
import type { IUnifiedSearchPluginServices } from '../types';
|
||||
import { SuggestionsComponent } from '../typeahead';
|
||||
import { onRaf } from '../utils';
|
||||
import { FilterButtonGroup } from '../filter_bar/filter_button_group/filter_button_group';
|
||||
import { QuerySuggestion, QuerySuggestionTypes } from '../autocomplete';
|
||||
import { getTheme, getAutocomplete } from '../services';
|
||||
import { getTheme } from '../services';
|
||||
import './query_string_input.scss';
|
||||
|
||||
export interface QueryStringInputProps {
|
||||
|
@ -93,7 +94,7 @@ export interface QueryStringInputProps {
|
|||
}
|
||||
|
||||
interface Props extends QueryStringInputProps {
|
||||
kibana: KibanaReactContextValue<IDataPluginServices>;
|
||||
kibana: KibanaReactContextValue<IUnifiedSearchPluginServices>;
|
||||
}
|
||||
|
||||
interface State {
|
||||
|
@ -202,7 +203,9 @@ export default class QueryStringInputUI extends PureComponent<Props, State> {
|
|||
const queryString = this.getQueryString();
|
||||
|
||||
const recentSearchSuggestions = this.getRecentSearchSuggestions(queryString);
|
||||
const hasQuerySuggestions = getAutocomplete().hasQuerySuggestions(language);
|
||||
const hasQuerySuggestions = await this.services.unifiedSearch.autocomplete.hasQuerySuggestions(
|
||||
language
|
||||
);
|
||||
|
||||
if (
|
||||
!hasQuerySuggestions ||
|
||||
|
@ -223,7 +226,7 @@ export default class QueryStringInputUI extends PureComponent<Props, State> {
|
|||
if (this.abortController) this.abortController.abort();
|
||||
this.abortController = new AbortController();
|
||||
const suggestions =
|
||||
(await getAutocomplete().getQuerySuggestions({
|
||||
(await this.services.unifiedSearch.autocomplete.getQuerySuggestions({
|
||||
language,
|
||||
indexPatterns,
|
||||
query: queryString,
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
import React, { useRef, memo, useEffect, useState, useCallback } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { EsqlLang, monaco } from '@kbn/monaco';
|
||||
import { IDataPluginServices } from '@kbn/data-plugin/public';
|
||||
import type { AggregateQuery } from '@kbn/es-query';
|
||||
import { getAggregateQueryMode } from '@kbn/es-query';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
|
@ -47,6 +46,7 @@ import { EditorFooter } from './editor_footer';
|
|||
import { ResizableButton } from './resizable_button';
|
||||
|
||||
import './overwrite.scss';
|
||||
import { IUnifiedSearchPluginServices } from '../../types';
|
||||
|
||||
export interface TextBasedLanguagesEditorProps {
|
||||
query: AggregateQuery;
|
||||
|
@ -106,7 +106,7 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
|
|||
Array<{ startLineNumber: number; message: string }>
|
||||
>([]);
|
||||
const [documentationSections, setDocumentationSections] = useState<DocumentationSections>();
|
||||
const kibana = useKibana<IDataPluginServices>();
|
||||
const kibana = useKibana<IUnifiedSearchPluginServices>();
|
||||
const { uiSettings } = kibana.services;
|
||||
|
||||
const styles = textBasedLanguagedEditorStyles(
|
||||
|
|
|
@ -27,9 +27,10 @@ import React, { useCallback, useEffect, useState, useRef } from 'react';
|
|||
import { css } from '@emotion/react';
|
||||
import { sortBy } from 'lodash';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { IDataPluginServices, SavedQuery, SavedQueryService } from '@kbn/data-plugin/public';
|
||||
import { SavedQuery, SavedQueryService } from '@kbn/data-plugin/public';
|
||||
import type { SavedQueryAttributes } from '@kbn/data-plugin/common';
|
||||
import './saved_query_management_list.scss';
|
||||
import type { IUnifiedSearchPluginServices } from '../types';
|
||||
|
||||
export interface SavedQueryManagementListProps {
|
||||
showSaveQuery?: boolean;
|
||||
|
@ -120,7 +121,7 @@ export function SavedQueryManagementList({
|
|||
onClose,
|
||||
hasFiltersOrQuery,
|
||||
}: SavedQueryManagementListProps) {
|
||||
const kibana = useKibana<IDataPluginServices>();
|
||||
const kibana = useKibana<IUnifiedSearchPluginServices>();
|
||||
const [savedQueries, setSavedQueries] = useState([] as SavedQuery[]);
|
||||
const [selectedSavedQuery, setSelectedSavedQuery] = useState(null as SavedQuery | null);
|
||||
const [toBeDeletedSavedQuery, setToBeDeletedSavedQuery] = useState(null as SavedQuery | null);
|
||||
|
|
|
@ -21,13 +21,15 @@ import { useFilterManager } from './lib/use_filter_manager';
|
|||
import { useTimefilter } from './lib/use_timefilter';
|
||||
import { useSavedQuery } from './lib/use_saved_query';
|
||||
import { useQueryStringManager } from './lib/use_query_string_manager';
|
||||
import { UnifiedSearchPublicPluginStart } from '../types';
|
||||
|
||||
interface StatefulSearchBarDeps {
|
||||
core: CoreStart;
|
||||
data: Omit<DataPublicPluginStart, 'ui'>;
|
||||
data: DataPublicPluginStart;
|
||||
storage: IStorageWrapper;
|
||||
usageCollection?: UsageCollectionSetup;
|
||||
isScreenshotMode?: boolean;
|
||||
unifiedSearch: Omit<UnifiedSearchPublicPluginStart, 'ui'>;
|
||||
}
|
||||
|
||||
export type StatefulSearchBarProps<QT extends Query | AggregateQuery = Query> =
|
||||
|
@ -127,6 +129,7 @@ export function createSearchBar({
|
|||
data,
|
||||
usageCollection,
|
||||
isScreenshotMode = false,
|
||||
unifiedSearch,
|
||||
}: StatefulSearchBarDeps) {
|
||||
// App name should come from the core application service.
|
||||
// Until it's available, we'll ask the user to provide it for the pre-wired component.
|
||||
|
@ -179,6 +182,7 @@ export function createSearchBar({
|
|||
data,
|
||||
storage,
|
||||
usageCollection,
|
||||
unifiedSearch,
|
||||
...core,
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -19,9 +19,9 @@ import { Query, Filter, TimeRange, AggregateQuery, isOfQueryType } from '@kbn/es
|
|||
import { withKibana, KibanaReactContextValue } from '@kbn/kibana-react-plugin/public';
|
||||
import type { TimeHistoryContract, SavedQuery } from '@kbn/data-plugin/public';
|
||||
import type { SavedQueryAttributes } from '@kbn/data-plugin/common';
|
||||
import { IDataPluginServices } from '@kbn/data-plugin/public';
|
||||
import { DataView } from '@kbn/data-views-plugin/public';
|
||||
|
||||
import type { IUnifiedSearchPluginServices } from '../types';
|
||||
import { SavedQueryMeta, SaveQueryForm } from '../saved_query_form';
|
||||
import { SavedQueryManagementList } from '../saved_query_management';
|
||||
import { QueryBarMenu, QueryBarMenuProps } from '../query_string_input/query_bar_menu';
|
||||
|
@ -32,7 +32,7 @@ import type { SuggestionsListSize } from '../typeahead/suggestions_component';
|
|||
import { searchBarStyles } from './search_bar.styles';
|
||||
|
||||
export interface SearchBarInjectedDeps {
|
||||
kibana: KibanaReactContextValue<IDataPluginServices>;
|
||||
kibana: KibanaReactContextValue<IUnifiedSearchPluginServices>;
|
||||
intl: InjectedIntl;
|
||||
timeHistory?: TimeHistoryContract;
|
||||
// Filter bar
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
import { ThemeServiceStart, OverlayStart } from '@kbn/core/public';
|
||||
import { createGetterSetter } from '@kbn/kibana-utils-plugin/public';
|
||||
import { DataViewsContract } from '@kbn/data-views-plugin/public';
|
||||
import { AutocompleteStart } from '.';
|
||||
|
||||
export const [getIndexPatterns, setIndexPatterns] =
|
||||
createGetterSetter<DataViewsContract>('IndexPatterns');
|
||||
|
@ -17,6 +16,3 @@ export const [getIndexPatterns, setIndexPatterns] =
|
|||
export const [getTheme, setTheme] = createGetterSetter<ThemeServiceStart>('Theme');
|
||||
|
||||
export const [getOverlays, setOverlays] = createGetterSetter<OverlayStart>('Overlays');
|
||||
|
||||
export const [getAutocomplete, setAutocomplete] =
|
||||
createGetterSetter<AutocompleteStart>('Autocomplete');
|
||||
|
|
|
@ -11,8 +11,10 @@ import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
|
|||
import type { ScreenshotModePluginStart } from '@kbn/screenshot-mode-plugin/public';
|
||||
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
import type { UiActionsSetup, UiActionsStart } from '@kbn/ui-actions-plugin/public';
|
||||
import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public';
|
||||
import { UsageCollectionSetup, UsageCollectionStart } from '@kbn/usage-collection-plugin/public';
|
||||
import { Query, AggregateQuery } from '@kbn/es-query';
|
||||
import { CoreStart } from '@kbn/core/public';
|
||||
import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public';
|
||||
import { AutocompleteSetup, AutocompleteStart } from './autocomplete';
|
||||
import type { IndexPatternSelectProps, StatefulSearchBarProps } from '.';
|
||||
|
||||
|
@ -56,7 +58,7 @@ export interface UnifiedSearchPublicPluginStart {
|
|||
autocomplete: AutocompleteStart;
|
||||
/**
|
||||
* prewired UI components
|
||||
* {@link DataPublicPluginStartUi}
|
||||
* {@link UnifiedSearchPublicPluginStartUi}
|
||||
*/
|
||||
ui: UnifiedSearchPublicPluginStartUi;
|
||||
}
|
||||
|
@ -70,3 +72,18 @@ export type FilterPanelOption =
|
|||
| 'negateFilter'
|
||||
| 'disableFilter'
|
||||
| 'deleteFilter';
|
||||
|
||||
export interface IUnifiedSearchPluginServices extends Partial<CoreStart> {
|
||||
unifiedSearch: {
|
||||
autocomplete: AutocompleteStart;
|
||||
};
|
||||
appName: string;
|
||||
uiSettings: CoreStart['uiSettings'];
|
||||
savedObjects: CoreStart['savedObjects'];
|
||||
notifications: CoreStart['notifications'];
|
||||
application: CoreStart['application'];
|
||||
http: CoreStart['http'];
|
||||
storage: IStorageWrapper;
|
||||
data: DataPublicPluginStart;
|
||||
usageCollection?: UsageCollectionStart;
|
||||
}
|
||||
|
|
|
@ -8,3 +8,11 @@
|
|||
|
||||
export { onRaf } from './on_raf';
|
||||
export { shallowEqual } from './shallow_equal';
|
||||
|
||||
export type { FilterItem } from './or_filter';
|
||||
export {
|
||||
ConditionTypes,
|
||||
isOrFilter,
|
||||
getConditionalOperationType,
|
||||
buildOrFilter,
|
||||
} from './or_filter';
|
||||
|
|
47
src/plugins/unified_search/public/utils/or_filter.ts
Normal file
47
src/plugins/unified_search/public/utils/or_filter.ts
Normal file
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
// Methods from this file will be removed after they are moved to the package
|
||||
import { buildEmptyFilter, Filter } from '@kbn/es-query';
|
||||
|
||||
export enum ConditionTypes {
|
||||
OR = 'OR',
|
||||
AND = 'AND',
|
||||
}
|
||||
|
||||
/** @internal **/
|
||||
export type FilterItem = Filter | FilterItem[];
|
||||
|
||||
/** to: @kbn/es-query **/
|
||||
export const isOrFilter = (filter: Filter) => Boolean(filter?.meta?.type === 'OR');
|
||||
|
||||
/**
|
||||
* Defines a conditional operation type (AND/OR) from the filter otherwise returns undefined.
|
||||
* @param {FilterItem} filter
|
||||
*/
|
||||
export const getConditionalOperationType = (filter: FilterItem) => {
|
||||
if (Array.isArray(filter)) {
|
||||
return ConditionTypes.AND;
|
||||
} else if (isOrFilter(filter)) {
|
||||
return ConditionTypes.OR;
|
||||
}
|
||||
};
|
||||
|
||||
/** to: @kbn/es-query **/
|
||||
export const buildOrFilter = (filters: FilterItem) => {
|
||||
const filter = buildEmptyFilter(false);
|
||||
|
||||
return {
|
||||
...filter,
|
||||
meta: {
|
||||
...filter.meta,
|
||||
type: 'OR',
|
||||
params: filters,
|
||||
},
|
||||
};
|
||||
};
|
|
@ -20,8 +20,8 @@ import { i18n } from '@kbn/i18n';
|
|||
import classNames from 'classnames';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { connect } from 'react-redux';
|
||||
import { IDataPluginServices } from '@kbn/data-plugin/public';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { IUnifiedSearchPluginServices } from '@kbn/unified-search-plugin/public/types';
|
||||
import {
|
||||
GraphState,
|
||||
hasDatasourceSelector,
|
||||
|
@ -75,7 +75,7 @@ function GuidancePanelComponent(props: GuidancePanelProps) {
|
|||
const { onFillWorkspace, onOpenFieldPicker, onIndexPatternSelected, hasDatasource, hasFields } =
|
||||
props;
|
||||
|
||||
const kibana = useKibana<IDataPluginServices>();
|
||||
const kibana = useKibana<IUnifiedSearchPluginServices>();
|
||||
const { services, overlays } = kibana;
|
||||
const { savedObjects, uiSettings, application, data } = services;
|
||||
const [hasDataViews, setHasDataViews] = useState<boolean>(true);
|
||||
|
|
|
@ -19,8 +19,6 @@ import {
|
|||
import { act } from 'react-dom/test-utils';
|
||||
import { QueryStringInput } from '@kbn/unified-search-plugin/public';
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import { setAutocomplete } from '@kbn/unified-search-plugin/public/services';
|
||||
import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { I18nProvider, InjectedIntl } from '@kbn/i18n-react';
|
||||
|
||||
|
@ -106,11 +104,6 @@ describe('search_bar', () => {
|
|||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
const autocompleteStart = unifiedSearchPluginMock.createStartContract();
|
||||
setAutocomplete(autocompleteStart.autocomplete);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
store = createMockGraphStore({
|
||||
sagas: [submitSearchSaga],
|
||||
|
|
|
@ -12,9 +12,9 @@ import { i18n } from '@kbn/i18n';
|
|||
import { connect } from 'react-redux';
|
||||
import { toElasticsearchQuery, fromKueryExpression, Query } from '@kbn/es-query';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { IDataPluginServices } from '@kbn/data-plugin/public';
|
||||
import { QueryStringInput } from '@kbn/unified-search-plugin/public';
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import { IUnifiedSearchPluginServices } from '@kbn/unified-search-plugin/public/types';
|
||||
import { IndexPatternSavedObject, IndexPatternProvider, WorkspaceField } from '../types';
|
||||
import { openSourceModal } from '../services/source_modal';
|
||||
import {
|
||||
|
@ -95,7 +95,7 @@ export function SearchBarComponent(props: SearchBarStateProps & SearchBarProps)
|
|||
fetchPattern();
|
||||
}, [currentDatasource, indexPatternProvider, onIndexPatternChange]);
|
||||
|
||||
const kibana = useKibana<IDataPluginServices>();
|
||||
const kibana = useKibana<IUnifiedSearchPluginServices>();
|
||||
const { services, overlays } = kibana;
|
||||
const { savedObjects, uiSettings } = services;
|
||||
if (!overlays) return null;
|
||||
|
|
|
@ -15,8 +15,6 @@ import { FilterManager } from '@kbn/data-plugin/public';
|
|||
import { SearchBar } from '@kbn/unified-search-plugin/public';
|
||||
import type { QueryBarComponentProps } from '.';
|
||||
import { QueryBar } from '.';
|
||||
import { setAutocomplete } from '@kbn/unified-search-plugin/public/services';
|
||||
import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks';
|
||||
|
||||
const mockUiSettingsForFilterManager = coreMock.createStart().uiSettings;
|
||||
|
||||
|
@ -275,11 +273,6 @@ describe('QueryBar ', () => {
|
|||
});
|
||||
|
||||
describe('#onSavedQueryUpdated', () => {
|
||||
beforeEach(() => {
|
||||
const autocompleteStart = unifiedSearchPluginMock.createStartContract();
|
||||
setAutocomplete(autocompleteStart.autocomplete);
|
||||
});
|
||||
|
||||
test('is only reference that changed when dataProviders props get updated', async () => {
|
||||
await act(async () => {
|
||||
const wrapper = await getWrapper(
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import React, { memo, useState } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiSuperDatePicker } from '@elastic/eui';
|
||||
import type { IDataPluginServices } from '@kbn/data-plugin/public';
|
||||
import type { IUnifiedSearchPluginServices } from '@kbn/unified-search-plugin/public';
|
||||
import { euiStyled } from '@kbn/kibana-react-plugin/common';
|
||||
import type { EuiSuperDatePickerRecentRange } from '@elastic/eui';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
|
@ -16,7 +16,6 @@ import type {
|
|||
OnRefreshChangeProps,
|
||||
} from '@elastic/eui/src/components/date_picker/types';
|
||||
import { UI_SETTINGS } from '@kbn/data-plugin/common';
|
||||
|
||||
import { useTestIdGenerator } from '../../../hooks/use_test_id_generator';
|
||||
import { useActionHistoryUrlParams } from './use_action_history_url_params';
|
||||
|
||||
|
@ -52,7 +51,7 @@ export const ActionLogDateRangePicker = memo(
|
|||
}) => {
|
||||
const { startDate: startDateFromUrl, endDate: endDateFromUrl } = useActionHistoryUrlParams();
|
||||
const getTestId = useTestIdGenerator('response-actions-list');
|
||||
const kibana = useKibana<IDataPluginServices>();
|
||||
const kibana = useKibana<IUnifiedSearchPluginServices>();
|
||||
const { uiSettings } = kibana.services;
|
||||
const [commonlyUsedRanges] = useState(() => {
|
||||
return (
|
||||
|
|
|
@ -18,8 +18,6 @@ import { FilterStateStore } from '@kbn/es-query';
|
|||
import { FilterManager } from '@kbn/data-plugin/public';
|
||||
import { mockDataProviders } from '../data_providers/mock/mock_data_providers';
|
||||
import { buildGlobalQuery } from '../helpers';
|
||||
import { setAutocomplete } from '@kbn/unified-search-plugin/public/services';
|
||||
import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks';
|
||||
|
||||
import type { QueryBarTimelineComponentProps } from '.';
|
||||
import { QueryBarTimeline, getDataProviderFilter, TIMELINE_FILTER_DROP_AREA } from '.';
|
||||
|
@ -182,11 +180,6 @@ describe('Timeline QueryBar ', () => {
|
|||
});
|
||||
|
||||
describe('#onSavedQuery', () => {
|
||||
beforeEach(() => {
|
||||
const autocompleteStart = unifiedSearchPluginMock.createStartContract();
|
||||
setAutocomplete(autocompleteStart.autocomplete);
|
||||
});
|
||||
|
||||
test('is only reference that changed when dataProviders props get updated', async () => {
|
||||
const Proxy = (props: QueryBarTimelineComponentProps) => (
|
||||
<TestProviders>
|
||||
|
|
|
@ -8,15 +8,8 @@
|
|||
import React from 'react';
|
||||
import { OverviewPageComponent } from './overview';
|
||||
import { render } from '../lib/helper/rtl_helpers';
|
||||
import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks';
|
||||
import { setAutocomplete } from '@kbn/unified-search-plugin/public/services';
|
||||
|
||||
describe('MonitorPage', () => {
|
||||
beforeEach(() => {
|
||||
const autocompleteStart = unifiedSearchPluginMock.createStartContract();
|
||||
setAutocomplete(autocompleteStart.autocomplete);
|
||||
});
|
||||
|
||||
it('renders expected elements for valid props', async () => {
|
||||
const { findByText, findByPlaceholderText } = render(<OverviewPageComponent />);
|
||||
|
||||
|
|
|
@ -13,9 +13,8 @@ import userEvent from '@testing-library/user-event';
|
|||
import { FilterManager } from '@kbn/data-plugin/public';
|
||||
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import { TestProvidersComponent, unifiedSearch } from '../../../../common/mocks/test_providers';
|
||||
import { TestProvidersComponent } from '../../../../common/mocks/test_providers';
|
||||
import { getByTestSubj } from '../../../../../common/test/utils';
|
||||
import { setAutocomplete } from '@kbn/unified-search-plugin/public/services';
|
||||
|
||||
const mockUiSettingsForFilterManager = coreMock.createStart().uiSettings;
|
||||
|
||||
|
@ -27,10 +26,6 @@ describe('QueryBar ', () => {
|
|||
const onSavedQuery = jest.fn();
|
||||
const onChangedQuery = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
setAutocomplete(unifiedSearch.autocomplete);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await act(async () => {
|
||||
render(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue