diff --git a/packages/eui/changelogs/upcoming/9142.md b/packages/eui/changelogs/upcoming/9142.md
new file mode 100644
index 00000000000..b1df289a90d
--- /dev/null
+++ b/packages/eui/changelogs/upcoming/9142.md
@@ -0,0 +1,3 @@
+**Bug fixes**
+
+- Fixed `EuiInMemoryTable` support for controlled search for plain text (when `searchFormat="text"`) by properly handling `search.query` and `search.defaulQuery`
diff --git a/packages/eui/src/components/basic_table/in_memory_table.test.tsx b/packages/eui/src/components/basic_table/in_memory_table.test.tsx
index ecd940ba313..817ceee1527 100644
--- a/packages/eui/src/components/basic_table/in_memory_table.test.tsx
+++ b/packages/eui/src/components/basic_table/in_memory_table.test.tsx
@@ -1533,7 +1533,7 @@ describe('EuiInMemoryTable', () => {
);
expect(mockOnChange).toHaveBeenCalledTimes(1);
expect(mockOnChange).toHaveBeenCalledWith({
- query: Query.parse(`"${TEXT}"`),
+ query: null,
queryText: TEXT,
error: null,
});
@@ -1569,5 +1569,108 @@ describe('EuiInMemoryTable', () => {
);
expect(tableContent).toHaveLength(2); // 2 matches for "ba"
});
+
+ it('supports controlled operation, query is reflected in the search box', () => {
+ const items = [{ title: 'foo' }, { title: 'bar' }, { title: 'baz' }];
+ const columns = [{ field: 'title', name: 'Title' }];
+ const CONTROLLED_QUERY = 'controlled search';
+
+ const { getByTestSubject, rerender } = render(
+
+ );
+
+ const searchbox = getByTestSubject('searchbox') as HTMLInputElement;
+ expect(searchbox.value).toBe(CONTROLLED_QUERY);
+
+ // Update the controlled query
+ const UPDATED_QUERY = 'updated search';
+ rerender(
+
+ );
+
+ expect(searchbox.value).toBe(UPDATED_QUERY);
+ });
+
+ it('renders defaultQuery in the search box on initial render', () => {
+ const items = [{ title: 'foo' }, { title: 'bar' }, { title: 'baz' }];
+ const columns = [{ field: 'title', name: 'Title' }];
+ const DEFAULT_QUERY = 'default search text';
+
+ const { getByTestSubject } = render(
+
+ );
+
+ const searchbox = getByTestSubject('searchbox') as HTMLInputElement;
+
+ expect(searchbox.value).toBe(DEFAULT_QUERY);
+
+ fireEvent.keyUp(searchbox, {
+ target: { value: 'something else' },
+ });
+
+ expect(searchbox.value).toBe('something else');
+ });
+
+ it('ignores Query objects in the search box', () => {
+ const items = [{ title: 'foo' }, { title: 'bar' }, { title: 'baz' }];
+ const columns = [{ field: 'title', name: 'Title' }];
+ const query = Query.parse('ba');
+
+ const { getByTestSubject, rerender } = render(
+
+ );
+
+ const searchbox = getByTestSubject('searchbox') as HTMLInputElement;
+
+ // not `[object Object]`
+ expect(searchbox.value).toBe('');
+
+ rerender(
+
+ );
+
+ // not `[object Object]`
+ expect(searchbox.value).toBe('');
+ });
});
});
diff --git a/packages/eui/src/components/basic_table/in_memory_table.tsx b/packages/eui/src/components/basic_table/in_memory_table.tsx
index 03d636e1110..5ac3b93fd9d 100644
--- a/packages/eui/src/components/basic_table/in_memory_table.tsx
+++ b/packages/eui/src/components/basic_table/in_memory_table.tsx
@@ -85,7 +85,11 @@ type InMemoryTableProps = Omit<
*/
noItemsMessage?: ReactNode;
/**
- * Configures {@link Search}.
+ * Configures the search bar. Can be `true` for defaults,
+ * or an {@link EuiSearchBarProps} object.
+ *
+ * When `searchFormat="text"`, `query` and `defaultQuery` must be strings
+ * ({@link Query} objects are ignored).
*/
search?: Search;
/**
@@ -93,6 +97,7 @@ type InMemoryTableProps = Omit<
*
* 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).
@@ -159,7 +164,7 @@ interface State {
search?: Search;
};
search?: Search;
- query: Query | null;
+ query: Query | string | null;
pageIndex: number;
pageSize?: number;
pageSizeOptions?: number[];
@@ -169,23 +174,34 @@ interface State {
showPerPageOptions: boolean | undefined;
}
+/**
+ * Extracts and formats a query from search props based on the search format
+ * @param search - The search configuration
+ * @param defaultQuery - Whether to use the defaultQuery property as fallback
+ * @param searchFormat - The search format: 'eql' for parsed queries, 'text' for plain text
+ * @returns Formatted query string or Query object
+ */
const getQueryFromSearch = (
search: Search | undefined,
- defaultQuery: boolean
-) => {
- let query: Query | string;
+ defaultQuery: boolean,
+ searchFormat: InMemoryTableProps<{}>['searchFormat']
+): Query | string => {
if (!search) {
- query = '';
- } else {
- query =
- (defaultQuery
- ? (search as EuiSearchBarProps).defaultQuery ||
- (search as EuiSearchBarProps).query ||
- ''
- : (search as EuiSearchBarProps).query) || '';
+ return searchFormat === 'text' ? '""' : '';
+ }
+
+ const searchProps = search as EuiSearchBarProps;
+ const queryString = defaultQuery
+ ? searchProps.defaultQuery ?? searchProps.query ?? ''
+ : searchProps.query ?? '';
+
+ if (searchFormat === 'text') {
+ return `"${queryString}"`;
}
- return isString(query) ? EuiSearchBar.Query.parse(query) : query;
+ return isString(queryString)
+ ? EuiSearchBar.Query.parse(queryString)
+ : queryString;
};
const getInitialPagination = (
@@ -398,7 +414,11 @@ export class EuiInMemoryTable extends Component<
...updatedPrevState.prevProps,
search: nextProps.search,
},
- query: getQueryFromSearch(nextProps.search, false),
+ query: getQueryFromSearch(
+ nextProps.search,
+ false,
+ nextProps.searchFormat ?? 'eql'
+ ),
};
}
if (updatedPrevState !== prevState) {
@@ -423,7 +443,7 @@ export class EuiInMemoryTable extends Component<
search,
},
search: search,
- query: getQueryFromSearch(search, true),
+ query: getQueryFromSearch(search, true, props.searchFormat ?? 'eql'),
pageIndex: pageIndex || 0,
pageSize,
pageSizeOptions,
@@ -542,13 +562,12 @@ export class EuiInMemoryTable extends Component<
// 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}"`;
const { search } = this.props;
if (isEuiSearchBarProps(search)) {
if (search.onChange) {
const shouldQueryInMemory = search.onChange({
- query: EuiSearchBar.Query.parse(finalQuery),
+ query: null,
queryText: escapedQueryText,
error: null,
});
@@ -559,7 +578,7 @@ export class EuiInMemoryTable extends Component<
}
this.setState({
- query: EuiSearchBar.Query.parse(finalQuery),
+ query: `"${escapedQueryText}"`,
});
};
@@ -570,13 +589,37 @@ export class EuiInMemoryTable extends Component<
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
+ const { box = {}, query, defaultQuery } = search as EuiSearchBarProps;
+ const {
+ schema, // destructure `schema` so it doesn't get rendered to DOM
+ ...searchBoxProps
+ } = box;
+
+ // in the unexpected case a Query object is passed with searchFormat=text
+ if (process.env.NODE_ENV === 'development') {
+ if (query != null && !isString(query)) {
+ console.warn(
+ 'EuiInMemoryTable: `query` should be a string when using searchFormat="text". Query objects are only supported with searchFormat="eql".'
+ );
+ }
+ if (defaultQuery != null && !isString(defaultQuery)) {
+ console.warn(
+ 'EuiInMemoryTable: `defaultQuery` should be a string when using searchFormat="text". Query objects are only supported with searchFormat="eql".'
+ );
+ }
+ }
+
+ // use only string values, ignore Query objects
+ const displayQuery = isString(query)
+ ? query
+ : isString(defaultQuery)
+ ? defaultQuery
+ : '';
searchBar = (
);
diff --git a/packages/eui/src/components/search_bar/search_bar.tsx b/packages/eui/src/components/search_bar/search_bar.tsx
index ed4f5da1ba1..c57ce764fd2 100644
--- a/packages/eui/src/components/search_bar/search_bar.tsx
+++ b/packages/eui/src/components/search_bar/search_bar.tsx
@@ -34,6 +34,16 @@ interface ArgsWithQuery {
error: null;
}
+/**
+ * When `searchFormat` is 'text', `query` is null and the search is performed
+ * on the `queryText` directly without EQL parsing
+ */
+interface ArgsWithPlainText {
+ query: null;
+ queryText: string;
+ error: null;
+}
+
interface ArgsWithError {
query: null;
queryText: string;
@@ -48,7 +58,10 @@ export interface SchemaType {
recognizedFields?: string[];
}
-export type EuiSearchBarOnChangeArgs = ArgsWithQuery | ArgsWithError;
+export type EuiSearchBarOnChangeArgs =
+ | ArgsWithQuery
+ | ArgsWithPlainText
+ | ArgsWithError;
type HintPopOverProps = Partial<
Pick<
@@ -72,17 +85,17 @@ export interface EuiSearchBarProps extends CommonProps {
onChange?: (args: EuiSearchBarOnChangeArgs) => void | boolean;
/**
- The initial query the bar will hold when first mounted
+ * The initial query the bar will hold when first mounted
*/
defaultQuery?: QueryType;
/**
- If you wish to use the search bar as a controlled component, continuously pass the query via this prop.
+ * If you wish to use the search bar as a controlled component, continuously pass the query via this prop.
*/
query?: QueryType;
/**
- Configures the search box. Set `placeholder` to change the placeholder text in the box and `incremental` to support incremental (as you type) search.
+ * Configures the search box. Set `placeholder` to change the placeholder text in the box and `incremental` to support incremental (as you type) search.
*/
box?: EuiFieldSearchProps & {
// Boolean values are not meaningful to this EuiSearchBox, but are allowed so that other
@@ -92,7 +105,7 @@ export interface EuiSearchBarProps extends CommonProps {
};
/**
- An array of search filters. See {@link SearchFilterConfig}.
+ * An array of search filters. See {@link SearchFilterConfig}.
*/
filters?: SearchFilterConfig[];