mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Discover] Some cleanups for the new in-table search (#208980)
- Addresses https://github.com/elastic/kibana/issues/208939 ## Summary This PR makes some cleanups to the code introduced in https://github.com/elastic/kibana/pull/206454 and adds more tests. ### 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
This commit is contained in:
parent
d4199dcac1
commit
b1b28c3258
12 changed files with 496 additions and 33 deletions
|
@ -10,3 +10,4 @@
|
|||
export { generateMockData } from './data';
|
||||
export { getRenderCellValueMock } from './render_cell_value_mock';
|
||||
export { DataGridWithInTableSearchExample } from './data_grid_example';
|
||||
export { MockContext, useMockContextValue } from './mock_context';
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* 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';
|
||||
|
||||
interface MockContextValue {
|
||||
mockContextValue?: string;
|
||||
}
|
||||
|
||||
export const MockContext = React.createContext<MockContextValue>({});
|
||||
|
||||
export const useMockContextValue = () => React.useContext(MockContext).mockContextValue;
|
|
@ -9,18 +9,28 @@
|
|||
|
||||
import React from 'react';
|
||||
import type { EuiDataGridCellValueElementProps } from '@elastic/eui';
|
||||
import { useMockContextValue } from './mock_context';
|
||||
|
||||
export function getRenderCellValueMock(testData: string[][]) {
|
||||
return function OriginalRenderCellValue({
|
||||
colIndex,
|
||||
rowIndex,
|
||||
}: EuiDataGridCellValueElementProps) {
|
||||
const mockContextValue = useMockContextValue();
|
||||
const cellValue = testData[rowIndex][colIndex];
|
||||
|
||||
if (!cellValue) {
|
||||
throw new Error('Testing unexpected errors');
|
||||
}
|
||||
|
||||
return <div>{cellValue}</div>;
|
||||
return (
|
||||
<div>
|
||||
{cellValue}
|
||||
{
|
||||
// testing that it can access the parent context value
|
||||
mockContextValue ? <span>{mockContextValue}</span> : null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -44,7 +44,6 @@ exports[`InTableSearchInput renders input 1`] = `
|
|||
class="euiText emotion-euiText-s-euiTextColor-subdued"
|
||||
>
|
||||
5/10
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
@ -140,7 +139,6 @@ exports[`InTableSearchInput renders input when loading 1`] = `
|
|||
class="euiText emotion-euiText-s-euiTextColor-subdued"
|
||||
>
|
||||
0/0
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
|
|
@ -0,0 +1,386 @@
|
|||
/*
|
||||
* 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 { render, waitFor, screen } from '@testing-library/react';
|
||||
import { InTableSearchControl, InTableSearchControlProps } from './in_table_search_control';
|
||||
import {
|
||||
CELL_MATCH_INDEX_ATTRIBUTE,
|
||||
COUNTER_TEST_SUBJ,
|
||||
HIGHLIGHT_CLASS_NAME,
|
||||
BUTTON_NEXT_TEST_SUBJ,
|
||||
} from './constants';
|
||||
import { wrapRenderCellValueWithInTableSearchSupport } from './wrap_render_cell_value';
|
||||
import { getRenderCellValueMock } from './__mocks__';
|
||||
|
||||
describe('InTableSearchControl', () => {
|
||||
const testData = [
|
||||
['aaaa', '100'],
|
||||
['bbb', 'abb'],
|
||||
['abc', 'aaac'],
|
||||
];
|
||||
|
||||
const testData2 = [
|
||||
['bb', 'cc'],
|
||||
['bc', 'caa'],
|
||||
];
|
||||
|
||||
const visibleColumns = Array.from({ length: 2 }, (_, i) => `column${i}`);
|
||||
const getColumnIndexFromId = (columnId: string) => parseInt(columnId.replace('column', ''), 10);
|
||||
|
||||
it('should update correctly when deps change', async () => {
|
||||
const initialProps: InTableSearchControlProps = {
|
||||
inTableSearchTerm: 'a',
|
||||
pageSize: 10,
|
||||
visibleColumns,
|
||||
rows: testData,
|
||||
renderCellValue: jest.fn(
|
||||
wrapRenderCellValueWithInTableSearchSupport(getRenderCellValueMock(testData))
|
||||
),
|
||||
getColumnIndexFromId: jest.fn(getColumnIndexFromId),
|
||||
scrollToCell: jest.fn(),
|
||||
shouldOverrideCmdF: jest.fn(),
|
||||
onChange: jest.fn(),
|
||||
onChangeCss: jest.fn(),
|
||||
onChangeToExpectedPage: jest.fn(),
|
||||
};
|
||||
|
||||
const { rerender } = render(<InTableSearchControl {...initialProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId(COUNTER_TEST_SUBJ)).toHaveTextContent('1/9');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(initialProps.onChangeToExpectedPage).toHaveBeenCalledWith(0);
|
||||
});
|
||||
|
||||
expect(initialProps.getColumnIndexFromId).toHaveBeenCalledWith('column0');
|
||||
expect(initialProps.scrollToCell).toHaveBeenCalledWith({
|
||||
align: 'center',
|
||||
columnIndex: 0,
|
||||
rowIndex: 0,
|
||||
});
|
||||
expect(initialProps.onChange).not.toHaveBeenCalled();
|
||||
expect(initialProps.onChangeCss).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
styles: expect.stringContaining(
|
||||
"[data-gridcell-row-index='0'][data-gridcell-column-id='column0']"
|
||||
),
|
||||
})
|
||||
);
|
||||
expect(initialProps.onChangeCss).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
styles: expect.stringContaining(
|
||||
`.${HIGHLIGHT_CLASS_NAME}[${CELL_MATCH_INDEX_ATTRIBUTE}='0']`
|
||||
),
|
||||
})
|
||||
);
|
||||
|
||||
rerender(
|
||||
<InTableSearchControl
|
||||
{...initialProps}
|
||||
rows={testData2}
|
||||
renderCellValue={jest.fn(
|
||||
wrapRenderCellValueWithInTableSearchSupport(getRenderCellValueMock(testData2))
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId(COUNTER_TEST_SUBJ)).toHaveTextContent('1/2');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(initialProps.onChangeToExpectedPage).toHaveBeenNthCalledWith(2, 0);
|
||||
});
|
||||
|
||||
expect(initialProps.getColumnIndexFromId).toHaveBeenLastCalledWith('column1');
|
||||
expect(initialProps.scrollToCell).toHaveBeenLastCalledWith({
|
||||
align: 'center',
|
||||
columnIndex: 1,
|
||||
rowIndex: 1,
|
||||
});
|
||||
expect(initialProps.onChange).not.toHaveBeenCalled();
|
||||
expect(initialProps.onChangeCss).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
styles: expect.stringContaining(
|
||||
"[data-gridcell-row-index='1'][data-gridcell-column-id='column1']"
|
||||
),
|
||||
})
|
||||
);
|
||||
expect(initialProps.onChangeCss).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
styles: expect.stringContaining(
|
||||
`.${HIGHLIGHT_CLASS_NAME}[${CELL_MATCH_INDEX_ATTRIBUTE}='0']`
|
||||
),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should update correctly when search term changes', async () => {
|
||||
const initialProps: InTableSearchControlProps = {
|
||||
inTableSearchTerm: 'aa',
|
||||
pageSize: null,
|
||||
visibleColumns,
|
||||
rows: testData,
|
||||
renderCellValue: jest.fn(
|
||||
wrapRenderCellValueWithInTableSearchSupport(getRenderCellValueMock(testData))
|
||||
),
|
||||
getColumnIndexFromId: jest.fn(getColumnIndexFromId),
|
||||
scrollToCell: jest.fn(),
|
||||
shouldOverrideCmdF: jest.fn(),
|
||||
onChange: jest.fn(),
|
||||
onChangeCss: jest.fn(),
|
||||
onChangeToExpectedPage: jest.fn(),
|
||||
};
|
||||
|
||||
const { rerender } = render(<InTableSearchControl {...initialProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId(COUNTER_TEST_SUBJ)).toHaveTextContent('1/3');
|
||||
});
|
||||
|
||||
rerender(<InTableSearchControl {...initialProps} inTableSearchTerm="b" />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId(COUNTER_TEST_SUBJ)).toHaveTextContent('1/6');
|
||||
});
|
||||
});
|
||||
|
||||
it('should change pages correctly', async () => {
|
||||
const initialProps: InTableSearchControlProps = {
|
||||
inTableSearchTerm: 'abc',
|
||||
pageSize: 2,
|
||||
visibleColumns,
|
||||
rows: testData,
|
||||
renderCellValue: jest.fn(
|
||||
wrapRenderCellValueWithInTableSearchSupport(getRenderCellValueMock(testData))
|
||||
),
|
||||
getColumnIndexFromId: jest.fn(getColumnIndexFromId),
|
||||
scrollToCell: jest.fn(),
|
||||
shouldOverrideCmdF: jest.fn(),
|
||||
onChange: jest.fn(),
|
||||
onChangeCss: jest.fn(),
|
||||
onChangeToExpectedPage: jest.fn(),
|
||||
};
|
||||
|
||||
const { rerender } = render(<InTableSearchControl {...initialProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId(COUNTER_TEST_SUBJ)).toHaveTextContent('1/1');
|
||||
});
|
||||
|
||||
expect(initialProps.onChangeToExpectedPage).toHaveBeenCalledWith(1);
|
||||
|
||||
rerender(<InTableSearchControl {...initialProps} inTableSearchTerm="c" pageSize={1} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId(COUNTER_TEST_SUBJ)).toHaveTextContent('1/2');
|
||||
});
|
||||
|
||||
expect(initialProps.onChangeToExpectedPage).toHaveBeenNthCalledWith(2, 2);
|
||||
|
||||
rerender(<InTableSearchControl {...initialProps} inTableSearchTerm="100" />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId(COUNTER_TEST_SUBJ)).toHaveTextContent('1/1');
|
||||
});
|
||||
|
||||
expect(initialProps.onChangeToExpectedPage).toHaveBeenNthCalledWith(3, 0);
|
||||
|
||||
rerender(<InTableSearchControl {...initialProps} inTableSearchTerm="random" />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId(COUNTER_TEST_SUBJ)).toHaveTextContent('0/0');
|
||||
});
|
||||
|
||||
rerender(<InTableSearchControl {...initialProps} inTableSearchTerm="100" pageSize={null} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId(COUNTER_TEST_SUBJ)).toHaveTextContent('1/1');
|
||||
});
|
||||
|
||||
expect(initialProps.onChangeToExpectedPage).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('should highlight the active match correctly', async () => {
|
||||
const initialProps: InTableSearchControlProps = {
|
||||
inTableSearchTerm: 'aa',
|
||||
pageSize: 2,
|
||||
visibleColumns,
|
||||
rows: testData,
|
||||
renderCellValue: jest.fn(
|
||||
wrapRenderCellValueWithInTableSearchSupport(getRenderCellValueMock(testData))
|
||||
),
|
||||
getColumnIndexFromId: jest.fn(getColumnIndexFromId),
|
||||
scrollToCell: jest.fn(),
|
||||
shouldOverrideCmdF: jest.fn(),
|
||||
onChange: jest.fn(),
|
||||
onChangeCss: jest.fn(),
|
||||
onChangeToExpectedPage: jest.fn(),
|
||||
};
|
||||
|
||||
render(<InTableSearchControl {...initialProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId(COUNTER_TEST_SUBJ)).toHaveTextContent('1/3');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(initialProps.onChangeToExpectedPage).toHaveBeenCalledWith(0);
|
||||
});
|
||||
|
||||
expect(initialProps.scrollToCell).toHaveBeenCalledWith({
|
||||
align: 'center',
|
||||
columnIndex: 0,
|
||||
rowIndex: 0,
|
||||
});
|
||||
expect(initialProps.onChangeCss).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
styles: expect.stringContaining(
|
||||
"[data-gridcell-row-index='0'][data-gridcell-column-id='column0']"
|
||||
),
|
||||
})
|
||||
);
|
||||
expect(initialProps.onChangeCss).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
styles: expect.stringContaining(
|
||||
`.${HIGHLIGHT_CLASS_NAME}[${CELL_MATCH_INDEX_ATTRIBUTE}='0']`
|
||||
),
|
||||
})
|
||||
);
|
||||
|
||||
screen.getByTestId(BUTTON_NEXT_TEST_SUBJ).click();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId(COUNTER_TEST_SUBJ)).toHaveTextContent('2/3');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(initialProps.onChangeToExpectedPage).toHaveBeenNthCalledWith(2, 0);
|
||||
});
|
||||
|
||||
expect(initialProps.scrollToCell).toHaveBeenNthCalledWith(2, {
|
||||
align: 'center',
|
||||
columnIndex: 0,
|
||||
rowIndex: 0,
|
||||
});
|
||||
expect(initialProps.onChangeCss).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({
|
||||
styles: expect.stringContaining(
|
||||
"[data-gridcell-row-index='0'][data-gridcell-column-id='column0']"
|
||||
),
|
||||
})
|
||||
);
|
||||
expect(initialProps.onChangeCss).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({
|
||||
styles: expect.stringContaining(
|
||||
`.${HIGHLIGHT_CLASS_NAME}[${CELL_MATCH_INDEX_ATTRIBUTE}='1']`
|
||||
),
|
||||
})
|
||||
);
|
||||
|
||||
screen.getByTestId(BUTTON_NEXT_TEST_SUBJ).click();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId(COUNTER_TEST_SUBJ)).toHaveTextContent('3/3');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(initialProps.onChangeToExpectedPage).toHaveBeenNthCalledWith(3, 1);
|
||||
});
|
||||
|
||||
expect(initialProps.scrollToCell).toHaveBeenNthCalledWith(3, {
|
||||
align: 'center',
|
||||
columnIndex: 1,
|
||||
rowIndex: 0,
|
||||
});
|
||||
expect(initialProps.onChangeCss).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
expect.objectContaining({
|
||||
styles: expect.stringContaining(
|
||||
"[data-gridcell-row-index='2'][data-gridcell-column-id='column1']"
|
||||
),
|
||||
})
|
||||
);
|
||||
expect(initialProps.onChangeCss).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
expect.objectContaining({
|
||||
styles: expect.stringContaining(
|
||||
`.${HIGHLIGHT_CLASS_NAME}[${CELL_MATCH_INDEX_ATTRIBUTE}='0']`
|
||||
),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle timeouts', async () => {
|
||||
const initialProps: InTableSearchControlProps = {
|
||||
inTableSearchTerm: 'aa',
|
||||
pageSize: null,
|
||||
visibleColumns,
|
||||
rows: testData,
|
||||
renderCellValue: jest.fn(
|
||||
wrapRenderCellValueWithInTableSearchSupport(getRenderCellValueMock(testData))
|
||||
),
|
||||
getColumnIndexFromId: jest.fn(getColumnIndexFromId),
|
||||
scrollToCell: jest.fn(),
|
||||
shouldOverrideCmdF: jest.fn(),
|
||||
onChange: jest.fn(),
|
||||
onChangeCss: jest.fn(),
|
||||
onChangeToExpectedPage: jest.fn(),
|
||||
};
|
||||
|
||||
const { rerender } = render(<InTableSearchControl {...initialProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId(COUNTER_TEST_SUBJ)).toHaveTextContent('1/3');
|
||||
});
|
||||
|
||||
rerender(<InTableSearchControl {...initialProps} renderCellValue={jest.fn()} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId(COUNTER_TEST_SUBJ)).toHaveTextContent('0/0');
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle ignore errors in cells', async () => {
|
||||
const initialProps: InTableSearchControlProps = {
|
||||
inTableSearchTerm: 'aa',
|
||||
pageSize: null,
|
||||
visibleColumns: [visibleColumns[0]],
|
||||
rows: testData,
|
||||
renderCellValue: jest.fn(
|
||||
wrapRenderCellValueWithInTableSearchSupport(getRenderCellValueMock(testData))
|
||||
),
|
||||
getColumnIndexFromId: jest.fn(getColumnIndexFromId),
|
||||
scrollToCell: jest.fn(),
|
||||
shouldOverrideCmdF: jest.fn(),
|
||||
onChange: jest.fn(),
|
||||
onChangeCss: jest.fn(),
|
||||
onChangeToExpectedPage: jest.fn(),
|
||||
};
|
||||
|
||||
const { rerender } = render(<InTableSearchControl {...initialProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId(COUNTER_TEST_SUBJ)).toHaveTextContent('1/2');
|
||||
});
|
||||
|
||||
rerender(
|
||||
<InTableSearchControl {...initialProps} visibleColumns={[...visibleColumns, 'extraColumn']} />
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId(COUNTER_TEST_SUBJ)).toHaveTextContent('1/3');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -9,6 +9,7 @@
|
|||
|
||||
import React, { useCallback, useState, useEffect, useRef } from 'react';
|
||||
import { EuiButtonIcon, EuiToolTip, useEuiTheme } from '@elastic/eui';
|
||||
import useEvent from 'react-use/lib/useEvent';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { css, type SerializedStyles } from '@emotion/react';
|
||||
import { useFindMatches } from './matches/use_find_matches';
|
||||
|
@ -33,7 +34,7 @@ const innerCss = css`
|
|||
}
|
||||
|
||||
.euiFormControlLayout__append {
|
||||
padding-inline-end: 0 !important;
|
||||
padding-inline: 0 !important;
|
||||
background: none;
|
||||
}
|
||||
|
||||
|
@ -69,8 +70,9 @@ export const InTableSearchControl: React.FC<InTableSearchControlProps> = ({
|
|||
}) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const buttonRef = useRef<HTMLButtonElement | null>(null);
|
||||
const shouldReturnFocusToButtonRef = useRef<boolean>(false);
|
||||
const [isInputVisible, setIsInputVisible] = useState<boolean>(false);
|
||||
const [isInputVisible, setIsInputVisible] = useState<boolean>(Boolean(props.inTableSearchTerm));
|
||||
|
||||
const onScrollToActiveMatch: UseFindMatchesProps['onScrollToActiveMatch'] = useCallback(
|
||||
({ rowIndex, columnId, matchIndexWithinCell }) => {
|
||||
|
@ -133,8 +135,8 @@ export const InTableSearchControl: React.FC<InTableSearchControlProps> = ({
|
|||
);
|
||||
|
||||
// listens for the cmd+f or ctrl+f keydown event to open the input
|
||||
useEffect(() => {
|
||||
const handleGlobalKeyDown = (event: KeyboardEvent) => {
|
||||
const handleGlobalKeyDown = useCallback(
|
||||
(event: KeyboardEvent) => {
|
||||
if (
|
||||
(event.metaKey || event.ctrlKey) &&
|
||||
event.key === 'f' &&
|
||||
|
@ -150,24 +152,17 @@ export const InTableSearchControl: React.FC<InTableSearchControlProps> = ({
|
|||
) as HTMLInputElement
|
||||
)?.focus();
|
||||
}
|
||||
};
|
||||
},
|
||||
[showInput, shouldOverrideCmdF]
|
||||
);
|
||||
|
||||
document.addEventListener('keydown', handleGlobalKeyDown);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleGlobalKeyDown);
|
||||
};
|
||||
}, [showInput, shouldOverrideCmdF]);
|
||||
useEvent('keydown', handleGlobalKeyDown);
|
||||
|
||||
// returns focus to the button when the input was cancelled by pressing the escape key
|
||||
useEffect(() => {
|
||||
if (shouldReturnFocusToButtonRef.current && !isInputVisible) {
|
||||
shouldReturnFocusToButtonRef.current = false;
|
||||
(
|
||||
containerRef.current?.querySelector(
|
||||
`[data-test-subj="${BUTTON_TEST_SUBJ}"]`
|
||||
) as HTMLButtonElement
|
||||
)?.focus();
|
||||
buttonRef.current?.focus();
|
||||
}
|
||||
}, [isInputVisible]);
|
||||
|
||||
|
@ -197,6 +192,7 @@ export const InTableSearchControl: React.FC<InTableSearchControlProps> = ({
|
|||
>
|
||||
<EuiButtonIcon
|
||||
data-test-subj={BUTTON_TEST_SUBJ}
|
||||
buttonRef={buttonRef}
|
||||
iconType="search"
|
||||
size="xs"
|
||||
color="text"
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
*/
|
||||
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { escapeRegExp, memoize } from 'lodash';
|
||||
import { escapeRegExp } from 'lodash';
|
||||
import { HIGHLIGHT_COLOR, HIGHLIGHT_CLASS_NAME, CELL_MATCH_INDEX_ATTRIBUTE } from './constants';
|
||||
import { InTableSearchHighlightsWrapperProps } from './types';
|
||||
|
||||
|
@ -49,9 +49,21 @@ export const InTableSearchHighlightsWrapper: React.FC<InTableSearchHighlightsWra
|
|||
return <div ref={cellValueRef}>{children}</div>;
|
||||
};
|
||||
|
||||
const getSearchTermRegExp = memoize((searchTerm: string): RegExp => {
|
||||
return new RegExp(`(${escapeRegExp(searchTerm.trim())})`, 'gi');
|
||||
});
|
||||
const searchTermRegExpCache = new Map<string, RegExp>();
|
||||
|
||||
const getSearchTermRegExp = (searchTerm: string): RegExp => {
|
||||
if (searchTermRegExpCache.has(searchTerm)) {
|
||||
return searchTermRegExpCache.get(searchTerm)!;
|
||||
}
|
||||
|
||||
const searchTermRegExp = new RegExp(`(${escapeRegExp(searchTerm.trim())})`, 'gi');
|
||||
searchTermRegExpCache.set(searchTerm, searchTermRegExp);
|
||||
return searchTermRegExp;
|
||||
};
|
||||
|
||||
export const clearSearchTermRegExpCache = () => {
|
||||
searchTermRegExpCache.clear();
|
||||
};
|
||||
|
||||
function modifyDOMAndAddSearchHighlights(
|
||||
originalNode: Node,
|
||||
|
|
|
@ -108,7 +108,6 @@ export const InTableSearchInput: React.FC<InTableSearchInputProps> = React.memo(
|
|||
{matchesCount && activeMatchPosition
|
||||
? `${activeMatchPosition}/${matchesCount}`
|
||||
: '0/0'}
|
||||
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { AllCellsProps, RowMatches } from '../types';
|
||||
|
||||
const TIMEOUT_PER_ROW = 2000; // 2 sec per row max
|
||||
|
@ -28,14 +28,15 @@ export function RowCellsRenderer({
|
|||
const matchesCountPerColumnIdRef = useRef<Record<string, number>>({});
|
||||
const rowMatchesCountRef = useRef<number>(0);
|
||||
const remainingNumberOfResultsRef = useRef<number>(visibleColumns.length);
|
||||
const isCompletedRef = useRef<boolean>(false);
|
||||
const hasCompletedRef = useRef<boolean>(false);
|
||||
const [hasTimedOut, setHasTimedOut] = useState<boolean>(false);
|
||||
|
||||
// all cells in the row were processed
|
||||
const onComplete = useCallback(() => {
|
||||
if (isCompletedRef.current) {
|
||||
if (hasCompletedRef.current) {
|
||||
return;
|
||||
}
|
||||
isCompletedRef.current = true; // report only once
|
||||
hasCompletedRef.current = true; // report only once
|
||||
onRowProcessed({
|
||||
rowIndex,
|
||||
rowMatchesCount: rowMatchesCountRef.current,
|
||||
|
@ -69,7 +70,8 @@ export function RowCellsRenderer({
|
|||
}
|
||||
|
||||
timerRef.current = setTimeout(() => {
|
||||
onCompleteRef.current?.();
|
||||
onCompleteRef.current?.(); // at least report back the already collected results
|
||||
setHasTimedOut(true);
|
||||
}, TIMEOUT_PER_ROW);
|
||||
|
||||
return () => {
|
||||
|
@ -77,7 +79,12 @@ export function RowCellsRenderer({
|
|||
clearTimeout(timerRef.current);
|
||||
}
|
||||
};
|
||||
}, [rowIndex]);
|
||||
}, [rowIndex, setHasTimedOut]);
|
||||
|
||||
if (hasTimedOut) {
|
||||
// stop any further processing
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
DataGridWithInTableSearchExample,
|
||||
generateMockData,
|
||||
getRenderCellValueMock,
|
||||
MockContext,
|
||||
} from './__mocks__';
|
||||
import { useDataGridInTableSearch } from './use_data_grid_in_table_search';
|
||||
import {
|
||||
|
@ -135,4 +136,27 @@ describe('useDataGridInTableSearch', () => {
|
|||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle parent contexts correctly', async () => {
|
||||
render(
|
||||
<MockContext.Provider value={{ mockContextValue: 'test access to any parent context' }}>
|
||||
<DataGridWithInTableSearchExample rowsCount={100} columnsCount={2} pageSize={null} />
|
||||
</MockContext.Provider>
|
||||
);
|
||||
|
||||
screen.getByTestId(BUTTON_TEST_SUBJ).click();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId(INPUT_TEST_SUBJ)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const searchTerm = 'test access';
|
||||
const input = screen.getByTestId(INPUT_TEST_SUBJ);
|
||||
fireEvent.change(input, { target: { value: searchTerm } });
|
||||
expect(input).toHaveValue(searchTerm);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId(COUNTER_TEST_SUBJ)).toHaveTextContent('1/200');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,12 +7,13 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React, { useMemo, useRef, useState } from 'react';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import type { SerializedStyles } from '@emotion/react';
|
||||
import type { EuiDataGridProps, EuiDataGridRefProps } from '@elastic/eui';
|
||||
import { InTableSearchControl, InTableSearchControlProps } from './in_table_search_control';
|
||||
import { RenderCellValueWrapper } from './types';
|
||||
import { wrapRenderCellValueWithInTableSearchSupport } from './wrap_render_cell_value';
|
||||
import { clearSearchTermRegExpCache } from './in_table_search_highlights_wrapper';
|
||||
|
||||
export interface UseDataGridInTableSearchProps
|
||||
extends Pick<InTableSearchControlProps, 'rows' | 'visibleColumns'> {
|
||||
|
@ -87,7 +88,13 @@ export const useDataGridInTableSearch = (
|
|||
}
|
||||
return dataGridWrapper.contains?.(element) ?? false;
|
||||
}}
|
||||
onChange={(searchTerm) => setInTableSearchState({ inTableSearchTerm: searchTerm || '' })}
|
||||
onChange={(searchTerm) => {
|
||||
const nextSearchTerm = searchTerm || '';
|
||||
setInTableSearchState({ inTableSearchTerm: nextSearchTerm });
|
||||
if (!nextSearchTerm) {
|
||||
clearSearchTermRegExpCache();
|
||||
}
|
||||
}}
|
||||
onChangeCss={(styles) =>
|
||||
setInTableSearchState((prevState) => ({ ...prevState, inTableSearchTermCss: styles }))
|
||||
}
|
||||
|
@ -123,6 +130,12 @@ export const useDataGridInTableSearch = (
|
|||
};
|
||||
}, [cellContext, inTableSearchTerm]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearSearchTermRegExpCache();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
inTableSearchTermCss,
|
||||
|
|
|
@ -27,7 +27,6 @@ export interface DataTableContext {
|
|||
isPlainRecord?: boolean;
|
||||
pageIndex: number | undefined; // undefined when the pagination is disabled
|
||||
pageSize: number | undefined;
|
||||
inTableSearchTerm?: string;
|
||||
}
|
||||
|
||||
const defaultContext = {} as unknown as DataTableContext;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue