diff --git a/frontend/src/components/QuickFilters/QuickFilters.tsx b/frontend/src/components/QuickFilters/QuickFilters.tsx index c70ca59c2df6..efa1ee536e81 100644 --- a/frontend/src/components/QuickFilters/QuickFilters.tsx +++ b/frontend/src/components/QuickFilters/QuickFilters.tsx @@ -50,7 +50,7 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element { filterConfig, isDynamicFilters, customFilters, - setIsStale, + refetchCustomFilters, isCustomFiltersLoading, } = useFilterConfig({ signal, config }); @@ -263,7 +263,7 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element { signal={signal} setIsSettingsOpen={setIsSettingsOpen} customFilters={customFilters} - setIsStale={setIsStale} + refetchCustomFilters={refetchCustomFilters} /> )} diff --git a/frontend/src/components/QuickFilters/QuickFiltersSettings/QuickFiltersSettings.tsx b/frontend/src/components/QuickFilters/QuickFiltersSettings/QuickFiltersSettings.tsx index aa78d2610781..20e8e5b581a4 100644 --- a/frontend/src/components/QuickFilters/QuickFiltersSettings/QuickFiltersSettings.tsx +++ b/frontend/src/components/QuickFilters/QuickFiltersSettings/QuickFiltersSettings.tsx @@ -14,12 +14,12 @@ function QuickFiltersSettings({ signal, setIsSettingsOpen, customFilters, - setIsStale, + refetchCustomFilters, }: { signal: SignalType | undefined; setIsSettingsOpen: (isSettingsOpen: boolean) => void; customFilters: FilterType[]; - setIsStale: (isStale: boolean) => void; + refetchCustomFilters: () => void; }): JSX.Element { const { handleSettingsClose, @@ -34,7 +34,7 @@ function QuickFiltersSettings({ } = useQuickFilterSettings({ setIsSettingsOpen, customFilters, - setIsStale, + refetchCustomFilters, signal, }); diff --git a/frontend/src/components/QuickFilters/QuickFiltersSettings/hooks/useQuickFilterSettings.tsx b/frontend/src/components/QuickFilters/QuickFiltersSettings/hooks/useQuickFilterSettings.tsx index bf4406c3045a..42be1bece827 100644 --- a/frontend/src/components/QuickFilters/QuickFiltersSettings/hooks/useQuickFilterSettings.tsx +++ b/frontend/src/components/QuickFilters/QuickFiltersSettings/hooks/useQuickFilterSettings.tsx @@ -12,7 +12,7 @@ import { Filter as FilterType } from 'types/api/quickFilters/getCustomFilters'; interface UseQuickFilterSettingsProps { setIsSettingsOpen: (isSettingsOpen: boolean) => void; customFilters: FilterType[]; - setIsStale: (isStale: boolean) => void; + refetchCustomFilters: () => void; signal?: SignalType; } @@ -32,7 +32,7 @@ interface UseQuickFilterSettingsReturn { const useQuickFilterSettings = ({ customFilters, setIsSettingsOpen, - setIsStale, + refetchCustomFilters, signal, }: UseQuickFilterSettingsProps): UseQuickFilterSettingsReturn => { const [inputValue, setInputValue] = useState(''); @@ -46,7 +46,7 @@ const useQuickFilterSettings = ({ } = useMutation(updateCustomFiltersAPI, { onSuccess: () => { setIsSettingsOpen(false); - setIsStale(true); + refetchCustomFilters(); logEvent('Quick Filters Settings: changes saved', { addedFilters, }); diff --git a/frontend/src/components/QuickFilters/hooks/useFilterConfig.tsx b/frontend/src/components/QuickFilters/hooks/useFilterConfig.tsx index fb2659a6817b..2f7d0bc70ed1 100644 --- a/frontend/src/components/QuickFilters/hooks/useFilterConfig.tsx +++ b/frontend/src/components/QuickFilters/hooks/useFilterConfig.tsx @@ -1,12 +1,8 @@ import getCustomFilters from 'api/quickFilters/getCustomFilters'; import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; -import { useMemo, useState } from 'react'; +import { useMemo } from 'react'; import { useQuery } from 'react-query'; -import { ErrorResponse, SuccessResponse } from 'types/api'; -import { - Filter as FilterType, - PayloadProps, -} from 'types/api/quickFilters/getCustomFilters'; +import { Filter as FilterType } from 'types/api/quickFilters/getCustomFilters'; import { IQuickFiltersConfig, SignalType } from '../types'; import { getFilterConfig } from '../utils'; @@ -18,37 +14,34 @@ interface UseFilterConfigProps { interface UseFilterConfigReturn { filterConfig: IQuickFiltersConfig[]; customFilters: FilterType[]; - setCustomFilters: React.Dispatch>; isCustomFiltersLoading: boolean; isDynamicFilters: boolean; - setIsStale: React.Dispatch>; + refetchCustomFilters: () => void; } const useFilterConfig = ({ signal, config, }: UseFilterConfigProps): UseFilterConfigReturn => { - const [customFilters, setCustomFilters] = useState([]); - const [isStale, setIsStale] = useState(true); + const { + isFetching: isCustomFiltersLoading, + data: customFilters = [], + refetch, + } = useQuery( + [REACT_QUERY_KEY.GET_CUSTOM_FILTERS, signal], + async () => { + const res = await getCustomFilters({ signal: signal || '' }); + return 'payload' in res && res.payload?.filters ? res.payload.filters : []; + }, + { + enabled: !!signal, + }, + ); + const isDynamicFilters = useMemo(() => customFilters.length > 0, [ customFilters, ]); - const { isFetching: isCustomFiltersLoading } = useQuery< - SuccessResponse | ErrorResponse, - Error - >( - [REACT_QUERY_KEY.GET_CUSTOM_FILTERS, signal], - () => getCustomFilters({ signal: signal || '' }), - { - onSuccess: (data) => { - if ('payload' in data && data.payload?.filters) { - setCustomFilters(data.payload.filters || ([] as FilterType[])); - } - setIsStale(false); - }, - enabled: !!signal && isStale, - }, - ); + const filterConfig = useMemo( () => getFilterConfig(signal, customFilters, config), [config, customFilters, signal], @@ -57,10 +50,9 @@ const useFilterConfig = ({ return { filterConfig, customFilters, - setCustomFilters, isCustomFiltersLoading, isDynamicFilters, - setIsStale, + refetchCustomFilters: refetch, }; }; diff --git a/frontend/src/components/QuickFilters/tests/QuickFilters.test.tsx b/frontend/src/components/QuickFilters/tests/QuickFilters.test.tsx index a251095eb41e..fb3e745f9a42 100644 --- a/frontend/src/components/QuickFilters/tests/QuickFilters.test.tsx +++ b/frontend/src/components/QuickFilters/tests/QuickFilters.test.tsx @@ -364,3 +364,141 @@ describe('Quick Filters with custom filters', () => { jest.useRealTimers(); }); }); + +describe('Quick Filters refetch behavior', () => { + it('fetches custom filters on every mount when signal is provided', async () => { + let getCalls = 0; + + server.use( + rest.get(quickFiltersListURL, (_req, res, ctx) => { + getCalls += 1; + return res(ctx.status(200), ctx.json(quickFiltersListResponse)); + }), + ); + + const { unmount } = render(); + expect(await screen.findByText(FILTER_SERVICE_NAME)).toBeInTheDocument(); + + unmount(); + + render(); + expect(await screen.findByText(FILTER_SERVICE_NAME)).toBeInTheDocument(); + + expect(getCalls).toBe(2); + }); + + it('does not fetch custom filters when signal is undefined', async () => { + let getCalls = 0; + + server.use( + rest.get(quickFiltersListURL, (_req, res, ctx) => { + getCalls += 1; + return res(ctx.status(200), ctx.json(quickFiltersListResponse)); + }), + ); + + render(); + + await waitFor(() => expect(getCalls).toBe(0)); + }); + + it('refetches custom filters after saving settings', async () => { + let getCalls = 0; + putHandler.mockClear(); + + server.use( + rest.get(quickFiltersListURL, (_req, res, ctx) => { + getCalls += 1; + return res(ctx.status(200), ctx.json(quickFiltersListResponse)); + }), + rest.put(saveQuickFiltersURL, async (req, res, ctx) => { + putHandler(await req.json()); + return res(ctx.status(200), ctx.json({})); + }), + ); + + const user = userEvent.setup({ pointerEventsCheck: 0 }); + render(); + + expect(await screen.findByText(FILTER_SERVICE_NAME)).toBeInTheDocument(); + + const icon = await screen.findByTestId(SETTINGS_ICON_TEST_ID); + const settingsButton = icon.closest('button') ?? icon; + await user.click(settingsButton); + + const target = await screen.findByText(FILTER_OS_DESCRIPTION); + const removeBtn = target.parentElement?.querySelector( + 'button', + ) as HTMLButtonElement; + await user.click(removeBtn); + + await user.click(screen.getByText(SAVE_CHANGES_TEXT)); + + await waitFor(() => expect(putHandler).toHaveBeenCalled()); + await waitFor(() => expect(getCalls).toBeGreaterThanOrEqual(2)); + }); + + it('renders updated filters after refetch post-save', async () => { + const updatedResponse = { + ...quickFiltersListResponse, + data: { + ...quickFiltersListResponse.data, + filters: [ + ...(quickFiltersListResponse.data.filters ?? []), + { + key: 'new.custom.filter', + dataType: 'string', + type: 'resource', + } as const, + ], + }, + }; + + let getCount = 0; + server.use( + rest.get(quickFiltersListURL, (_req, res, ctx) => { + getCount += 1; + return getCount >= 2 + ? res(ctx.status(200), ctx.json(updatedResponse)) + : res(ctx.status(200), ctx.json(quickFiltersListResponse)); + }), + rest.put(saveQuickFiltersURL, async (_req, res, ctx) => + res(ctx.status(200), ctx.json({})), + ), + ); + + const user = userEvent.setup({ pointerEventsCheck: 0 }); + render(); + + expect(await screen.findByText(FILTER_SERVICE_NAME)).toBeInTheDocument(); + + const icon = await screen.findByTestId(SETTINGS_ICON_TEST_ID); + const settingsButton = icon.closest('button') ?? icon; + await user.click(settingsButton); + + // Make a minimal change so Save button appears + const target = await screen.findByText(FILTER_OS_DESCRIPTION); + const removeBtn = target.parentElement?.querySelector( + 'button', + ) as HTMLButtonElement; + await user.click(removeBtn); + + await user.click(screen.getByText(SAVE_CHANGES_TEXT)); + + await waitFor(() => { + expect(screen.getByText('New Custom Filter')).toBeInTheDocument(); + }); + }); + + it('shows empty state when GET fails', async () => { + server.use( + rest.get(quickFiltersListURL, (_req, res, ctx) => + res(ctx.status(500), ctx.json({})), + ), + ); + + render(); + + expect(await screen.findByText('No filters found')).toBeInTheDocument(); + }); +});