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 8f937d86164177..01ad05c19c6d8b 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_bar.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_bar.tsx @@ -33,6 +33,8 @@ import { SavedQueriesItem } from './saved_queries_item'; import { FilterExpressionItem } from './filter_expression_item'; import { UI_SETTINGS } from '../../../common'; +import { SavedQueryMeta } from '../saved_query_form'; +import { SavedQueryService } from '../..'; interface Props { filters: Filter[]; @@ -46,6 +48,9 @@ interface Props { selectedSavedQueries?: SavedQuery[]; removeSelectedSavedQuery: (savedQuery: SavedQuery) => void; onMultipleFiltersUpdated?: (filters: Filter[]) => void; + savedQueryService: SavedQueryService; + onFilterSave: (savedQueryMeta: SavedQueryMeta, saveAsNew?: boolean) => Promise; + onFilterBadgeSave: (groupId: number, alias: string) => void; } const FilterBarUI = React.memo(function FilterBarUI(props: Props) { @@ -98,7 +103,13 @@ const FilterBarUI = React.memo(function FilterBarUI(props: Props) { } function renderMultipleFilters() { - const firstDepthGroupedFilters = groupBy(props.multipleFilters, 'groupId'); + const groupedByAlias = groupBy(props.multipleFilters, 'meta.alias'); + const filtersWithoutLabel = groupedByAlias.null || groupedByAlias.undefined; + const labels = Object.keys(groupedByAlias).filter( + (key) => key !== 'null' && key !== 'undefined' + ); + + const firstDepthGroupedFilters = groupBy(filtersWithoutLabel, 'groupId'); const GroupBadge: JSX.Element[] = []; for (const [groupId, groupedFilters] of Object.entries(firstDepthGroupedFilters)) { const badge = ( @@ -110,10 +121,34 @@ const FilterBarUI = React.memo(function FilterBarUI(props: Props) { onRemove={onRemoveFilterGroup} onUpdate={onUpdateFilterGroup} filtersGroupsCount={Object.entries(firstDepthGroupedFilters).length} + savedQueryService={props.savedQueryService} + onFilterSave={props.onFilterSave} + onFilterBadgeSave={props.onFilterBadgeSave} /> ); GroupBadge.push(badge); } + + let groupId: string; + labels.map((label) => { + // we should have same groupIds on our labeled filters group + groupId = (groupedByAlias[label][0] as any).groupId; + groupedByAlias[label].forEach((filter) => ((filter as any).groupId = groupId)); + const labelBadge = ( + {}} + onRemove={onRemoveFilterGroup} + onUpdate={onUpdateFilterGroup} + filtersGroupsCount={1} + customLabel={label} + /> + ); + GroupBadge.push(labelBadge); + }); + return GroupBadge; } 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 8745db2e560b05..ed22ec9ea9bef5 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 @@ -13,6 +13,8 @@ import { EuiTextColor, EuiPopover, EuiContextMenu, + EuiIcon, + EuiContextMenuPanelDescriptor, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { groupBy } from 'lodash'; @@ -22,6 +24,8 @@ import { FILTERS } from '../../../common'; import { existsOperator, isOneOfOperator } from './filter_editor/lib/filter_operators'; import { IIndexPattern } from '../..'; import { getDisplayValueFromFilter, getIndexPatternFromFilter } from '../../query'; +import { SavedQueryMeta, SaveQueryForm } from '../saved_query_form'; +import { SavedQueryService } from '../..'; const FILTER_ITEM_OK = ''; const FILTER_ITEM_WARNING = 'warn'; @@ -46,6 +50,10 @@ interface Props { groupId: string; filtersGroupsCount: number; onUpdate?: (filters: Filter[], groupId: string, toggleNegate: boolean) => void; + savedQueryService?: SavedQueryService; + onFilterSave?: (savedQueryMeta: SavedQueryMeta, saveAsNew?: boolean) => Promise; + customLabel?: string; + onFilterBadgeSave?: (groupId: number, alias: string) => void; } export const FilterExpressionItem: FC = ({ @@ -56,8 +64,17 @@ export const FilterExpressionItem: FC = ({ groupId, filtersGroupsCount, onUpdate, + savedQueryService, + onFilterSave, + customLabel, + onFilterBadgeSave, }: Props) => { const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const filters: Filter[] = groupedFilters.map((filter: Filter) => ({ + $state: filter.$state, + meta: filter.meta, + query: filter.query, + })); function handleBadgeClick() { // if (e.shiftKey) { // onToggleDisabled(); @@ -149,7 +166,7 @@ export const FilterExpressionItem: FC = ({ }, ]; - return [ + const panels: EuiContextMenuPanelDescriptor[] = [ { id: 0, items: mainPanelItems, @@ -172,6 +189,45 @@ export const FilterExpressionItem: FC = ({ // ), // }, ]; + + if (!customLabel && savedQueryService && onFilterSave && onFilterBadgeSave) { + const saveAsFilterPanelItem = { + name: i18n.translate('data.filter.filterBar.saveAsFilterButtonLabel', { + defaultMessage: `Save as filter`, + }), + icon: 'save', + panel: 2, + 'data-test-subj': 'saveAsFilter', + }; + + const saveAsFilterPanelContent = { + id: 2, + title: i18n.translate('data.filter.filterBar.saveAsFilterButtonLabel', { + defaultMessage: `Save as filter`, + }), + content: ( +
+ { + onFilterSave(savedQueryMeta, true); + setIsPopoverOpen(false); + }} + onClose={() => setIsPopoverOpen(false)} + showTimeFilterOption={false} + showFilterOption={false} + filters={filters} + onFilterBadgeSave={(alias: string) => onFilterBadgeSave(Number(groupId), alias)} + /> +
+ ), + }; + + mainPanelItems.splice(mainPanelItems.length - 1, 0, saveAsFilterPanelItem); + panels.push(saveAsFilterPanelContent); + } + + return panels; } /** * Checks if filter field exists in any of the index patterns provided, @@ -406,9 +462,16 @@ export const FilterExpressionItem: FC = ({ onClick={handleBadgeClick} >
- {filterExpression.map((expression) => { - return <>{expression}; - })} + {customLabel ? ( + <> + + {customLabel} + + ) : ( + filterExpression.map((expression) => { + return <>{expression}; + }) + )}
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 7840a8ea6ea084..cf997f1a196c0f 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 @@ -51,6 +51,7 @@ 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 { SavedQueryMeta } from '../saved_query_form'; import { IIndexPattern, IFieldType } from '../..'; @@ -95,6 +96,7 @@ export function AddFilterModal({ timeRangeForSuggestionsOverride, savedQueryManagement, initialAddFilterMode, + saveFilters, }: { onSubmit: (filters: Filter[]) => void; onMultipleFiltersSubmit: (filters: FilterGroup[], buildFilters: Filter[]) => void; @@ -105,6 +107,7 @@ export function AddFilterModal({ timeRangeForSuggestionsOverride?: boolean; savedQueryManagement?: JSX.Element; initialAddFilterMode?: string; + saveFilters: (savedQueryMeta: SavedQueryMeta) => void; }) { const [selectedIndexPattern, setSelectedIndexPattern] = useState( getIndexPatternFromFilter(filter, indexPatterns) @@ -370,6 +373,13 @@ export function AddFilterModal({ $state.store ); onSubmit([builtCustomFilter]); + saveFilters({ + title: customLabel, + description: '', + shouldIncludeFilters: false, + shouldIncludeTimefilter: false, + filters: [builtCustomFilter], + }); } else if (addFilterMode === 'quick_form' && selectedIndexPattern) { const builtFilters = localFilters.map((localFilter) => { if (localFilter.field && localFilter.operator) { @@ -391,6 +401,15 @@ export function AddFilterModal({ ) as Filter[]; // onSubmit(finalFilters); onMultipleFiltersSubmit(localFilters, finalFilters); + if (alias) { + saveFilters({ + title: customLabel, + description: '', + shouldIncludeFilters: false, + shouldIncludeTimefilter: false, + filters: finalFilters, + }); + } } } else if (addFilterMode === 'saved_filters') { applySavedQueries(); 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 ef6af7b7d2b74c..d1885690042250 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 @@ -38,6 +38,7 @@ 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'; const SuperDatePicker = React.memo( EuiSuperDatePicker as any @@ -49,6 +50,7 @@ const QueryStringInput = withKibana(QueryStringInputUI); // @internal export interface QueryBarTopRowProps { filters: Filter[]; + multipleFilters: Filter[]; onFiltersUpdated?: (filters: Filter[]) => void; onMultipleFiltersUpdated?: (filters: Filter[]) => void; applySelectedSavedQueries?: () => void; @@ -85,6 +87,7 @@ export interface QueryBarTopRowProps { toggleAddFilterModal?: (value: boolean) => void; isAddFilterModalOpen?: boolean; addFilterMode?: string; + onNewFiltersSave: (savedQueryMeta: SavedQueryMeta) => void; } const SharingMetaFields = React.memo(function SharingMetaFields({ @@ -394,18 +397,30 @@ export const QueryBarTopRow = React.memo( } function onAddMultipleFiltersANDOR(selectedFilters: FilterGroup[], buildFilters: Filter[]) { + const lastFilter: any = props.multipleFilters[props.multipleFilters.length - 1]; const mappedFilters = mapAndFlattenFilters(buildFilters); + if (lastFilter !== undefined) lastFilter.relationship = 'AND'; const mergedFilters = mappedFilters.map((filter, idx) => { + let groupId = selectedFilters[idx].groupId; + let id = selectedFilters[idx].id; + // groupId starts from 1; id starts from 0 + + if (lastFilter !== undefined) { + groupId += lastFilter.groupId; + id += lastFilter.id + 1; + } + return { ...filter, - groupId: selectedFilters[idx].groupId, - id: selectedFilters[idx].id, + groupId, + id, relationship: selectedFilters[idx].relationship, subGroupId: selectedFilters[idx].subGroupId, }; }); props.toggleAddFilterModal?.(false); - props?.onMultipleFiltersUpdated?.(mergedFilters); + props?.onMultipleFiltersUpdated?.([...props.multipleFilters, ...mergedFilters]); + // props?.onMultipleFiltersUpdated?.(mergedFilters); const filters = [...props.filters, ...buildFilters]; props?.onFiltersUpdated?.(filters); @@ -448,6 +463,7 @@ export const QueryBarTopRow = React.memo( timeRangeForSuggestionsOverride={props.timeRangeForSuggestionsOverride} savedQueryManagement={props.savedQueryManagement} initialAddFilterMode={props.addFilterMode} + saveFilters={props.onNewFiltersSave} /> )} diff --git a/src/plugins/data/public/ui/saved_query_form/save_query_form.tsx b/src/plugins/data/public/ui/saved_query_form/save_query_form.tsx index 75ea4237370a51..83cbbb7d4ef79a 100644 --- a/src/plugins/data/public/ui/saved_query_form/save_query_form.tsx +++ b/src/plugins/data/public/ui/saved_query_form/save_query_form.tsx @@ -9,6 +9,7 @@ import React, { useEffect, useState, useCallback } from 'react'; import { EuiButton, EuiForm, EuiFormRow, EuiFieldText, EuiSwitch, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { Filter } from '@kbn/es-query'; import { sortBy, isEqual } from 'lodash'; import { SavedQuery, SavedQueryService } from '../..'; @@ -19,6 +20,8 @@ interface Props { onClose: () => void; showFilterOption: boolean | undefined; showTimeFilterOption: boolean | undefined; + filters?: Filter[]; + onFilterBadgeSave?: (alias: string) => void; } export interface SavedQueryMeta { @@ -27,6 +30,7 @@ export interface SavedQueryMeta { description: string; shouldIncludeFilters: boolean; shouldIncludeTimefilter: boolean; + filters?: Filter[]; } export function SaveQueryForm({ @@ -36,6 +40,8 @@ export function SaveQueryForm({ onClose, showFilterOption = true, showTimeFilterOption = true, + filters, + onFilterBadgeSave, }: Props) { const [title, setTitle] = useState(savedQuery?.attributes.title ?? ''); const [enabledSaveButton, setEnabledSaveButton] = useState(Boolean(savedQuery)); @@ -111,7 +117,9 @@ export function SaveQueryForm({ description, shouldIncludeFilters, shouldIncludeTimefilter, + filters, }); + if (onFilterBadgeSave) onFilterBadgeSave(title); } }, [ validate, @@ -121,6 +129,8 @@ export function SaveQueryForm({ description, shouldIncludeFilters, shouldIncludeTimefilter, + filters, + onFilterBadgeSave, ]); const onInputChange = useCallback((event) => { 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 27631b84112df8..4dd6fbd29abb18 100644 --- a/src/plugins/data/public/ui/search_bar/search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/search_bar.tsx @@ -266,7 +266,9 @@ class SearchBarUI extends Component { query: this.state.query, }; - if (savedQueryMeta.shouldIncludeFilters) { + if (savedQueryMeta.filters !== undefined) { + savedQueryAttributes.filters = savedQueryMeta.filters; + } else { savedQueryAttributes.filters = this.props.filters; } @@ -308,7 +310,7 @@ class SearchBarUI extends Component { openFilterSetPopover: false, }); - if (this.props.onSaved) { + if (!savedQueryMeta.filters && this.props.onSaved) { this.props.onSaved(response); } } catch (error) { @@ -383,6 +385,21 @@ class SearchBarUI extends Component { // console.dir(filters); }; + public onFilterBadgeSave = (groupId: number, alias: string) => { + const multipleFilters = this.state.multipleFilters.map((filter: any) => { + if (Number(filter.groupId) === groupId) + return { + ...filter, + meta: { + ...filter.meta, + alias, + }, + }; + return filter; + }); + this.setState({ multipleFilters }); + }; + public applyTimeFilterOverrideModal = (selectedQueries?: SavedQuery[]) => { const queries = [...(selectedQueries || []), ...this.state.selectedSavedQueries]; this.setState({ finalSelectedSavedQueries: queries }); @@ -589,6 +606,7 @@ class SearchBarUI extends Component { filters={this.props.filters!} onFiltersUpdated={this.props.onFiltersUpdated} onMultipleFiltersUpdated={this.onMultipleFiltersUpdated} + multipleFilters={this.state.multipleFilters} screenTitle={this.props.screenTitle} onSubmit={this.onQueryBarSubmit} indexPatterns={this.props.indexPatterns} @@ -631,6 +649,7 @@ class SearchBarUI extends Component { toggleAddFilterModal={this.toggleAddFilterModal} isAddFilterModalOpen={this.state.isAddFilterModalOpen} addFilterMode={this.state.addFilterMode} + onNewFiltersSave={(savedQueryMeta) => this.onSave(savedQueryMeta, true)} /> ); } @@ -695,6 +714,9 @@ class SearchBarUI extends Component { removeSelectedSavedQuery={this.removeSelectedSavedQuery} onMultipleFiltersUpdated={this.onMultipleFiltersUpdated} multipleFilters={this.state.multipleFilters} + savedQueryService={this.savedQueryService} + onFilterSave={this.onSave} + onFilterBadgeSave={this.onFilterBadgeSave} /> );