Skip to content

Commit 0c6ccb0

Browse files
authored
[EuiInMemoryTable] Allow consumers to use non-EQL plain text search with special characters (#7175)
1 parent d1b54f6 commit 0c6ccb0

File tree

8 files changed

+208
-111
lines changed

8 files changed

+208
-111
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@
127127
"@emotion/eslint-plugin": "^11.11.0",
128128
"@emotion/jest": "^11.11.0",
129129
"@emotion/react": "^11.11.0",
130-
"@faker-js/faker": "^7.6.0",
130+
"@faker-js/faker": "^8.0.2",
131131
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.3",
132132
"@storybook/addon-essentials": "^7.3.1",
133133
"@storybook/addon-interactions": "^7.3.1",

src-docs/src/views/tables/in_memory/in_memory_search.tsx

Lines changed: 34 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {
1111
EuiSpacer,
1212
EuiSwitch,
1313
EuiFlexGroup,
14-
EuiFlexItem,
1514
EuiCallOut,
1615
EuiCode,
1716
} from '../../../../../src/components';
@@ -27,16 +26,23 @@ type User = {
2726
};
2827

2928
const users: User[] = [];
29+
const usersWithSpecialCharacters: User[] = [];
3030

3131
for (let i = 0; i < 20; i++) {
32-
users.push({
32+
const userData = {
3333
id: i + 1,
34-
firstName: faker.name.firstName(),
35-
lastName: faker.name.lastName(),
34+
firstName: faker.person.firstName(),
35+
lastName: faker.person.lastName(),
3636
github: faker.internet.userName(),
3737
dateOfBirth: faker.date.past(),
3838
online: faker.datatype.boolean(),
39-
location: faker.address.country(),
39+
location: faker.location.country(),
40+
};
41+
users.push(userData);
42+
usersWithSpecialCharacters.push({
43+
...userData,
44+
firstName: `${userData.firstName} "${faker.string.symbol(10)}"`,
45+
lastName: `${userData.lastName} ${faker.internet.emoji()}`,
4046
});
4147
}
4248

@@ -108,6 +114,7 @@ export default () => {
108114
const [incremental, setIncremental] = useState(false);
109115
const [filters, setFilters] = useState(false);
110116
const [contentBetween, setContentBetween] = useState(false);
117+
const [textSearchFormat, setTextSearchFormat] = useState(false);
111118

112119
const search: EuiSearchBarProps = {
113120
box: {
@@ -138,34 +145,34 @@ export default () => {
138145
return (
139146
<>
140147
<EuiFlexGroup>
141-
<EuiFlexItem grow={false}>
142-
<EuiSwitch
143-
label="Incremental"
144-
checked={incremental}
145-
onChange={() => setIncremental(!incremental)}
146-
/>
147-
</EuiFlexItem>
148-
<EuiFlexItem grow={false}>
149-
<EuiSwitch
150-
label="With Filters"
151-
checked={filters}
152-
onChange={() => setFilters(!filters)}
153-
/>
154-
</EuiFlexItem>
155-
<EuiFlexItem grow={false}>
156-
<EuiSwitch
157-
label="Content between"
158-
checked={contentBetween}
159-
onChange={() => setContentBetween(!contentBetween)}
160-
/>
161-
</EuiFlexItem>
148+
<EuiSwitch
149+
label="Incremental"
150+
checked={incremental}
151+
onChange={() => setIncremental(!incremental)}
152+
/>
153+
<EuiSwitch
154+
label="With Filters"
155+
checked={filters}
156+
onChange={() => setFilters(!filters)}
157+
/>
158+
<EuiSwitch
159+
label="Content between"
160+
checked={contentBetween}
161+
onChange={() => setContentBetween(!contentBetween)}
162+
/>
163+
<EuiSwitch
164+
label="Plain text search"
165+
checked={textSearchFormat}
166+
onChange={() => setTextSearchFormat(!textSearchFormat)}
167+
/>
162168
</EuiFlexGroup>
163169
<EuiSpacer size="l" />
164170
<EuiInMemoryTable
165171
tableCaption="Demo of EuiInMemoryTable with search"
166-
items={users}
172+
items={textSearchFormat ? usersWithSpecialCharacters : users}
167173
columns={columns}
168174
search={search}
175+
searchFormat={textSearchFormat ? 'text' : 'eql'}
169176
pagination={true}
170177
sorting={true}
171178
childrenBetween={

src/components/basic_table/in_memory_table.test.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1437,4 +1437,34 @@ describe('EuiInMemoryTable', () => {
14371437
expect(tableContent.at(2).text()).toBe('baz');
14381438
});
14391439
});
1440+
1441+
describe('text search format', () => {
1442+
it('allows searching for any text with special characters in it', () => {
1443+
const specialCharacterSearch =
1444+
'!@#$%^&*(){}+=-_hello:world"`<>?/👋~.,;|\\';
1445+
const items = [
1446+
{ title: specialCharacterSearch },
1447+
{ title: 'no special characters' },
1448+
];
1449+
const columns = [{ field: 'title', name: 'Title' }];
1450+
1451+
const { getByTestSubject, container } = render(
1452+
<EuiInMemoryTable
1453+
items={items}
1454+
searchFormat="text"
1455+
search={{ box: { incremental: true, 'data-test-subj': 'searchbox' } }}
1456+
columns={columns}
1457+
/>
1458+
);
1459+
fireEvent.keyUp(getByTestSubject('searchbox'), {
1460+
target: { value: specialCharacterSearch },
1461+
});
1462+
1463+
const tableContent = container.querySelectorAll(
1464+
'.euiTableRowCell .euiTableCellContent'
1465+
);
1466+
expect(tableContent).toHaveLength(1); // only 1 match
1467+
expect(tableContent[0]).toHaveTextContent(specialCharacterSearch);
1468+
});
1469+
});
14401470
});

src/components/basic_table/in_memory_table.tsx

Lines changed: 57 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,15 @@ import { PropertySort } from '../../services';
2323
import { Pagination as PaginationBarType } from './pagination_bar';
2424
import { isString } from '../../services/predicate';
2525
import { Comparators, Direction } from '../../services/sort';
26-
import { EuiSearchBar, Query } from '../search_bar';
26+
import {
27+
EuiSearchBar,
28+
EuiSearchBarProps,
29+
Query,
30+
SchemaType,
31+
} from '../search_bar/search_bar';
32+
import { EuiSearchBox } from '../search_bar/search_box';
2733
import { EuiSpacer } from '../spacer';
2834
import { CommonProps } from '../common';
29-
import { EuiSearchBarProps } from '../search_bar/search_bar';
30-
import { SchemaType } from '../search_bar/search_box';
3135
import {
3236
EuiTablePaginationProps,
3337
euiTablePaginationDefaults,
@@ -76,6 +80,18 @@ type InMemoryTableProps<T> = Omit<
7680
* Configures #Search.
7781
*/
7882
search?: Search;
83+
/**
84+
* By default, tables use `eql` format for search which allows using advanced filters.
85+
*
86+
* However, certain special characters (such as quotes, parentheses, and colons)
87+
* are reserved for EQL syntax and will error if used.
88+
* If your table does not require filter search and instead requires searching for certain
89+
* symbols, use a plain `text` search format instead (note that filters will be ignored
90+
* in this format).
91+
*
92+
* @default "eql"
93+
*/
94+
searchFormat?: 'eql' | 'text';
7995
/**
8096
* Configures #Pagination
8197
*/
@@ -285,6 +301,7 @@ export class EuiInMemoryTable<T> extends Component<
285301
static defaultProps = {
286302
responsive: true,
287303
tableLayout: 'fixed',
304+
searchFormat: 'eql',
288305
};
289306
tableRef: React.RefObject<EuiBasicTable>;
290307

@@ -521,9 +538,34 @@ export class EuiInMemoryTable<T> extends Component<
521538
}));
522539
};
523540

541+
// Alternative to onQueryChange - allows consumers to specify they want the
542+
// search bar to ignore EQL syntax and only use the searchbar for plain text
543+
onPlainTextSearch = (searchValue: string) => {
544+
const escapedQueryText = searchValue.replace(/["\\]/g, '\\$&');
545+
const finalQuery = `"${escapedQueryText}"`;
546+
this.setState({
547+
query: EuiSearchBar.Query.parse(finalQuery),
548+
});
549+
};
550+
524551
renderSearchBar() {
525-
const { search } = this.props;
526-
if (search) {
552+
const { search, searchFormat } = this.props;
553+
if (!search) return;
554+
555+
let searchBar: ReactNode;
556+
557+
if (searchFormat === 'text') {
558+
const _searchBoxProps = (search as EuiSearchBarProps)?.box || {}; // Work around | boolean type
559+
const { schema, ...searchBoxProps } = _searchBoxProps; // Destructure `schema` so it doesn't get rendered to DOM
560+
561+
searchBar = (
562+
<EuiSearchBox
563+
query="" // Unused, passed to satisfy Typescript
564+
{...searchBoxProps}
565+
onSearch={this.onPlainTextSearch}
566+
/>
567+
);
568+
} else {
527569
let searchBarProps: Omit<EuiSearchBarProps, 'onChange'> = {};
528570

529571
if (isEuiSearchBarProps(search)) {
@@ -538,13 +580,17 @@ export class EuiInMemoryTable<T> extends Component<
538580
}
539581
}
540582

541-
return (
542-
<>
543-
<EuiSearchBar onChange={this.onQueryChange} {...searchBarProps} />
544-
<EuiSpacer size="l" />
545-
</>
583+
searchBar = (
584+
<EuiSearchBar onChange={this.onQueryChange} {...searchBarProps} />
546585
);
547586
}
587+
588+
return (
589+
<>
590+
{searchBar}
591+
<EuiSpacer size="l" />
592+
</>
593+
);
548594
}
549595

550596
resolveSearchSchema(): SchemaType {
@@ -653,6 +699,7 @@ export class EuiInMemoryTable<T> extends Component<
653699
tableLayout,
654700
items: _unuseditems,
655701
search,
702+
searchFormat,
656703
onTableChange,
657704
executeQueryOptions,
658705
allowNeutralSort,

src/components/search_bar/search_bar.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import React, { Component, ReactElement } from 'react';
1111
import { htmlIdGenerator } from '../../services/accessibility';
1212
import { isString } from '../../services/predicate';
1313
import { EuiFlexGroup, EuiFlexItem } from '../flex';
14-
import { EuiSearchBox, SchemaType } from './search_box';
14+
import { EuiSearchBox } from './search_box';
1515
import { EuiSearchBarFilters, SearchFilterConfig } from './search_filters';
1616
import { Query } from './query';
1717
import { CommonProps } from '../common';
@@ -36,6 +36,12 @@ interface ArgsWithError {
3636
error: Error;
3737
}
3838

39+
export interface SchemaType {
40+
strict?: boolean;
41+
fields?: any;
42+
flags?: string[];
43+
}
44+
3945
export type EuiSearchBarOnChangeArgs = ArgsWithQuery | ArgsWithError;
4046

4147
type HintPopOverProps = Partial<

0 commit comments

Comments
 (0)