Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[EuiInMemoryTable] Allow consumers to use non-EQL plain text search with special characters #7175

Merged
merged 9 commits into from
Sep 18, 2023
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@
"@emotion/eslint-plugin": "^11.11.0",
"@emotion/jest": "^11.11.0",
"@emotion/react": "^11.11.0",
"@faker-js/faker": "^7.6.0",
"@faker-js/faker": "^8.0.2",
Copy link
Contributor Author

@cee-chen cee-chen Sep 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Upgrade needed for the new faker.string.symbol API I'm using to generate random special characters. Probably safe to do now that the faker controversy is behind us 🙈

"@pmmmwh/react-refresh-webpack-plugin": "^0.5.3",
"@storybook/addon-essentials": "^7.3.1",
"@storybook/addon-interactions": "^7.3.1",
Expand Down
61 changes: 34 additions & 27 deletions src-docs/src/views/tables/in_memory/in_memory_search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
EuiSpacer,
EuiSwitch,
EuiFlexGroup,
EuiFlexItem,
EuiCallOut,
EuiCode,
} from '../../../../../src/components';
Expand All @@ -27,16 +26,23 @@ type User = {
};

const users: User[] = [];
const usersWithSpecialCharacters: User[] = [];

for (let i = 0; i < 20; i++) {
users.push({
const userData = {
id: i + 1,
firstName: faker.name.firstName(),
lastName: faker.name.lastName(),
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
github: faker.internet.userName(),
dateOfBirth: faker.date.past(),
online: faker.datatype.boolean(),
location: faker.address.country(),
location: faker.location.country(),
};
users.push(userData);
usersWithSpecialCharacters.push({
...userData,
firstName: `${userData.firstName} "${faker.string.symbol(10)}"`,
lastName: `${userData.lastName} ${faker.internet.emoji()}`,
});
}

Expand Down Expand Up @@ -108,6 +114,7 @@ export default () => {
const [incremental, setIncremental] = useState(false);
const [filters, setFilters] = useState(false);
const [contentBetween, setContentBetween] = useState(false);
const [textSearchFormat, setTextSearchFormat] = useState(false);

const search: EuiSearchBarProps = {
box: {
Expand Down Expand Up @@ -138,34 +145,34 @@ export default () => {
return (
<>
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiSwitch
label="Incremental"
checked={incremental}
onChange={() => setIncremental(!incremental)}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiSwitch
label="With Filters"
checked={filters}
onChange={() => setFilters(!filters)}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiSwitch
label="Content between"
checked={contentBetween}
onChange={() => setContentBetween(!contentBetween)}
/>
</EuiFlexItem>
<EuiSwitch
label="Incremental"
checked={incremental}
onChange={() => setIncremental(!incremental)}
/>
<EuiSwitch
label="With Filters"
checked={filters}
onChange={() => setFilters(!filters)}
/>
<EuiSwitch
label="Content between"
checked={contentBetween}
onChange={() => setContentBetween(!contentBetween)}
/>
<EuiSwitch
label="Plain text search"
checked={textSearchFormat}
onChange={() => setTextSearchFormat(!textSearchFormat)}
/>
</EuiFlexGroup>
<EuiSpacer size="l" />
<EuiInMemoryTable
tableCaption="Demo of EuiInMemoryTable with search"
items={users}
items={textSearchFormat ? usersWithSpecialCharacters : users}
columns={columns}
search={search}
searchFormat={textSearchFormat ? 'text' : 'eql'}
pagination={true}
sorting={true}
childrenBetween={
Expand Down
30 changes: 30 additions & 0 deletions src/components/basic_table/in_memory_table.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1437,4 +1437,34 @@ describe('EuiInMemoryTable', () => {
expect(tableContent.at(2).text()).toBe('baz');
});
});

describe('text search format', () => {
it('allows searching for any text with special characters in it', () => {
const specialCharacterSearch =
'!@#$%^&*(){}+=-_hello:world"`<>?/👋~.,;|\\';
const items = [
{ title: specialCharacterSearch },
{ title: 'no special characters' },
];
const columns = [{ field: 'title', name: 'Title' }];

const { getByTestSubject, container } = render(
<EuiInMemoryTable
items={items}
searchFormat="text"
search={{ box: { incremental: true, 'data-test-subj': 'searchbox' } }}
columns={columns}
/>
);
fireEvent.keyUp(getByTestSubject('searchbox'), {
target: { value: specialCharacterSearch },
});

const tableContent = container.querySelectorAll(
'.euiTableRowCell .euiTableCellContent'
);
expect(tableContent).toHaveLength(1); // only 1 match
expect(tableContent[0]).toHaveTextContent(specialCharacterSearch);
});
});
});
67 changes: 57 additions & 10 deletions src/components/basic_table/in_memory_table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,15 @@ import { PropertySort } from '../../services';
import { Pagination as PaginationBarType } from './pagination_bar';
import { isString } from '../../services/predicate';
import { Comparators, Direction } from '../../services/sort';
import { EuiSearchBar, Query } from '../search_bar';
import {
EuiSearchBar,
EuiSearchBarProps,
Query,
SchemaType,
} from '../search_bar/search_bar';
import { EuiSearchBox } from '../search_bar/search_box';
import { EuiSpacer } from '../spacer';
import { CommonProps } from '../common';
import { EuiSearchBarProps } from '../search_bar/search_bar';
import { SchemaType } from '../search_bar/search_box';
import {
EuiTablePaginationProps,
euiTablePaginationDefaults,
Expand Down Expand Up @@ -76,6 +80,18 @@ type InMemoryTableProps<T> = Omit<
* Configures #Search.
*/
search?: Search;
/**
* By default, tables use `eql` format for search which allows using advanced filters.
*
* However, certain special characters (such as quotes, parentheses, and colons)
* are reserved for EQL syntax and will error if used.
* If your table does not require filter search and instead requires searching for certain
* symbols, use a plain `text` search format instead (note that filters will be ignored
* in this format).
*
* @default "eql"
*/
searchFormat?: 'eql' | 'text';
/**
* Configures #Pagination
*/
Expand Down Expand Up @@ -285,6 +301,7 @@ export class EuiInMemoryTable<T> extends Component<
static defaultProps = {
responsive: true,
tableLayout: 'fixed',
searchFormat: 'eql',
};
tableRef: React.RefObject<EuiBasicTable>;

Expand Down Expand Up @@ -521,9 +538,34 @@ export class EuiInMemoryTable<T> extends Component<
}));
};

// Alternative to onQueryChange - allows consumers to specify they want the
// search bar to ignore EQL syntax and only use the searchbar for plain text
onPlainTextSearch = (searchValue: string) => {
const escapedQueryText = searchValue.replace(/["\\]/g, '\\$&');
const finalQuery = `"${escapedQueryText}"`;
this.setState({
query: EuiSearchBar.Query.parse(finalQuery),
});
};

renderSearchBar() {
const { search } = this.props;
if (search) {
const { search, searchFormat } = this.props;
if (!search) return;

let searchBar: ReactNode;

if (searchFormat === 'text') {
const _searchBoxProps = (search as EuiSearchBarProps)?.box || {}; // Work around | boolean type
const { schema, ...searchBoxProps } = _searchBoxProps; // Destructure `schema` so it doesn't get rendered to DOM
cee-chen marked this conversation as resolved.
Show resolved Hide resolved

searchBar = (
<EuiSearchBox
query="" // Unused, passed to satisfy Typescript
{...searchBoxProps}
onSearch={this.onPlainTextSearch}
/>
);
} else {
let searchBarProps: Omit<EuiSearchBarProps, 'onChange'> = {};

if (isEuiSearchBarProps(search)) {
Expand All @@ -538,13 +580,17 @@ export class EuiInMemoryTable<T> extends Component<
}
}

return (
<>
<EuiSearchBar onChange={this.onQueryChange} {...searchBarProps} />
<EuiSpacer size="l" />
</>
searchBar = (
<EuiSearchBar onChange={this.onQueryChange} {...searchBarProps} />
);
}

return (
<>
{searchBar}
<EuiSpacer size="l" />
</>
);
}

resolveSearchSchema(): SchemaType {
Expand Down Expand Up @@ -653,6 +699,7 @@ export class EuiInMemoryTable<T> extends Component<
tableLayout,
items: _unuseditems,
search,
searchFormat,
onTableChange,
executeQueryOptions,
allowNeutralSort,
Expand Down
8 changes: 7 additions & 1 deletion src/components/search_bar/search_bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import React, { Component, ReactElement } from 'react';
import { htmlIdGenerator } from '../../services/accessibility';
import { isString } from '../../services/predicate';
import { EuiFlexGroup, EuiFlexItem } from '../flex';
import { EuiSearchBox, SchemaType } from './search_box';
import { EuiSearchBox } from './search_box';
import { EuiSearchBarFilters, SearchFilterConfig } from './search_filters';
import { Query } from './query';
import { CommonProps } from '../common';
Expand All @@ -36,6 +36,12 @@ interface ArgsWithError {
error: Error;
}

export interface SchemaType {
strict?: boolean;
fields?: any;
flags?: string[];
}

export type EuiSearchBarOnChangeArgs = ArgsWithQuery | ArgsWithError;

type HintPopOverProps = Partial<
Expand Down
Loading
Loading