Skip to content

Commit

Permalink
[UnifiedFieldList] Allow wildcards in field search (#155540)
Browse files Browse the repository at this point in the history
Closes #97459

## Summary

This PR allows to search by field names with wildcard.

<img width="318" alt="Screenshot 2023-04-24 at 10 47 18"
src="https://user-images.githubusercontent.com/1415710/233946401-5588b757-9ba7-4e4f-adda-4ad4a936f528.png">


### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
  • Loading branch information
jughosta authored Apr 25, 2023
1 parent 2a6f46a commit c5d6364
Show file tree
Hide file tree
Showing 7 changed files with 158 additions and 6 deletions.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -147,4 +147,18 @@ describe('UnifiedFieldList <FieldItemButton />', () => {
);
expect(component).toMatchSnapshot();
});

test('renders properly for wildcard search', () => {
const component = shallow(
<FieldItemButton
field={scriptedField}
fieldSearchHighlight="sc*te"
isEmpty={false}
isSelected={false}
isActive={false}
onClick={undefined}
/>
);
expect(component).toMatchSnapshot();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand Down Expand Up @@ -194,7 +195,7 @@ export function FieldItemButton<T extends FieldListItem = DataViewField>({
fieldIcon={<FieldIcon {...iconProps} />}
fieldName={
<EuiHighlight
search={fieldSearchHighlight || ''}
search={getSearchHighlight(displayName, fieldSearchHighlight)}
title={title}
data-test-subj={`field-${field.name}`}
>
Expand Down Expand Up @@ -230,3 +231,15 @@ function FieldConflictInfoIcon() {
</EuiToolTip>
);
}

function getSearchHighlight(displayName: string, fieldSearchHighlight?: string): string {
const searchHighlight = fieldSearchHighlight || '';
if (
searchHighlight.includes('*') &&
fieldNameWildcardMatcher({ name: displayName }, searchHighlight)
) {
return displayName;
}

return searchHighlight;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<DataViewField> = {
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<DataViewField> = {
allFields: dataView.fields,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down Expand Up @@ -74,11 +75,7 @@ export function useFieldFilters<T extends FieldListItem = DataViewField>({
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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
});
});
Original file line number Diff line number Diff line change
@@ -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)
);
};

0 comments on commit c5d6364

Please sign in to comment.