diff --git a/src/plugins/unified_field_list/public/components/field_item_button/__snapshots__/field_item_button.test.tsx.snap b/src/plugins/unified_field_list/public/components/field_item_button/__snapshots__/field_item_button.test.tsx.snap index d1068660c1eaa..899d6a7579123 100644 --- a/src/plugins/unified_field_list/public/components/field_item_button/__snapshots__/field_item_button.test.tsx.snap +++ b/src/plugins/unified_field_list/public/components/field_item_button/__snapshots__/field_item_button.test.tsx.snap @@ -88,6 +88,35 @@ exports[`UnifiedFieldList renders properly for text-based co /> `; +exports[`UnifiedFieldList renders properly for wildcard search 1`] = ` + + } + fieldName={ + + script date + + } + isActive={false} + key="field-item-button-script date" + size="s" +/> +`; + exports[`UnifiedFieldList renders properly when a conflict field 1`] = ` ', () => { ); expect(component).toMatchSnapshot(); }); + + test('renders properly for wildcard search', () => { + const component = shallow( + + ); + expect(component).toMatchSnapshot(); + }); }); diff --git a/src/plugins/unified_field_list/public/components/field_item_button/field_item_button.tsx b/src/plugins/unified_field_list/public/components/field_item_button/field_item_button.tsx index df24125027e2f..ec0078431a8bc 100644 --- a/src/plugins/unified_field_list/public/components/field_item_button/field_item_button.tsx +++ b/src/plugins/unified_field_list/public/components/field_item_button/field_item_button.tsx @@ -14,6 +14,7 @@ import { EuiButtonIcon, EuiButtonIconProps, EuiHighlight, EuiIcon, EuiToolTip } import type { DataViewField } from '@kbn/data-views-plugin/common'; import { type FieldListItem, type GetCustomFieldType } from '../../types'; import { FieldIcon, getFieldIconProps } from '../field_icon'; +import { fieldNameWildcardMatcher } from '../../utils/field_name_wildcard_matcher'; import './field_item_button.scss'; /** @@ -194,7 +195,7 @@ export function FieldItemButton({ fieldIcon={} fieldName={ @@ -230,3 +231,15 @@ function FieldConflictInfoIcon() { ); } + +function getSearchHighlight(displayName: string, fieldSearchHighlight?: string): string { + const searchHighlight = fieldSearchHighlight || ''; + if ( + searchHighlight.includes('*') && + fieldNameWildcardMatcher({ name: displayName }, searchHighlight) + ) { + return displayName; + } + + return searchHighlight; +} diff --git a/src/plugins/unified_field_list/public/hooks/use_field_filters.test.tsx b/src/plugins/unified_field_list/public/hooks/use_field_filters.test.tsx index 77327fcbee402..1295d14f374b2 100644 --- a/src/plugins/unified_field_list/public/hooks/use_field_filters.test.tsx +++ b/src/plugins/unified_field_list/public/hooks/use_field_filters.test.tsx @@ -68,6 +68,33 @@ describe('UnifiedFieldList useFieldFilters()', () => { expect(result.current.onFilterField!(dataView.getFieldByName('bytes')!)).toBe(false); }); + it('should update correctly on search by name which has a wildcard', async () => { + const props: FieldFiltersParams = { + allFields: dataView.fields, + services: mockedServices, + }; + const { result } = renderHook(useFieldFilters, { + initialProps: props, + }); + + expect(result.current.fieldSearchHighlight).toBe(''); + expect(result.current.onFilterField).toBeUndefined(); + + act(() => { + result.current.fieldListFiltersProps.onChangeNameFilter('message*me1'); + }); + + expect(result.current.fieldSearchHighlight).toBe('message*me1'); + expect(result.current.onFilterField).toBeDefined(); + expect(result.current.onFilterField!({ displayName: 'test' } as DataViewField)).toBe(false); + expect(result.current.onFilterField!({ displayName: 'message' } as DataViewField)).toBe(false); + expect(result.current.onFilterField!({ displayName: 'message.name1' } as DataViewField)).toBe( + true + ); + expect(result.current.onFilterField!({ name: 'messagename10' } as DataViewField)).toBe(false); + expect(result.current.onFilterField!({ name: 'message.test' } as DataViewField)).toBe(false); + }); + it('should update correctly on filter by type', async () => { const props: FieldFiltersParams = { allFields: dataView.fields, diff --git a/src/plugins/unified_field_list/public/hooks/use_field_filters.ts b/src/plugins/unified_field_list/public/hooks/use_field_filters.ts index 803739caba7c6..c3e08ff335602 100644 --- a/src/plugins/unified_field_list/public/hooks/use_field_filters.ts +++ b/src/plugins/unified_field_list/public/hooks/use_field_filters.ts @@ -13,6 +13,7 @@ import type { CoreStart } from '@kbn/core-lifecycle-browser'; import { type FieldListFiltersProps } from '../components/field_list_filters'; import { type FieldListItem, type FieldTypeKnown, GetCustomFieldType } from '../types'; import { getFieldIconType } from '../utils/field_types'; +import { fieldNameWildcardMatcher } from '../utils/field_name_wildcard_matcher'; const htmlId = htmlIdGenerator('fieldList'); @@ -74,11 +75,7 @@ export function useFieldFilters({ onFilterField: fieldSearchHighlight?.length || selectedFieldTypes.length > 0 ? (field: T) => { - if ( - fieldSearchHighlight?.length && - !field.name?.toLowerCase().includes(fieldSearchHighlight) && - !field.displayName?.toLowerCase().includes(fieldSearchHighlight) - ) { + if (fieldSearchHighlight && !fieldNameWildcardMatcher(field, fieldSearchHighlight)) { return false; } if (selectedFieldTypes.length > 0) { diff --git a/src/plugins/unified_field_list/public/utils/field_name_wildcard_matcher.test.tsx b/src/plugins/unified_field_list/public/utils/field_name_wildcard_matcher.test.tsx new file mode 100644 index 0000000000000..2637ddf4046d2 --- /dev/null +++ b/src/plugins/unified_field_list/public/utils/field_name_wildcard_matcher.test.tsx @@ -0,0 +1,38 @@ +/* + * 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 DataViewField } from '@kbn/data-views-plugin/common'; +import { fieldNameWildcardMatcher } from './field_name_wildcard_matcher'; + +describe('UnifiedFieldList fieldNameWildcardMatcher()', () => { + it('should work correctly', async () => { + expect(fieldNameWildcardMatcher({ displayName: 'test' } as DataViewField, 'no')).toBe(false); + expect( + fieldNameWildcardMatcher({ displayName: 'test', name: 'yes' } as DataViewField, 'yes') + ).toBe(true); + + const search = 'test*ue'; + expect(fieldNameWildcardMatcher({ displayName: 'test' } as DataViewField, search)).toBe(false); + expect(fieldNameWildcardMatcher({ displayName: 'test.value' } as DataViewField, search)).toBe( + true + ); + expect(fieldNameWildcardMatcher({ name: 'test.this_value' } as DataViewField, search)).toBe( + true + ); + expect(fieldNameWildcardMatcher({ name: 'message.test' } as DataViewField, search)).toBe(false); + expect( + fieldNameWildcardMatcher({ name: 'test.this_value.maybe' } as DataViewField, search) + ).toBe(false); + expect( + fieldNameWildcardMatcher({ name: 'test.this_value.maybe' } as DataViewField, `${search}*`) + ).toBe(true); + expect( + fieldNameWildcardMatcher({ name: 'test.this_value.maybe' } as DataViewField, '*value*') + ).toBe(true); + }); +}); diff --git a/src/plugins/unified_field_list/public/utils/field_name_wildcard_matcher.ts b/src/plugins/unified_field_list/public/utils/field_name_wildcard_matcher.ts new file mode 100644 index 0000000000000..98b0e64b7bc78 --- /dev/null +++ b/src/plugins/unified_field_list/public/utils/field_name_wildcard_matcher.ts @@ -0,0 +1,34 @@ +/* + * 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 { escapeRegExp, memoize } from 'lodash'; + +const makeRegEx = memoize(function makeRegEx(glob: string) { + const globRegex = glob.split('*').map(escapeRegExp).join('.*'); + return new RegExp(globRegex.includes('*') ? `^${globRegex}$` : globRegex, 'i'); +}); + +/** + * Checks if field displayName or name matches the provided search string. + * The search string can have wildcard. + * @param field + * @param fieldSearchHighlight + */ +export const fieldNameWildcardMatcher = ( + field: { name: string; displayName?: string }, + fieldSearchHighlight: string +): boolean => { + if (!fieldSearchHighlight) { + return false; + } + + return ( + (!!field.displayName && makeRegEx(fieldSearchHighlight).test(field.displayName)) || + makeRegEx(fieldSearchHighlight).test(field.name) + ); +};