From d2bf2b44c55bdeeb98f7e3e9095a4ad5764e3a85 Mon Sep 17 00:00:00 2001 From: Rahul Jain Date: Mon, 11 May 2026 20:23:39 -0700 Subject: [PATCH 1/2] Fix topic message search refresh behavior --- .../Topics/Topic/Messages/Filters/Filters.tsx | 17 +++++-- .../Filters/__tests__/Filters.spec.tsx | 45 ++++++++++++++++++- .../src/components/common/Search/Search.tsx | 30 +++++++------ frontend/src/lib/hooks/useMessagesFilters.ts | 29 +++++++++--- 4 files changed, 97 insertions(+), 24 deletions(-) diff --git a/frontend/src/components/Topics/Topic/Messages/Filters/Filters.tsx b/frontend/src/components/Topics/Topic/Messages/Filters/Filters.tsx index 5430cae2fe..fb001dd40e 100644 --- a/frontend/src/components/Topics/Topic/Messages/Filters/Filters.tsx +++ b/frontend/src/components/Topics/Topic/Messages/Filters/Filters.tsx @@ -1,7 +1,7 @@ import 'react-datepicker/dist/react-datepicker.css'; import { SerdeUsage, TopicMessageConsuming } from 'generated-sources'; -import React, { ChangeEvent, useMemo, useState } from 'react'; +import React, { ChangeEvent, useEffect, useMemo, useState } from 'react'; import MultiSelect from 'components/common/MultiSelect/MultiSelect.styled'; import Select from 'components/common/Select/Select'; import { Button } from 'components/common/Button/Button'; @@ -56,7 +56,6 @@ const Filters: React.FC = ({ offset, setOffsetValue, search, - setSearch, partitions: p, setPartition, smartFilter, @@ -66,6 +65,11 @@ const Filters: React.FC = ({ const { data: topic } = useTopicDetails({ clusterName, topicName }); const [createdEditedSmartId, setCreatedEditedSmartId] = useState(); + const [searchValue, setSearchValue] = useState(search); + + useEffect(() => { + setSearchValue(search); + }, [search]); const partitions = useMemo(() => { return (topic?.partitions || []).reduce<{ @@ -101,7 +105,7 @@ const Filters: React.FC = ({ if (isLiveMode(mode) && isFetching) { abortFetchData(); } - refreshData(); + refreshData(searchValue); }; return ( @@ -185,7 +189,12 @@ const Filters: React.FC = ({ - + ({ + useDebouncedCallback: (fn: (value: string) => void) => fn, +})); + jest.mock('lib/hooks/api/topics', () => ({ useTopicDetails: jest.fn(), })); @@ -44,6 +49,11 @@ jest.mock( const clusterName = 'cluster-name'; const topicName = 'topic-name'; +const SearchParamsValue = () => { + const [searchParams] = useSearchParams(); + return
{searchParams.toString()}
; +}; + const renderComponent = ( props?: Partial, queryParams?: Partial> @@ -52,7 +62,10 @@ const renderComponent = ( return render( - + <> + + + , { initialEntries: [ @@ -128,6 +141,36 @@ describe('Filters component', () => { }); }); + describe('search refresh behavior', () => { + it('clears the field without applying the cleared search until refresh', async () => { + renderComponent( + {}, + { [MessagesFilterKeys.stringFilter]: 'searchFilter' } + ); + + const searchInput = screen.getByPlaceholderText('Search'); + expect(searchInput).toHaveValue('searchFilter'); + + await userEvent.click(screen.getByTestId('search-clear-button')); + expect(searchInput).toHaveValue(''); + expect(screen.getByTestId('search-params')).toHaveTextContent( + `${MessagesFilterKeys.stringFilter}=searchFilter` + ); + expect(screen.getByTestId('search-params')).not.toHaveTextContent( + `${MessagesFilterKeys.r}=r` + ); + + await userEvent.click(screen.getByText('Refresh')); + + expect(screen.getByTestId('search-params')).not.toHaveTextContent( + MessagesFilterKeys.stringFilter + ); + expect(screen.getByTestId('search-params')).toHaveTextContent( + `${MessagesFilterKeys.r}=r` + ); + }); + }); + describe('change from and to offset filter', () => { const inputValue = 'Hello World!'; diff --git a/frontend/src/components/common/Search/Search.tsx b/frontend/src/components/common/Search/Search.tsx index 92c8bfc38e..8863c2ca42 100644 --- a/frontend/src/components/common/Search/Search.tsx +++ b/frontend/src/components/common/Search/Search.tsx @@ -18,6 +18,7 @@ interface SearchProps { onChange?: (value: string) => void; value?: string; extraActions?: ReactNode; + debounceMs?: number; } const Search: React.FC = ({ @@ -26,32 +27,38 @@ const Search: React.FC = ({ value, onChange, extraActions, + debounceMs = 500, }) => { const [searchParams, setSearchParams] = useSearchParams(); const ref = useRef>(null); - const [showIcon, setShowIcon] = useState(!!value || !!searchParams.get('q')); + const [showIcon, setShowIcon] = useState( + typeof value !== 'undefined' ? !!value : !!searchParams.get('q') + ); useEffect(() => { - if (ref.current !== null && value) { + if (ref.current !== null && typeof value !== 'undefined') { ref.current.value = value; } - }, [value]); + setShowIcon( + typeof value !== 'undefined' ? !!value : !!searchParams.get('q') + ); + }, [searchParams, value]); - const handleChange = useDebouncedCallback((e) => { - setShowIcon(!!e.target.value); + const handleChange = useDebouncedCallback((nextValue: string) => { + setShowIcon(!!nextValue); if (ref.current != null) { - ref.current.value = e.target.value; + ref.current.value = nextValue; } if (onChange) { - onChange(e.target.value); + onChange(nextValue); } else { - searchParams.set('q', e.target.value); + searchParams.set('q', nextValue); if (searchParams.get('page')) { searchParams.set('page', '1'); } setSearchParams(searchParams); } - }, 500); + }, debounceMs); const clearSearchValue = () => { if (onChange) { @@ -64,9 +71,6 @@ const Search: React.FC = ({ if (ref.current != null) { ref.current.value = ''; } - if (onChange) { - onChange(''); - } setShowIcon(false); }; @@ -74,7 +78,7 @@ const Search: React.FC = ({ handleChange(nextValue)} defaultValue={value || searchParams.get('q') || ''} inputSize="M" disabled={disabled} diff --git a/frontend/src/lib/hooks/useMessagesFilters.ts b/frontend/src/lib/hooks/useMessagesFilters.ts index 617f5859c0..da9dafba2c 100644 --- a/frontend/src/lib/hooks/useMessagesFilters.ts +++ b/frontend/src/lib/hooks/useMessagesFilters.ts @@ -63,7 +63,6 @@ export function usePaginateTopics(initSearchParams?: URLSearchParams) { export function useMessagesFilters(topicName: string) { const [searchParams, setSearchParams] = useSearchParams(); - const refreshData = useRefreshData(searchParams); const { clusterName } = useAppParams<{ clusterName: ClusterName }>(); const storageKey = `${topicName}:${clusterName}`; @@ -73,6 +72,16 @@ export function useMessagesFilters(topicName: string) { removeMessagesFiltersField, } = useMessagesFiltersFields(storageKey); + const syncSearchValue = (params: URLSearchParams, value: string) => { + if (value) { + setMessagesFiltersField(MessagesFilterKeys.stringFilter, value); + params.set(MessagesFilterKeys.stringFilter, value); + } else { + removeMessagesFiltersField(MessagesFilterKeys.stringFilter); + params.delete(MessagesFilterKeys.stringFilter); + } + }; + useEffect(() => { setSearchParams((params) => { initMessagesFiltersFields(params); @@ -179,13 +188,21 @@ export function useMessagesFilters(topicName: string) { const setSearch = (value: string) => { setSearchParams((params) => { - if (value) { - setMessagesFiltersField(MessagesFilterKeys.stringFilter, value); - params.set(MessagesFilterKeys.stringFilter, value); + syncSearchValue(params, value); + return params; + }); + }; + + const refreshData = (nextSearch = search) => { + setSearchParams((params) => { + syncSearchValue(params, nextSearch); + + if (params.get(MessagesFilterKeys.r)) { + params.delete(MessagesFilterKeys.r); } else { - removeMessagesFiltersField(MessagesFilterKeys.stringFilter); - params.delete(MessagesFilterKeys.stringFilter); + params.set(MessagesFilterKeys.r, 'r'); } + return params; }); }; From 43da136c6ed29e6e6fecb1576d8388a91dbffef0 Mon Sep 17 00:00:00 2001 From: Rahul Jain Date: Mon, 1 Jun 2026 11:21:24 -0700 Subject: [PATCH 2/2] Fix search params update in debounced search --- .../src/components/common/Search/Search.tsx | 15 ++++--- .../common/Search/__tests__/Search.spec.tsx | 41 +++++++++++++++---- 2 files changed, 43 insertions(+), 13 deletions(-) diff --git a/frontend/src/components/common/Search/Search.tsx b/frontend/src/components/common/Search/Search.tsx index 8863c2ca42..df5ec8708f 100644 --- a/frontend/src/components/common/Search/Search.tsx +++ b/frontend/src/components/common/Search/Search.tsx @@ -52,11 +52,16 @@ const Search: React.FC = ({ if (onChange) { onChange(nextValue); } else { - searchParams.set('q', nextValue); - if (searchParams.get('page')) { - searchParams.set('page', '1'); - } - setSearchParams(searchParams); + setSearchParams((params) => { + const nextParams = new URLSearchParams(params); + + nextParams.set('q', nextValue); + if (nextParams.get('page')) { + nextParams.set('page', '1'); + } + + return nextParams; + }); } }, debounceMs); diff --git a/frontend/src/components/common/Search/__tests__/Search.spec.tsx b/frontend/src/components/common/Search/__tests__/Search.spec.tsx index 452babab93..a1675c03a4 100644 --- a/frontend/src/components/common/Search/__tests__/Search.spec.tsx +++ b/frontend/src/components/common/Search/__tests__/Search.spec.tsx @@ -2,7 +2,7 @@ import Search from 'components/common/Search/Search'; import React from 'react'; import { render } from 'lib/testHelpers'; import userEvent from '@testing-library/user-event'; -import { screen, fireEvent } from '@testing-library/react'; +import { screen, fireEvent, waitFor } from '@testing-library/react'; import { useSearchParams } from 'react-router-dom'; jest.mock('use-debounce', () => ({ @@ -16,11 +16,14 @@ jest.mock('react-router-dom', () => ({ })); const placeholder = 'I am a search placeholder'; +let searchParamsMock: URLSearchParams; describe('Search', () => { beforeEach(() => { + setSearchParamsMock.mockClear(); + searchParamsMock = new URLSearchParams(); (useSearchParams as jest.Mock).mockImplementation(() => [ - new URLSearchParams(), + searchParamsMock, setSearchParamsMock, ]); }); @@ -32,6 +35,25 @@ describe('Search', () => { expect(setSearchParamsMock).toHaveBeenCalledTimes(5); }); + it('updates search params from the latest URL state', () => { + render(); + + fireEvent.change(screen.getByPlaceholderText(placeholder), { + target: { value: 'topic' }, + }); + + const updateSearchParams = setSearchParamsMock.mock.calls[0][0] as ( + params: URLSearchParams + ) => URLSearchParams; + const nextParams = updateSearchParams( + new URLSearchParams('page=3&cluster=local') + ); + + expect(nextParams.get('q')).toBe('topic'); + expect(nextParams.get('page')).toBe('1'); + expect(nextParams.get('cluster')).toBe('local'); + }); + it('when placeholder is provided', () => { render(); expect(screen.getByPlaceholderText(placeholder)).toBeInTheDocument(); @@ -57,19 +79,22 @@ describe('Search', () => { }); it('Clear button should clear text from input', async () => { - render(); + render(); const searchField = screen.getAllByRole('textbox')[0]; - fireEvent.change(searchField, { target: { value: 'hello' } }); + await userEvent.type(searchField, 'hello'); expect(searchField).toHaveValue('hello'); - let clearButton = screen.queryByTestId('search-clear-button'); + const clearButton = await screen.findByTestId('search-clear-button'); expect(clearButton).toBeInTheDocument(); - await userEvent.click(clearButton!); + await userEvent.click(clearButton); expect(searchField).toHaveValue(''); - clearButton = screen.queryByTestId('search-clear-button'); - expect(clearButton).not.toBeInTheDocument(); + await waitFor(() => + expect( + screen.queryByTestId('search-clear-button') + ).not.toBeInTheDocument() + ); }); });