diff --git a/src/plugins/data/public/ui/filter_bar/filter_bar.tsx b/src/plugins/data/public/ui/filter_bar/filter_bar.tsx index 01ad05c19c6d8..629f0b0a2ace9 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_bar.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_bar.tsx @@ -6,33 +6,24 @@ * Side Public License, v 1. */ -import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiPopover } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { groupBy, isEqual } from 'lodash'; -import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n-react'; -import { - buildEmptyFilter, - Filter, - // enableFilter, - // disableFilter, - // pinFilter, - // toggleFilterDisabled, - toggleFilterNegated, - // unpinFilter, -} from '@kbn/es-query'; +import { InjectedIntl, injectI18n } from '@kbn/i18n-react'; +import { Filter, toggleFilterNegated } from '@kbn/es-query'; import classNames from 'classnames'; -import React, { useState, useRef } from 'react'; +import React, { useRef, useState } from 'react'; import { METRIC_TYPE } from '@kbn/analytics'; -import { FilterEditor } from './filter_editor'; -import { FILTER_EDITOR_WIDTH, FilterItem } from './filter_item'; -// import { FilterOptions } from './filter_options'; +import { FilterItem } from './filter_item'; import { useKibana } from '../../../../kibana_react/public'; import { IDataPluginServices, IIndexPattern } from '../..'; import type { SavedQuery } from '../../query'; import { SavedQueriesItem } from './saved_queries_item'; import { FilterExpressionItem } from './filter_expression_item'; -import { UI_SETTINGS } from '../../../common'; +import { EditFilterModal } from '../query_string_input/edit_filter_modal'; +import { mapAndFlattenFilters } from '../../query/filter_manager/lib/map_and_flatten_filters'; +import { FilterGroup } from '../query_string_input/edit_filter_modal'; import { SavedQueryMeta } from '../saved_query_form'; import { SavedQueryService } from '../..'; @@ -48,6 +39,9 @@ interface Props { selectedSavedQueries?: SavedQuery[]; removeSelectedSavedQuery: (savedQuery: SavedQuery) => void; onMultipleFiltersUpdated?: (filters: Filter[]) => void; + toggleEditFilterModal: (value: boolean) => void; + isEditFilterModalOpen?: boolean; + editFilterMode?: string; savedQueryService: SavedQueryService; onFilterSave: (savedQueryMeta: SavedQueryMeta, saveAsNew?: boolean) => Promise; onFilterBadgeSave: (groupId: number, alias: string) => void; @@ -55,9 +49,9 @@ interface Props { const FilterBarUI = React.memo(function FilterBarUI(props: Props) { const groupRef = useRef(null); - const [isAddFilterPopoverOpen, setIsAddFilterPopoverOpen] = useState(false); const kibana = useKibana(); const { appName, usageCollection, uiSettings } = kibana.services; + const [groupId, setGroupId] = useState(null); if (!uiSettings) return null; const reportUiCounter = usageCollection?.reportUiCounter.bind(usageCollection, appName); @@ -68,10 +62,59 @@ const FilterBarUI = React.memo(function FilterBarUI(props: Props) { } } - const onAddFilterClick = () => setIsAddFilterPopoverOpen(!isAddFilterPopoverOpen); + const onEditFilterClick = (groupId: number) => { + setGroupId(groupId); + props.toggleEditFilterModal?.(true); + }; + + const onDeleteFilterGroup = (groupId: string) => { + const multipleFilters = [...props.multipleFilters]; + const updatedMultipleFilters = multipleFilters.filter( + (filter) => filter.groupId !== Number(groupId) + ); + const filters = [...props.filters]; + const updatedFilters: Filter[] = []; + + updatedMultipleFilters.forEach((filter) => { + filters.forEach((f) => { + if (isEqual(f.query, filter.query)) { + updatedFilters.push(f); + } + }); + }); + onFiltersUpdated(updatedFilters); + props?.onMultipleFiltersUpdated?.(updatedMultipleFilters); + + props.toggleEditFilterModal?.(false); + }; + + function onAddMultipleFilters(selectedFilters: Filter[]) { + props.toggleEditFilterModal?.(false); + + const filters = [...props.filters, ...selectedFilters]; + props?.onFiltersUpdated?.(filters); + } + + function onEditMultipleFiltersANDOR(selectedFilters: FilterGroup[], buildFilters: Filter[]) { + const mappedFilters = mapAndFlattenFilters(buildFilters); + const mergedFilters = mappedFilters.map((filter, idx) => { + return { + ...filter, + groupId: selectedFilters[idx].groupId, + id: selectedFilters[idx].id, + relationship: selectedFilters[idx].relationship, + subGroupId: selectedFilters[idx].subGroupId, + }; + }); + props.toggleEditFilterModal?.(false); + props?.onMultipleFiltersUpdated?.(mergedFilters); + + const filters = [...props.filters, ...buildFilters]; + props?.onFiltersUpdated?.(filters); + } function renderItems() { - return props.filters.map((filter, i) => { + return props.multipleFilters.map((filter, i) => { // Do not display filters from saved queries if (filter.meta.isFromSavedQuery) return null; return ( @@ -117,10 +160,11 @@ const FilterBarUI = React.memo(function FilterBarUI(props: Props) { groupId={groupId} groupedFilters={groupedFilters} indexPatterns={props?.indexPatterns} - onClick={() => {}} + onClick={() => { }} onRemove={onRemoveFilterGroup} onUpdate={onUpdateFilterGroup} filtersGroupsCount={Object.entries(firstDepthGroupedFilters).length} + onEditFilterClick={onEditFilterClick} savedQueryService={props.savedQueryService} onFilterSave={props.onFilterSave} onFilterBadgeSave={props.onFilterBadgeSave} @@ -139,11 +183,15 @@ const FilterBarUI = React.memo(function FilterBarUI(props: Props) { groupId={groupId} groupedFilters={groupedByAlias[label]} indexPatterns={props?.indexPatterns} - onClick={() => {}} + onClick={() => { }} onRemove={onRemoveFilterGroup} onUpdate={onUpdateFilterGroup} + onEditFilterClick={onEditFilterClick} filtersGroupsCount={1} customLabel={label} + savedQueryService={props.savedQueryService} + onFilterSave={props.onFilterSave} + onFilterBadgeSave={props.onFilterBadgeSave} /> ); GroupBadge.push(labelBadge); @@ -152,65 +200,32 @@ const FilterBarUI = React.memo(function FilterBarUI(props: Props) { return GroupBadge; } - function renderAddFilter() { - const isPinned = uiSettings!.get(UI_SETTINGS.FILTERS_PINNED_BY_DEFAULT); - const [indexPattern] = props.indexPatterns; - const index = indexPattern && indexPattern.id; - const newFilter = buildEmptyFilter(isPinned, index); - - const button = ( - - +{' '} - - + function renderEditFilter() { + const currentEditFilters = props.multipleFilters.filter( + (filter) => filter.groupId == Number(groupId) ); return ( - setIsAddFilterPopoverOpen(false)} - anchorPosition="downLeft" - panelPaddingSize="none" - initialFocus=".filterEditor__hiddenItem" - ownFocus - repositionOnScroll - > - -
- setIsAddFilterPopoverOpen(false)} - key={JSON.stringify(newFilter)} - timeRangeForSuggestionsOverride={props.timeRangeForSuggestionsOverride} - /> -
-
-
+ {props.isEditFilterModalOpen && ( + props.toggleEditFilterModal?.(false)} + onCancel={() => props.toggleEditFilterModal?.(false)} + filter={props.multipleFilters[0]} + multipleFilters={currentEditFilters} + indexPatterns={props.indexPatterns!} + onRemoveFilterGroup={onDeleteFilterGroup} + timeRangeForSuggestionsOverride={props.timeRangeForSuggestionsOverride} + savedQueryManagement={undefined} + initialAddFilterMode={undefined} + /> + )}
); } - function onAdd(filter: Filter) { - reportUiCounter?.(METRIC_TYPE.CLICK, `filter:added`); - setIsAddFilterPopoverOpen(false); - - const filters = [...props.filters, filter]; - onFiltersUpdated(filters); - } - function onRemove(i: number) { reportUiCounter?.(METRIC_TYPE.CLICK, `filter:removed`); const filters = [...props.filters]; @@ -272,47 +287,6 @@ const FilterBarUI = React.memo(function FilterBarUI(props: Props) { onFiltersUpdated(filters); } - // function onEnableAll() { - // reportUiCounter?.(METRIC_TYPE.CLICK, `filter:enable_all`); - // const filters = props.filters.map(enableFilter); - // onFiltersUpdated(filters); - // } - - // function onDisableAll() { - // reportUiCounter?.(METRIC_TYPE.CLICK, `filter:disable_all`); - // const filters = props.filters.map(disableFilter); - // onFiltersUpdated(filters); - // } - - // function onPinAll() { - // reportUiCounter?.(METRIC_TYPE.CLICK, `filter:pin_all`); - // const filters = props.filters.map(pinFilter); - // onFiltersUpdated(filters); - // } - - // function onUnpinAll() { - // reportUiCounter?.(METRIC_TYPE.CLICK, `filter:unpin_all`); - // const filters = props.filters.map(unpinFilter); - // onFiltersUpdated(filters); - // } - - // function onToggleAllNegated() { - // reportUiCounter?.(METRIC_TYPE.CLICK, `filter:invert_all`); - // const filters = props.filters.map(toggleFilterNegated); - // onFiltersUpdated(filters); - // } - - // function onToggleAllDisabled() { - // reportUiCounter?.(METRIC_TYPE.CLICK, `filter:toggle_all`); - // const filters = props.filters.map(toggleFilterDisabled); - // onFiltersUpdated(filters); - // } - - // function onRemoveAll() { - // reportUiCounter?.(METRIC_TYPE.CLICK, `filter:remove_all`); - // onFiltersUpdated([]); - // } - const classes = classNames('globalFilterBar', props.className); return ( @@ -322,18 +296,6 @@ const FilterBarUI = React.memo(function FilterBarUI(props: Props) { alignItems="flexStart" responsive={false} > - {/* - - */} - diff --git a/src/plugins/data/public/ui/filter_bar/filter_expression_item.tsx b/src/plugins/data/public/ui/filter_bar/filter_expression_item.tsx index ed22ec9ea9bef..1956cc9459169 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_expression_item.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_expression_item.tsx @@ -50,6 +50,7 @@ interface Props { groupId: string; filtersGroupsCount: number; onUpdate?: (filters: Filter[], groupId: string, toggleNegate: boolean) => void; + onEditFilterClick: (groupId: number) => void; savedQueryService?: SavedQueryService; onFilterSave?: (savedQueryMeta: SavedQueryMeta, saveAsNew?: boolean) => Promise; customLabel?: string; @@ -64,6 +65,7 @@ export const FilterExpressionItem: FC = ({ groupId, filtersGroupsCount, onUpdate, + onEditFilterClick, savedQueryService, onFilterSave, customLabel, @@ -76,14 +78,13 @@ export const FilterExpressionItem: FC = ({ query: filter.query, })); function handleBadgeClick() { - // if (e.shiftKey) { - // onToggleDisabled(); - // } else { - // setIsPopoverOpen(!isPopoverOpen); - // } setIsPopoverOpen(!isPopoverOpen); } + function onEdit(groupId: number) { + onEditFilterClick(groupId); + } + function onDuplicate() { const multipleUpdatedFilters = groupedFilters?.map((filter: Filter) => { return { ...filter, groupId: filtersGroupsCount + 1 }; @@ -95,7 +96,11 @@ export const FilterExpressionItem: FC = ({ function onToggleNegated() { const isNegated = groupedFilters[0].groupNegated; const multipleUpdatedFilters = groupedFilters?.map((filter: Filter) => { - return { ...filter, groupNegated: !isNegated }; + if (filter.meta.negate) { + return { ...filter, meta: { ...filter.meta, negate: false } } + } else { + return { ...filter, groupNegated: !isNegated }; + } }); onUpdate?.(multipleUpdatedFilters, groupId, true); @@ -113,7 +118,10 @@ export const FilterExpressionItem: FC = ({ defaultMessage: `Edit`, }), icon: 'pencil', - panel: 1, + onClick: () => { + setIsPopoverOpen(false); + onEdit(groupId); + }, 'data-test-subj': 'editFilter', }, { @@ -141,11 +149,11 @@ export const FilterExpressionItem: FC = ({ { name: groupedFilters[0].meta.disabled ? i18n.translate('data.filter.filterBar.enableFilterButtonLabel', { - defaultMessage: `Re-enable`, - }) + defaultMessage: `Re-enable`, + }) : i18n.translate('data.filter.filterBar.disableFilterButtonLabel', { - defaultMessage: `Temporarily disable`, - }), + defaultMessage: `Temporarily disable`, + }), icon: `${groupedFilters[0].meta.disabled ? 'eye' : 'eyeClosed'}`, onClick: () => { setIsPopoverOpen(false); @@ -171,23 +179,6 @@ export const FilterExpressionItem: FC = ({ id: 0, items: mainPanelItems, }, - // { - // id: 1, - // width: FILTER_EDITOR_WIDTH, - // content: ( - //
- // { - // setIsPopoverOpen(false); - // }} - // timeRangeForSuggestionsOverride={props.timeRangeForSuggestionsOverride} - // /> - //
- // ), - // }, ]; if (!customLabel && savedQueryService && onFilterSave && onFilterBadgeSave) { @@ -411,8 +402,8 @@ export const FilterExpressionItem: FC = ({ const prefixText = filter.meta.negate ? ` ${i18n.translate('data.filter.filterBar.negatedFilterPrefix', { - defaultMessage: 'NOT ', - })}` + defaultMessage: 'NOT ', + })}` : ''; const prefix = filter.meta.negate && !filter.meta.disabled ? ( @@ -426,9 +417,8 @@ export const FilterExpressionItem: FC = ({ filterExpression.push(filterContent); const text = label.title; - filterText += `${filter?.meta?.key}: ${text} ${ - groupedFilters.length > 1 ? filter.relationship || '' : '' - } `; + filterText += `${filter?.meta?.key}: ${text} ${groupedFilters.length > 1 ? filter.relationship || '' : '' + } `; } if (needsParenthesis) { filterExpression.push()); diff --git a/src/plugins/data/public/ui/query_string_input/add_filter_modal.tsx b/src/plugins/data/public/ui/query_string_input/add_filter_modal.tsx index cf997f1a196c0..8f4578cceabba 100644 --- a/src/plugins/data/public/ui/query_string_input/add_filter_modal.tsx +++ b/src/plugins/data/public/ui/query_string_input/add_filter_modal.tsx @@ -9,13 +9,7 @@ import React, { useState } from 'react'; import { groupBy } from 'lodash'; import classNames from 'classnames'; -import { - Filter, - buildFilter, - buildCustomFilter, - cleanFilter, - getFilterParams, -} from '@kbn/es-query'; +import { Filter, buildFilter, buildCustomFilter, cleanFilter } from '@kbn/es-query'; import { EuiFormRow, EuiFlexGroup, @@ -103,6 +97,7 @@ export function AddFilterModal({ applySavedQueries: () => void; onCancel: () => void; filter: Filter; + multipleFilters?: Filter[]; indexPatterns: IIndexPattern[]; timeRangeForSuggestionsOverride?: boolean; savedQueryManagement?: JSX.Element; @@ -119,10 +114,11 @@ export function AddFilterModal({ { field: undefined, operator: undefined, - value: getFilterParams(filter), + value: undefined, groupId: 1, id: 0, subGroupId: 1, + relationship: undefined, }, ]); const [groupsCount, setGroupsCount] = useState(1); @@ -249,11 +245,11 @@ export function AddFilterModal({ placeholder={ selectedField ? i18n.translate('data.filter.filterEditor.operatorSelectPlaceholderSelect', { - defaultMessage: 'Operator', - }) + defaultMessage: 'Operator', + }) : i18n.translate('data.filter.filterEditor.operatorSelectPlaceholderWaiting', { - defaultMessage: 'Waiting', - }) + defaultMessage: 'Waiting', + }) } options={operators} selectedOptions={selectedOperator ? [selectedOperator] : []} diff --git a/src/plugins/data/public/ui/query_string_input/edit_filter_modal.tsx b/src/plugins/data/public/ui/query_string_input/edit_filter_modal.tsx new file mode 100644 index 0000000000000..231752081baae --- /dev/null +++ b/src/plugins/data/public/ui/query_string_input/edit_filter_modal.tsx @@ -0,0 +1,723 @@ +/* + * 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, { useState } from 'react'; +import { groupBy } from 'lodash'; +import classNames from 'classnames'; +import { + Filter, + buildFilter, + buildCustomFilter, + cleanFilter, + getFilterParams, + FieldFilter, +} from '@kbn/es-query'; +import { + EuiFormRow, + EuiFlexGroup, + EuiFlexItem, + EuiModalFooter, + EuiButtonEmpty, + EuiButton, + EuiModal, + EuiModalHeaderTitle, + EuiModalHeader, + EuiModalBody, + EuiTabs, + EuiTab, + EuiForm, + EuiSpacer, + EuiHorizontalRule, + EuiButtonIcon, + EuiText, + EuiIcon, + EuiFieldText, +} from '@elastic/eui'; +import { XJsonLang } from '@kbn/monaco'; +import { i18n } from '@kbn/i18n'; +import { CodeEditor } from '../../../../kibana_react/public'; +import { getIndexPatternFromFilter } from '../../query'; +import { + getFilterableFields, + getOperatorOptions, + getFieldFromFilter, + getOperatorFromFilter, +} from '../filter_bar/filter_editor/lib/filter_editor_utils'; +import { Operator } from '../filter_bar/filter_editor/lib/filter_operators'; + +import { GenericComboBox } from '../filter_bar/filter_editor/generic_combo_box'; +import { PhraseValueInput } from '../filter_bar/filter_editor/phrase_value_input'; +import { PhrasesValuesInput } from '../filter_bar/filter_editor/phrases_values_input'; +import { RangeValueInput } from '../filter_bar/filter_editor/range_value_input'; + +import { IIndexPattern, IFieldType } from '../..'; + +const tabs = [ + { + type: 'quick_form', + label: i18n.translate('data.filter.filterEditor.quickFormLabel', { + defaultMessage: 'Quick form', + }), + }, + { + type: 'query_builder', + label: i18n.translate('data.filter.filterEditor.queryBuilderLabel', { + defaultMessage: 'Query builder', + }), + } +]; + +export interface FilterGroup { + field: IFieldType | undefined; + operator: Operator | undefined; + value: any; + groupId: number; + id: number; + relationship?: string; + subGroupId?: number; +} + +export function EditFilterModal({ + onSubmit, + onMultipleFiltersSubmit, + onCancel, + applySavedQueries, + filter, + multipleFilters, + indexPatterns, + timeRangeForSuggestionsOverride, + savedQueryManagement, + initialAddFilterMode, + onRemoveFilterGroup, +}: { + onSubmit: (filters: Filter[]) => void; + onMultipleFiltersSubmit: (filters: FilterGroup[], buildFilters: Filter[]) => void; + applySavedQueries: () => void; + onCancel: () => void; + filter: Filter; + multipleFilters?: Filter[]; + indexPatterns: IIndexPattern[]; + timeRangeForSuggestionsOverride?: boolean; + savedQueryManagement?: JSX.Element; + initialAddFilterMode?: string; + onRemoveFilterGroup: (groupId: string) => void; +}) { + const [selectedIndexPattern, setSelectedIndexPattern] = useState( + getIndexPatternFromFilter(filter, indexPatterns) + ); + const [addFilterMode, setAddFilterMode] = useState(initialAddFilterMode ?? tabs[0].type); + const [customLabel, setCustomLabel] = useState(filter.meta.alias || ''); + const [queryDsl, setQueryDsl] = useState(JSON.stringify(cleanFilter(filter), null, 2)); + const [localFilters, setLocalFilters] = useState( + convertFilterToFilterGroup(multipleFilters) + ); + const [groupsCount, setGroupsCount] = useState(1); + + function convertFilterToFilterGroup(convertibleFilters: Filter[] | undefined): FilterGroup[] { + if (!convertibleFilters) { + return [ + { + field: undefined, + operator: undefined, + value: undefined, + groupId: 1, + id: 0, + subGroupId: 1, + relationship: undefined, + }, + ]; + } + + return convertibleFilters.map((convertedfilter) => { + return { + field: getFieldFromFilter(convertedfilter as FieldFilter, selectedIndexPattern), + operator: getOperatorFromFilter(convertedfilter), + value: getFilterParams(convertedfilter), + groupId: convertedfilter.groupId, + id: convertedfilter.id, + subGroupId: convertedfilter.subGroupId, + relationship: convertedfilter.relationship + }; + }); + } + + const onIndexPatternChange = ([selectedPattern]: IIndexPattern[]) => { + setSelectedIndexPattern(selectedPattern); + setLocalFilters([ + { field: undefined, operator: undefined, value: undefined, groupId: 1, id: 0, subGroupId: 1 }, + ]); + setGroupsCount(1); + }; + + const onFieldChange = ([field]: IFieldType[], localFilterIndex: number) => { + const index = localFilters.findIndex((f) => f.id === localFilterIndex); + const updatedLocalFilter = { ...localFilters[index], field }; + localFilters[index] = updatedLocalFilter; + setLocalFilters([...localFilters]); + }; + + const onOperatorChange = ([operator]: Operator[], localFilterIndex: number) => { + const index = localFilters.findIndex((f) => f.id === localFilterIndex); + // Only reset params when the operator type changes + const params = + localFilters[localFilterIndex].operator?.type === operator.type + ? localFilters[localFilterIndex].value + : undefined; + const updatedLocalFilter = { ...localFilters[index], operator, value: params }; + localFilters[index] = updatedLocalFilter; + setLocalFilters([...localFilters]); + }; + + const onParamsChange = (newFilterParams: any, localFilterIndex: number) => { + const index = localFilters.findIndex((f) => f.id === localFilterIndex); + const updatedLocalFilter = { ...localFilters[index], value: newFilterParams }; + localFilters[index] = updatedLocalFilter; + setLocalFilters([...localFilters]); + }; + + const renderIndexPatternInput = () => { + if ( + indexPatterns.length <= 1 && + indexPatterns.find((indexPattern) => indexPattern === selectedIndexPattern) + ) { + /** + * Don't render the index pattern selector if there's just one \ zero index patterns + * and if the index pattern the filter was LOADED with is in the indexPatterns list. + **/ + + return ''; + } + return ( + + indexPattern.title} + onChange={onIndexPatternChange} + singleSelection={{ asPlainText: true }} + isClearable={false} + data-test-subj="filterIndexPatternsSelect" + /> + + ); + }; + + const renderFieldInput = (localFilterIndex: number) => { + const fields = selectedIndexPattern ? getFilterableFields(selectedIndexPattern) : []; + const selectedField = localFilters.filter( + (localFilter) => localFilter.id === localFilterIndex + )[0]?.field; + return ( + + + field.name} + onChange={(selected: IFieldType[]) => { + onFieldChange(selected, localFilterIndex); + }} + singleSelection={{ asPlainText: true }} + isClearable={false} + data-test-subj="filterFieldSuggestionList" + /> + + + ); + }; + + const renderOperatorInput = (localFilterIndex: number) => { + const selectedField = localFilters.filter( + (localFilter) => localFilter.id === localFilterIndex + )[0]?.field; + const operators = selectedField ? getOperatorOptions(selectedField) : []; + const selectedOperator = localFilters.filter( + (localFilter) => localFilter.id === localFilterIndex + )[0]?.operator; + + return ( + + + message} + onChange={(selected: Operator[]) => { + onOperatorChange(selected, localFilterIndex); + }} + singleSelection={{ asPlainText: true }} + isClearable={false} + data-test-subj="filterOperatorList" + /> + + + ); + }; + + const renderParamsEditor = (localFilterIndex: number) => { + const selectedOperator = localFilters.filter( + (localFilter) => localFilter.id === localFilterIndex + )[0]?.operator; + const selectedField = localFilters.filter( + (localFilter) => localFilter.id === localFilterIndex + )[0]?.field; + const selectedParams = localFilters.filter( + (localFilter) => localFilter.id === localFilterIndex + )[0]?.value; + switch (selectedOperator?.type) { + case 'exists': + return ''; + case 'phrases': + return ( + { + onParamsChange(newFilterParams, localFilterIndex); + }} + timeRangeForSuggestionsOverride={timeRangeForSuggestionsOverride} + fullWidth + compressed + /> + ); + case 'range': + return ( + { + onParamsChange(newFilterParams, localFilterIndex); + }} + fullWidth + compressed + /> + ); + default: + return ( + { + onParamsChange(newFilterParams, localFilterIndex); + }} + data-test-subj="phraseValueInput" + timeRangeForSuggestionsOverride={timeRangeForSuggestionsOverride} + fullWidth + compressed + /> + ); + } + }; + + const renderCustomEditor = () => { + return ( + + + + ); + }; + + const onUpdateFilter = () => { + const { $state } = filter; + if (!$state || !$state.store) { + return; // typescript validation + } + const alias = customLabel || null; + if (addFilterMode === 'query_builder') { + const { index, disabled = false, negate = false } = filter.meta; + const newIndex = index || indexPatterns[0].id!; + const body = JSON.parse(queryDsl); + const builtCustomFilter = buildCustomFilter( + newIndex, + body, + disabled, + negate, + alias, + $state.store + ); + onSubmit([builtCustomFilter]); + } else if (addFilterMode === 'quick_form' && selectedIndexPattern) { + const builtFilters = localFilters.map((localFilter) => { + if (localFilter.field && localFilter.operator) { + return buildFilter( + selectedIndexPattern, + localFilter.field, + localFilter.operator.type, + localFilter.operator.negate, + filter.meta.disabled ?? false, + localFilter.value ?? '', + alias, + $state.store + ); + } + }); + if (builtFilters && builtFilters.length) { + const finalFilters = builtFilters.filter( + (value) => typeof value !== 'undefined' + ) as Filter[]; + // onSubmit(finalFilters); + onMultipleFiltersSubmit(localFilters, finalFilters); + } + } else if (addFilterMode === 'saved_filters') { + applySavedQueries(); + } + }; + + const onApplyChangesFilter = () => { + onUpdateFilter(); + }; + + const onDeliteFilter = () => { + onRemoveFilterGroup(multipleFilters[0]?.groupId); + }; + + const renderGroupedFilters = () => { + const groupedFiltersNew = groupBy(localFilters, 'groupId'); + const GroupComponent: JSX.Element[] = []; + for (const [groupId, groupedFilters] of Object.entries(groupedFiltersNew)) { + const filtersInGroup = groupedFilters.length; + const groupBySubgroups = groupBy(groupedFilters, 'subGroupId'); + const subGroups = []; + for (const [_, subGroupedFilters] of Object.entries(groupBySubgroups)) { + subGroups.push(subGroupedFilters); + } + + const temp = ( +
1 && groupsCount > 1 ? 'kbnQueryBar__filterModalGroups' : '' + )} + > + {subGroups.map((subGroup, subGroupIdx) => { + const classes = + subGroup.length > 1 && groupsCount > 1 + ? 'kbnQueryBar__filterModalSubGroups' + : groupsCount === 1 && subGroup.length > 1 + ? 'kbnQueryBar__filterModalGroups' + : ''; + return ( + <> +
+ {subGroup.map((localfilter, index) => { + return ( + <> + + + + + {renderFieldInput(localfilter.id)} + {renderOperatorInput(localfilter.id)} + + + {renderParamsEditor(localfilter.id)} + + {subGroup.length < 2 && ( + + { + const updatedLocalFilter = { ...localfilter, relationship: 'OR' }; + const idx = localFilters.findIndex( + (f) => f.id === localfilter.id && f.groupId === Number(groupId) + ); + const subGroupId = (localfilter?.subGroupId ?? 0) + 1; + if (subGroup.length < 2) { + localFilters[idx] = updatedLocalFilter; + } + setLocalFilters([ + ...localFilters, + { + field: undefined, + operator: undefined, + value: undefined, + relationship: undefined, + groupId: localfilter.groupId, + id: localFilters.length, + subGroupId, + }, + ]); + }} + iconType="returnKey" + size="s" + aria-label="Add filter group with OR" + /> + + )} + + { + const filtersOnGroup = localFilters.filter( + (f) => f.groupId === Number(groupId) + ); + const subGroupId = + filtersOnGroup.length > 2 + ? localfilter?.subGroupId ?? 0 + : (localfilter?.subGroupId ?? 0) + 1; + const updatedLocalFilter = { + ...localfilter, + relationship: 'AND', + subGroupId: filtersOnGroup.length > 1 ? subGroupId : 1, + }; + const idx = localFilters.findIndex( + (f) => f.id === localfilter.id && f.groupId === Number(groupId) + ); + localFilters[idx] = updatedLocalFilter; + setLocalFilters([ + ...localFilters, + { + field: undefined, + operator: undefined, + value: undefined, + relationship: undefined, + groupId: filtersOnGroup.length > 1 ? groupsCount : groupsCount + 1, + subGroupId, + id: localFilters.length, + }, + ]); + if (filtersOnGroup.length <= 1) { + setGroupsCount(groupsCount + 1); + } + }} + iconType="plus" + size="s" + aria-label="Add filter group with AND" + /> + + {localFilters.length > 1 && ( + + { + const currentIdx = localFilters.findIndex( + (f) => f.id === localfilter.id + ); + if (currentIdx > 0) { + localFilters[currentIdx - 1].relationship = 'AND'; + } + const updatedFilters = localFilters.filter( + (_, idx) => idx !== localfilter.id + ); + const filtersOnGroup = updatedFilters.filter( + (f) => f.groupId === Number(groupId) + ); + if (filtersOnGroup.length < 1) { + setGroupsCount(groupsCount - 1); + } + setLocalFilters(updatedFilters); + }} + iconType="trash" + size="s" + color="danger" + aria-label="Delete filter group" + /> + + )} + + {localfilter.relationship && + localfilter.relationship === 'OR' && + subGroup.length === 0 && ( + <> + + + + + + + {' '} + OR{' '} + + + + + + + + )} + + ); + })} +
+ <> + {subGroup.length > 0 && subGroupIdx !== subGroups.length - 1 && ( + <> + + + + + + + {' '} + OR{' '} + + + + + + + + )} + + + ); + })} +
+ ); + GroupComponent.push(temp); + } + return GroupComponent; + }; + + return ( + + + +

+ {i18n.translate('data.filter.editFilterModal.headerTitle', { + defaultMessage: 'Edit filter', + })} +

+
+ {renderIndexPatternInput()} +
+ + + + {tabs.map(({ label, type }) => ( + setAddFilterMode(type)} + data-test-subj={`${type}FilterMode`} + > + {label} + + ))} + + + + + + + + {addFilterMode === 'quick_form' && renderGroupedFilters()} + {addFilterMode === 'query_builder' && renderCustomEditor()} + {addFilterMode === 'saved_filters' && savedQueryManagement} + + + + + + {addFilterMode !== 'saved_filters' && ( + + + setCustomLabel(e.target.value)} + compressed + /> + + + )} + + + + + {i18n.translate('xpack.lens.palette.saveModal.cancelLabel', { + defaultMessage: 'Cancel', + })} + + + + + + {i18n.translate('data.filter.addFilterModal.deleteFilterBtnLabel', { + defaultMessage: 'Delete filter', + })} + + + + + + {i18n.translate('data.filter.addFilterModal.applyChangesFilterBtnLabel', { + defaultMessage: 'Apply changes', + })} + + + + + + +
+ ); +} diff --git a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx index d188569004225..da0d41c0c9bf7 100644 --- a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx @@ -8,7 +8,7 @@ import dateMath from '@elastic/datemath'; import classNames from 'classnames'; -import React, { useCallback, useMemo, useRef, useState, ReactNode } from 'react'; +import React, { useCallback, useMemo, useRef, useState } from 'react'; import deepEqual from 'fast-deep-equal'; import { buildEmptyFilter, Filter } from '@kbn/es-query'; import useObservable from 'react-use/lib/useObservable'; @@ -22,10 +22,8 @@ import { EuiFieldText, prettyDuration, EuiIconProps, - EuiSuperUpdateButton, OnRefreshProps, EuiButtonIcon, - EuiPopover, } from '@elastic/eui'; import { IDataPluginServices, IIndexPattern, TimeRange, TimeHistoryContract, Query } from '../..'; import { mapAndFlattenFilters } from '../../query/filter_manager/lib/map_and_flatten_filters'; @@ -36,7 +34,6 @@ import { getQueryLog } from '../../query'; import type { PersistedLog } from '../../query'; import { NoDataPopover } from './no_data_popover'; import { shallowEqual } from '../../utils/shallow_equal'; -import { SavedQuery } from '../..'; import { AddFilterModal, FilterGroup } from './add_filter_modal'; import { SavedQueryMeta } from '../saved_query_form'; @@ -382,12 +379,6 @@ export const QueryBarTopRow = React.memo( } const onAddFilterClick = () => props.toggleAddFilterModal?.(!props.isAddFilterModalOpen); - function onAdd(filter: Filter) { - props.toggleAddFilterModal?.(false); - - const filters = [...props.filters, filter]; - props?.onFiltersUpdated?.(filters); - } function onAddMultipleFilters(selectedFilters: Filter[]) { props.toggleAddFilterModal?.(false); diff --git a/src/plugins/data/public/ui/search_bar/search_bar.tsx b/src/plugins/data/public/ui/search_bar/search_bar.tsx index 4dd6fbd29abb1..dfa2660793689 100644 --- a/src/plugins/data/public/ui/search_bar/search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/search_bar.tsx @@ -40,7 +40,6 @@ import type { SavedQueryAttributes, TimeHistoryContract, SavedQuery } from '../. import { IDataPluginServices } from '../../types'; import { TimeRange, IIndexPattern } from '../../../common'; import { FilterBar } from '../filter_bar/filter_bar'; -// import { FilterOptions } from '../filter_bar/filter_options'; import { SavedQueryMeta, SaveQueryForm } from '../saved_query_form'; import { SavedQueryManagementComponent } from '../saved_query_management'; import { FilterSetMenu } from '../saved_query_management/filter_set_menu'; @@ -119,7 +118,9 @@ interface State { dateRangeFrom: string; dateRangeTo: string; isAddFilterModalOpen?: boolean; + isEditFilterModalOpen?: boolean; addFilterMode?: string; + editFilterMode?: string; filtersIdsFromSavedQueries?: string[]; overrideTimeFilterModalShow: boolean; } @@ -203,12 +204,14 @@ class SearchBarUI extends Component { currentProps: this.props, selectedSavedQueries: [], finalSelectedSavedQueries: [], - multipleFilters: [], + multipleFilters: this.props.filters?.length ? [...this.props.filters] : [], query: this.props.query ? { ...this.props.query } : undefined, dateRangeFrom: get(this.props, 'dateRangeFrom', 'now-15m'), dateRangeTo: get(this.props, 'dateRangeTo', 'now'), isAddFilterModalOpen: false, + isEditFilterModalOpen: false, addFilterMode: 'quick_form', + editFilterMode: 'quick_form', filtersIdsFromSavedQueries: [], overrideTimeFilterModalShow: false, }; @@ -541,6 +544,13 @@ class SearchBarUI extends Component { }); }; + public toggleEditFilterModal = (value: boolean, editFilterMode?: string) => { + this.setState({ + isEditFilterModalOpen: value, + editFilterMode: editFilterMode || 'quick_form', + }); + }; + public toggleFilterSetPopover = (value: boolean) => { this.setState({ openFilterSetPopover: value, @@ -615,15 +625,6 @@ class SearchBarUI extends Component { savedQueryManagement={savedQueryManagement} applySelectedSavedQueries={this.applyTimeFilterOverrideModal} fillSubmitButton={this.props.fillSubmitButton || false} - // prepend={ - // this.props.showFilterBar && this.state.query - // ? this.renderSavedQueryManagement( - // this.props.onClearSavedQuery, - // this.props.showSaveQuery, - // this.props.savedQuery - // ) - // : undefined - // } showDatePicker={this.props.showDatePicker} dateRangeFrom={this.state.dateRangeFrom} dateRangeTo={this.state.dateRangeTo} @@ -714,6 +715,9 @@ class SearchBarUI extends Component { removeSelectedSavedQuery={this.removeSelectedSavedQuery} onMultipleFiltersUpdated={this.onMultipleFiltersUpdated} multipleFilters={this.state.multipleFilters} + toggleEditFilterModal={this.toggleEditFilterModal} + isEditFilterModalOpen={this.state.isEditFilterModalOpen} + editFilterMode={this.state.editFilterMode} savedQueryService={this.savedQueryService} onFilterSave={this.onSave} onFilterBadgeSave={this.onFilterBadgeSave}