[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:
Nodir Latipov 2022-09-13 19:36:52 +05:00 committed by GitHub
parent efbf9e4d31
commit 1fcd75a706
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
62 changed files with 2791 additions and 183 deletions

View file

@ -291,7 +291,6 @@ export function plugin(initializerContext: PluginInitializerContext<ConfigSchema
export type {
DataPublicPluginSetup,
DataPublicPluginStart,
IDataPluginServices,
DataPublicPluginStartActions,
} from './types';

View file

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

View file

@ -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(() =>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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
*/

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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);

View file

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

View file

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

View file

@ -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[]>([]);

View file

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

View file

@ -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({

View file

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

View file

@ -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({

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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,
},
};
};

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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