diff --git a/frontend/src/hooks/api/getters/useFeatureSearch/useFeatureSearch.test.tsx b/frontend/src/hooks/api/getters/useFeatureSearch/useFeatureSearch.test.tsx
new file mode 100644
index 0000000000..98a82cbb68
--- /dev/null
+++ b/frontend/src/hooks/api/getters/useFeatureSearch/useFeatureSearch.test.tsx
@@ -0,0 +1,67 @@
+import React, { FC } from 'react';
+import { render, screen } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import { testServerRoute, testServerSetup } from 'utils/testServer';
+import { useFeatureSearch } from './useFeatureSearch';
+import { useSWRConfig } from 'swr';
+
+const server = testServerSetup();
+
+const TestComponent: FC<{ params: { project: string } }> = ({ params }) => {
+ const { loading, error, features, total } = useFeatureSearch(params);
+ const { cache } = useSWRConfig();
+
+ if (loading) {
+ return
Loading...
;
+ }
+
+ if (error) {
+ return Error: {error}
;
+ }
+
+ return (
+
+
Features: {features.map((f) => f.name).join(', ')}
+
Total: {total}
+
Cache: {[...cache.keys()]}
+
+ );
+};
+
+describe('useFeatureSearch', () => {
+ test('should return features after loading', async () => {
+ testServerRoute(server, '/api/admin/search/features?project=project1', {
+ features: [{ name: 'Feature1' }, { name: 'Feature2' }],
+ total: 2,
+ });
+
+ render();
+
+ await screen.findByText(/Loading.../);
+
+ await screen.findByText(/Features:/);
+ await screen.findByText(/Total:/);
+ });
+
+ test('should keep only latest cache entry', async () => {
+ testServerRoute(server, '/api/admin/search/features?project=project1', {
+ features: [{ name: 'Feature1' }],
+ total: 1,
+ });
+ render();
+ await screen.findByText(/Features:/);
+ await screen.findByText(
+ 'Cache: api/admin/search/features?project=project1',
+ );
+
+ testServerRoute(server, '/api/admin/search/features?project=project2', {
+ features: [{ name: 'Feature2' }],
+ total: 1,
+ });
+ render();
+ await screen.findByText(/Features:/);
+ await screen.findByText(
+ 'Cache: api/admin/search/features?project=project2',
+ );
+ });
+});
diff --git a/frontend/src/hooks/api/getters/useFeatureSearch/useFeatureSearch.ts b/frontend/src/hooks/api/getters/useFeatureSearch/useFeatureSearch.ts
index 1f0f976417..a9e56b4eba 100644
--- a/frontend/src/hooks/api/getters/useFeatureSearch/useFeatureSearch.ts
+++ b/frontend/src/hooks/api/getters/useFeatureSearch/useFeatureSearch.ts
@@ -1,4 +1,4 @@
-import useSWR, { SWRConfiguration } from 'swr';
+import useSWR, { SWRConfiguration, useSWRConfig } from 'swr';
import { useCallback, useEffect } from 'react';
import { formatApiPath } from 'utils/formatPath';
import handleErrorResponses from '../httpErrorResponseHandler';
@@ -24,6 +24,21 @@ const fallbackData: SearchFeaturesSchema = {
total: 0,
};
+const PREFIX_KEY = 'api/admin/search/features?';
+
+/**
+ With dynamic search and filter parameters we want to prevent cache from growing extensively.
+ We only keep the latest cache key `currentKey` and remove all other entries identified
+ by the `clearPrefix`
+ */
+const useClearSWRCache = (currentKey: string, clearPrefix: string) => {
+ const { cache } = useSWRConfig();
+ const keys = [...cache.keys()];
+ keys.filter((key) => key !== currentKey && key.startsWith(clearPrefix)).map(
+ (key) => cache.delete(key),
+ );
+};
+
const createFeatureSearch = () => {
const internalCache: InternalCache = {};
@@ -54,6 +69,7 @@ const createFeatureSearch = () => {
): UseFeatureSearchOutput => {
const { KEY, fetcher } = getFeatureSearchFetcher(params);
const cacheId = params.project || '';
+ useClearSWRCache(KEY, PREFIX_KEY);
useEffect(() => {
initCache(params.project || '');
@@ -103,7 +119,7 @@ const getFeatureSearchFetcher = (params: SearchFeaturesParams) => {
.map(([key, value]) => [key, value.toString()]), // TODO: parsing non-string parameters
),
).toString();
- const KEY = `api/admin/search/features?${urlSearchParams}`;
+ const KEY = `${PREFIX_KEY}${urlSearchParams}`;
const fetcher = () => {
const path = formatApiPath(KEY);
return fetch(path, {