Skip to content

Commit

Permalink
Add optional debounce to Page Filters input (#751)
Browse files Browse the repository at this point in the history
Add prop for debounce duration (in ms) to Page Filters search
Separate search state from page query params
Debounce call to setSearch if the prop is passed
Pass debounce prop from Domain Workflows filters
  • Loading branch information
adhityamamallan authored Dec 4, 2024
1 parent 89c3811 commit 08968df
Show file tree
Hide file tree
Showing 6 changed files with 77 additions and 26 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';

import { render, screen, act, fireEvent } from '@/test-utils/rtl';
import { render, screen, userEvent } from '@/test-utils/rtl';

import {
mockPageQueryParamConfig,
Expand All @@ -13,55 +13,81 @@ jest.mock('@/hooks/use-page-query-params/use-page-query-params', () =>
jest.fn(() => [mockQueryParamsValues, mockSetQueryParams])
);

beforeEach(() => {
jest.useFakeTimers();
});

afterEach(() => {
jest.clearAllMocks();
jest.useRealTimers();
});

describe(PageFiltersSearch.name, () => {
it('should render search bar correctly and call setSearch on input change', async () => {
setup({});
const { user } = setup({});

const searchInput = await screen.findByRole('textbox');

act(() => {
fireEvent.change(searchInput, { target: { value: 'test-search' } });
});
await user.type(searchInput, 'test-search');

expect(mockSetQueryParams).toHaveBeenCalledWith({ search: 'test-search' });
});

it('should prune quotes and spaces from input text if no regexp is passed', async () => {
setup({});
const { user } = setup({});

const searchInput = await screen.findByRole('textbox');

act(() => {
fireEvent.change(searchInput, { target: { value: ` "test-search'` } });
});
await user.type(searchInput, ` "test-search'`);

expect(mockSetQueryParams).toHaveBeenCalledWith({ search: 'test-search' });
});

it('should prune symbols from input text if regexp is passed', async () => {
setup({ searchTrimRegExp: /[-]/g });
const { user } = setup({ searchTrimRegExp: /[-]/g });

const searchInput = await screen.findByRole('textbox');

act(() => {
fireEvent.change(searchInput, { target: { value: 'test-search' } });
});
await user.type(searchInput, 'test-search');

expect(mockSetQueryParams).toHaveBeenCalledWith({ search: 'testsearch' });
});

it('should debounce setSearch if a debounce duration is passed', async () => {
const { user } = setup({ inputDebounceDurationMs: 400 });

const searchInput = await screen.findByRole('textbox');

await user.type(searchInput, 'test-');
jest.advanceTimersByTime(200);

await user.type(searchInput, 'search');
jest.advanceTimersByTime(500);

expect(mockSetQueryParams).toHaveBeenCalledTimes(1);
expect(mockSetQueryParams).toHaveBeenCalledWith({
search: 'test-search',
});
});
});

function setup({ searchTrimRegExp }: { searchTrimRegExp?: RegExp }) {
render(
function setup({
searchTrimRegExp,
inputDebounceDurationMs,
}: {
searchTrimRegExp?: RegExp;
inputDebounceDurationMs?: number;
}) {
const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
const renderResult = render(
<PageFiltersSearch
searchQueryParamKey="search"
searchPlaceholder="placeholder"
pageQueryParamsConfig={mockPageQueryParamConfig}
{...(searchTrimRegExp ? { searchTrimRegExp } : {})}
{...(inputDebounceDurationMs ? { inputDebounceDurationMs } : {})}
/>
);

return { user, ...renderResult };
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';

import { Search } from 'baseui/icon';
import { Input } from 'baseui/input';
import debounce from 'lodash/debounce';

import usePageQueryParams from '@/hooks/use-page-query-params/use-page-query-params';
import {
Expand All @@ -21,20 +22,42 @@ export default function PageFiltersSearch<
searchQueryParamKey,
searchPlaceholder,
searchTrimRegExp = /['"\s]/g,
inputDebounceDurationMs,
}: Props<P, K>) {
const [queryParams, setQueryParams] = usePageQueryParams(
pageQueryParamsConfig,
{ replace: true, pageRerender: false }
);

const queryParamsSearch = queryParams[searchQueryParamKey];

const [inputState, setInputState] = useState<string>('');

useEffect(() => {
setInputState(queryParamsSearch);
}, [queryParamsSearch]);

const setSearch = useCallback(
(value: string) =>
setQueryParams({ [searchQueryParamKey]: value } as Partial<
PageQueryParamSetterValues<P>
>),
[searchQueryParamKey, setQueryParams]
);

const setSearchMaybeDebounced = useMemo(() => {
if (inputDebounceDurationMs)
return debounce(setSearch, inputDebounceDurationMs);
return setSearch;
}, [setSearch, inputDebounceDurationMs]);

return (
<Input
value={queryParams[searchQueryParamKey]}
value={inputState}
onChange={(event) => {
const searchValue = event.target.value.replaceAll(searchTrimRegExp, '');
setQueryParams({
[searchQueryParamKey]: searchValue || undefined,
} as Partial<PageQueryParamSetterValues<P>>);
setInputState(searchValue);
setSearchMaybeDebounced(searchValue);
}}
placeholder={searchPlaceholder}
startEnhancer={() => <Search />}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ export type Props<
searchQueryParamKey: PageQueryParamValues<P>[K] extends string ? K : never;
searchPlaceholder: string;
searchTrimRegExp?: RegExp;
inputDebounceDurationMs?: number;
};
8 changes: 2 additions & 6 deletions src/components/page-filters/page-filters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,7 @@ export default function PageFilters<
>({
pageFiltersConfig,
pageQueryParamsConfig,
searchQueryParamKey,
searchPlaceholder,
searchTrimRegExp,
...restSearchProps
}: Props<P, K>) {
const [areFiltersShown, setAreFiltersShown] = useState(false);

Expand All @@ -32,9 +30,7 @@ export default function PageFilters<
<styled.SearchInputContainer>
<PageFiltersSearch
pageQueryParamsConfig={pageQueryParamsConfig}
searchQueryParamKey={searchQueryParamKey}
searchPlaceholder={searchPlaceholder}
searchTrimRegExp={searchTrimRegExp}
{...restSearchProps}
/>
<PageFiltersToggle
isActive={areFiltersShown}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const DOMAIN_WORKFLOWS_SEARCH_DEBOUNCE_MS = 250;

export default DOMAIN_WORKFLOWS_SEARCH_DEBOUNCE_MS;
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import PageFiltersToggle from '@/components/page-filters/page-filters-toggle/pag
import domainPageQueryParamsConfig from '@/views/domain-page/config/domain-page-query-params.config';

import domainWorkflowsFiltersConfig from '../config/domain-workflows-filters.config';
import DOMAIN_WORKFLOWS_SEARCH_DEBOUNCE_MS from '../config/domain-workflows-search-debounce-ms.config';
import DomainWorkflowsQueryInput from '../domain-workflows-query-input/domain-workflows-query-input';
import DomainWorkflowsQueryLabel from '../domain-workflows-query-label/domain-workflows-query-label';
import useListWorkflows from '../hooks/use-list-workflows';
Expand Down Expand Up @@ -70,6 +71,7 @@ export default function DomainWorkflowsHeader({ domain, cluster }: Props) {
pageQueryParamsConfig={domainPageQueryParamsConfig}
searchQueryParamKey="search"
searchPlaceholder="Search for Workflow ID, Run ID, or Workflow Type"
inputDebounceDurationMs={DOMAIN_WORKFLOWS_SEARCH_DEBOUNCE_MS}
/>
<PageFiltersToggle
isActive={areFiltersShown}
Expand Down

0 comments on commit 08968df

Please sign in to comment.