Skip to content

Commit

Permalink
[Discover] In-table search (#206454)
Browse files Browse the repository at this point in the history
- Closes #192360

## Summary

The default browser Find-in-page does not work great with the grid
virtualization and our pagination and it can only find matches in rows
which are currently displayed.

This PR adds in-table search support to the grid so users can find
matches in all grid rows (up to `500` sample docs/rows by default) and
jump between them with "Previous"/"Next" buttons.

![Jan-24-2025
22-03-54](https://github.com/user-attachments/assets/95b31fb8-4740-4c5f-ba91-8e1c19066e02)

The implementation is extracted in a new package
`@kbn/data-grid-in-table-search`. This would allow to use in-table
search with `EuiDataGrid` on other pages of Kibana too.

`Cmd+F` shortcut is overridden when one of grid elements is in focus
otherwise we keep the browser default behaviour.

### Checklist

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [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
- [ ] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [ ] This was checked for breaking HTTP API changes, and any breaking
changes have been approved by the breaking-change committee. The
`release_note:breaking` label should be applied in these situations.
- [ ] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
- [x] The PR description includes the appropriate Release Notes section,
and the correct `release_note:*` label is applied per the
[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

---------

Co-authored-by: kibanamachine <[email protected]>
Co-authored-by: florent-leborgne <[email protected]>
  • Loading branch information
3 people authored Jan 29, 2025
1 parent a060bae commit 8ffb2ff
Show file tree
Hide file tree
Showing 48 changed files with 2,934 additions and 26 deletions.
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,7 @@ src/platform/packages/shared/kbn-content-management-utils @elastic/kibana-data-d
src/platform/packages/shared/kbn-crypto @elastic/kibana-security
src/platform/packages/shared/kbn-crypto-browser @elastic/kibana-core
src/platform/packages/shared/kbn-custom-icons @elastic/obs-ux-logs-team
src/platform/packages/shared/kbn-data-grid-in-table-search @elastic/kibana-data-discovery
src/platform/packages/shared/kbn-data-service @elastic/kibana-visualizations @elastic/kibana-data-discovery
src/platform/packages/shared/kbn-data-view-utils @elastic/kibana-data-discovery
src/platform/packages/shared/kbn-datemath @elastic/kibana-data-discovery
Expand Down
1 change: 1 addition & 0 deletions .i18nrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@
"unifiedFieldList": "src/platform/packages/shared/kbn-unified-field-list",
"unifiedHistogram": "src/platform/plugins/shared/unified_histogram",
"unifiedDataTable": "src/platform/packages/shared/kbn-unified-data-table",
"dataGridInTableSearch": "src/platform/packages/shared/kbn-data-grid-in-table-search",
"unsavedChangesBadge": "src/platform/packages/private/kbn-unsaved-changes-badge",
"unsavedChangesPrompt": "src/platform/packages/shared/kbn-unsaved-changes-prompt",
"managedContentBadge": "src/platform/packages/private/kbn-managed-content-badge",
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,7 @@
"@kbn/dashboard-enhanced-plugin": "link:x-pack/platform/plugins/shared/dashboard_enhanced",
"@kbn/dashboard-plugin": "link:src/platform/plugins/shared/dashboard",
"@kbn/data-forge": "link:x-pack/platform/packages/shared/kbn-data-forge",
"@kbn/data-grid-in-table-search": "link:src/platform/packages/shared/kbn-data-grid-in-table-search",
"@kbn/data-plugin": "link:src/platform/plugins/shared/data",
"@kbn/data-quality-plugin": "link:x-pack/platform/plugins/shared/data_quality",
"@kbn/data-search-plugin": "link:test/plugin_functional/plugins/data_search",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# @kbn/data-grid-in-table-search

This package allows to extend `EuiDataGrid` with in-table search.

If you are already using `UnifiedDataTable` component, you can enable in-table search simply by passing `enableInTableSearch` prop to it.

```tsx
<UnifiedDataTable
enableInTableSearch={true}
// ... other props
/>
```

If you are using `EuiDataGrid` directly, you can enable in-table search by importing this package and following these steps:

1. include `useDataGridInTableSearch` hook in your component
2. pass `inTableSearchControl` to `EuiDataGrid` inside `additionalControls.right` prop or `renderCustomToolbar`
3. pass `inTableSearchCss` to the grid container element as `css` prop
4. update your `cellContext` prop with `cellContextWithInTableSearchSupport`
5. update your `renderCellValue` prop with `renderCellValueWithInTableSearchSupport`.

```tsx
import { useDataGridInTableSearch } from '@kbn/data-grid-in-table-search';

// ...

const dataGridRef = useRef<EuiDataGridRefProps>(null);
const [dataGridWrapper, setDataGridWrapper] = useState<HTMLElement | null>(null);

// ...

const { inTableSearchTermCss, inTableSearchControl, cellContextWithInTableSearchSupport, renderCellValueWithInTableSearchSupport } =
useDataGridInTableSearch({
dataGridWrapper,
dataGridRef,
visibleColumns,
rows,
cellContext,
renderCellValue,
pagination,
});

const toolbarVisibility: EuiDataGridProps['toolbarVisibility'] = useMemo(
() => ({
additionalControls: inTableSearchControl ? { right: inTableSearchControl } : false,
// ...
}),
[inTableSearchControl]
);

// ...
<div ref={(node) => setDataGridWrapper(node)} css={inTableSearchCss}>
<EuiDataGrid
ref={dataGridRef}
toolbarVisibility={toolbarVisibility}
renderCellValue={renderCellValueWithInTableSearchSupport}
cellContext={cellContextWithInTableSearchSupport}
pagination={pagination}
// ... other props
/>
</div>
```

An example of how to use this package can be found in `kbn-data-grid-in-table-search/__mocks__/data_grid_example.tsx`
or in `kbn-unified-data-table` package.






Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

export {
useDataGridInTableSearch,
type UseDataGridInTableSearchProps,
type UseDataGridInTableSearchReturn,
} from './src';

export {
BUTTON_TEST_SUBJ,
COUNTER_TEST_SUBJ,
BUTTON_PREV_TEST_SUBJ,
BUTTON_NEXT_TEST_SUBJ,
INPUT_TEST_SUBJ,
HIGHLIGHT_CLASS_NAME,
} from './src/constants';
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

module.exports = {
preset: '@kbn/test',
rootDir: '../../../../..',
roots: ['<rootDir>/src/platform/packages/shared/kbn-data-grid-in-table-search'],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "shared-browser",
"id": "@kbn/data-grid-in-table-search",
"owner": "@elastic/kibana-data-discovery",
"group": "platform",
"visibility": "shared"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "@kbn/data-grid-in-table-search",
"private": true,
"version": "1.0.0",
"license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0",
"sideEffects": false
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

export const generateMockData = (rowsCount: number, columnsCount: number) => {
const testData: string[][] = [];

Array.from({ length: rowsCount }).forEach((_, i) => {
const row: string[] = [];
Array.from({ length: columnsCount }).forEach((__, j) => {
row.push(`cell-in-row-${i}-col-${j}`);
});
testData.push(row);
});

return testData;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import React, { useMemo, useRef, useState } from 'react';
import { EuiDataGrid, EuiDataGridProps, EuiDataGridRefProps } from '@elastic/eui';
import { generateMockData } from './data';
import { getRenderCellValueMock } from './render_cell_value_mock';
import { useDataGridInTableSearch } from '../use_data_grid_in_table_search';

export interface DataGridWithInTableSearchExampleProps {
rowsCount: number;
columnsCount: number;
pageSize: number | null;
}

export const DataGridWithInTableSearchExample: React.FC<DataGridWithInTableSearchExampleProps> = ({
rowsCount,
columnsCount,
pageSize,
}) => {
const dataGridRef = useRef<EuiDataGridRefProps>(null);
const [dataGridWrapper, setDataGridWrapper] = useState<HTMLElement | null>(null);

const sampleData = useMemo(
() => generateMockData(rowsCount, columnsCount),
[rowsCount, columnsCount]
);
const columns = useMemo(
() => Array.from({ length: columnsCount }, (_, i) => ({ id: `column-${i}` })),
[columnsCount]
);

const [visibleColumns, setVisibleColumns] = useState(columns.map(({ id }) => id));

const renderCellValue = useMemo(() => getRenderCellValueMock(sampleData), [sampleData]);

const isPaginationEnabled = typeof pageSize === 'number';
const [pageIndex, setPageIndex] = useState(0);
const pagination = useMemo(() => {
return isPaginationEnabled
? {
onChangePage: setPageIndex,
onChangeItemsPerPage: () => {},
pageIndex,
pageSize,
}
: undefined;
}, [isPaginationEnabled, setPageIndex, pageSize, pageIndex]);

const {
inTableSearchTermCss,
inTableSearchControl,
cellContextWithInTableSearchSupport,
renderCellValueWithInTableSearchSupport,
} = useDataGridInTableSearch({
dataGridWrapper,
dataGridRef,
visibleColumns,
rows: sampleData,
cellContext: undefined,
renderCellValue,
pagination,
});

const toolbarVisibility: EuiDataGridProps['toolbarVisibility'] = useMemo(
() => ({
additionalControls: inTableSearchControl ? { right: inTableSearchControl } : false,
}),
[inTableSearchControl]
);

return (
<div ref={(node) => setDataGridWrapper(node)} css={inTableSearchTermCss}>
<EuiDataGrid
ref={dataGridRef}
aria-label="Data grid with in-table search"
columns={columns}
columnVisibility={{ visibleColumns, setVisibleColumns }}
toolbarVisibility={toolbarVisibility}
rowCount={rowsCount}
pagination={pagination}
cellContext={cellContextWithInTableSearchSupport}
renderCellValue={renderCellValueWithInTableSearchSupport}
width={800}
height={200}
/>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

export { generateMockData } from './data';
export { getRenderCellValueMock } from './render_cell_value_mock';
export { DataGridWithInTableSearchExample } from './data_grid_example';
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import React from 'react';
import type { EuiDataGridCellValueElementProps } from '@elastic/eui';

export function getRenderCellValueMock(testData: string[][]) {
return function OriginalRenderCellValue({
colIndex,
rowIndex,
}: EuiDataGridCellValueElementProps) {
const cellValue = testData[rowIndex][colIndex];

if (!cellValue) {
throw new Error('Testing unexpected errors');
}

return <div>{cellValue}</div>;
};
}
Loading

0 comments on commit 8ffb2ff

Please sign in to comment.