1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-09-19 17:52:45 +02:00

Add use change request search hook for UI. (#10664)

Adds a use change request search hook. The hook (and tests) are based
closely on the `useFeatureSearch` files.

I will wire them up to the table in an upcoming PR.

Also: fixes the orval schema to use numbers for offset and limit instead
of strings (enterprise pr incoming). Plus: updates a variable usage in
the use feature search hook.
This commit is contained in:
Thomas Heartman 2025-09-12 14:16:58 +02:00 committed by GitHub
parent af0b3529b7
commit a519cb84f5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 206 additions and 3 deletions

View File

@ -0,0 +1,76 @@
import type { FC } from 'react';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import { testServerRoute, testServerSetup } from 'utils/testServer';
import { useChangeRequestSearch } from './useChangeRequestSearch.ts';
import { useSWRConfig } from 'swr';
const server = testServerSetup();
const TestComponent: FC<{ params: { createdBy?: string; limit?: number } }> = ({
params,
}) => {
const { loading, error, changeRequests, total, refetch } =
useChangeRequestSearch(params);
const { cache } = useSWRConfig();
if (loading) {
return <div>Loading...</div>;
}
if (error) {
return <div>Error: {error}</div>;
}
return (
<div>
<button type='button' onClick={refetch}>
refetch
</button>
<div>
Change Requests:{' '}
{changeRequests.map((cr) => cr.title).join(', ')}
</div>
<div>Total: {total}</div>
<div>Cache: {[...cache.keys()]}</div>
</div>
);
};
describe('useChangeRequestSearch', () => {
test('should overwrite cache total with 0 if the next result has 0 values', async () => {
const createdBy = '789';
const url = `/api/admin/search/change-requests?createdBy=${createdBy}`;
testServerRoute(server, url, {
changeRequests: [
{
id: 1,
title: 'Change Request 1',
createdAt: '2024-01-01T00:00:00Z',
createdBy: { id: 789, username: 'testuser' },
environment: 'production',
project: 'test-project',
features: ['feature1'],
segments: [],
state: 'Draft',
},
],
total: 1,
});
const { rerender } = render(<TestComponent params={{ createdBy }} />);
await screen.findByText(/Total: 1/);
testServerRoute(server, url, {
changeRequests: [],
total: 0,
});
// force fetch
const button = await screen.findByRole('button', { name: 'refetch' });
button.click();
rerender(<TestComponent params={{ createdBy }} />);
await screen.findByText(/Total: 0/);
});
});

View File

@ -0,0 +1,127 @@
import useSWR, { type SWRConfiguration } from 'swr';
import { useCallback, useEffect } from 'react';
import { formatApiPath } from 'utils/formatPath';
import handleErrorResponses from '../httpErrorResponseHandler.js';
import type {
SearchChangeRequestsParams,
ChangeRequestSearchResponseSchema,
} from 'openapi';
import { useClearSWRCache } from 'hooks/useClearSWRCache';
type UseChangeRequestSearchOutput = {
loading: boolean;
initialLoad: boolean;
error: string;
refetch: () => void;
} & ChangeRequestSearchResponseSchema;
type CacheValue = {
total: number;
initialLoad: boolean;
[key: string]: number | boolean;
};
type InternalCache = Record<string, CacheValue>;
const fallbackData: ChangeRequestSearchResponseSchema = {
changeRequests: [],
total: 0,
};
const SWR_CACHE_SIZE = 10;
const PATH = 'api/admin/search/change-requests?';
const createChangeRequestSearch = () => {
const internalCache: InternalCache = {};
const initCache = (id: string) => {
internalCache[id] = {
total: 0,
initialLoad: true,
};
};
const set = (id: string, key: string, value: number | boolean) => {
if (!internalCache[id]) {
initCache(id);
}
internalCache[id][key] = value;
};
const get = (id: string) => {
if (!internalCache[id]) {
initCache(id);
}
return internalCache[id];
};
return (
params: SearchChangeRequestsParams,
options: SWRConfiguration = {},
cachePrefix: string = '',
): UseChangeRequestSearchOutput => {
const { KEY, fetcher } = getChangeRequestSearchFetcher(params);
const swrKey = `${cachePrefix}${KEY}`;
const cacheId = 'global';
useClearSWRCache(swrKey, PATH, SWR_CACHE_SIZE);
useEffect(() => {
initCache(cacheId);
}, []);
const { data, error, mutate, isLoading } =
useSWR<ChangeRequestSearchResponseSchema>(swrKey, fetcher, options);
const refetch = useCallback(() => {
mutate();
}, [mutate]);
const cacheValues = get(cacheId);
if (data?.total !== undefined) {
set(cacheId, 'total', data.total);
}
if (!isLoading && cacheValues.initialLoad) {
set(cacheId, 'initialLoad', false);
}
const returnData = data || fallbackData;
return {
...returnData,
loading: isLoading,
error,
refetch,
total: cacheValues.total,
initialLoad: isLoading && cacheValues.initialLoad,
};
};
};
export const DEFAULT_PAGE_LIMIT = 25;
const getChangeRequestSearchFetcher = (params: SearchChangeRequestsParams) => {
const urlSearchParams = new URLSearchParams(
Array.from(
Object.entries(params)
.filter(([_, value]) => !!value)
.map(([key, value]) => [key, value.toString()]),
),
).toString();
const KEY = `${PATH}${urlSearchParams}`;
const fetcher = () => {
const path = formatApiPath(KEY);
return fetch(path, {
method: 'GET',
})
.then(handleErrorResponses('Change request search'))
.then((res) => res.json());
};
return {
fetcher,
KEY,
};
};
export const useChangeRequestSearch = createChangeRequestSearch();

View File

@ -63,7 +63,7 @@ const createFeatureSearch = () => {
useClearSWRCache(swrKey, PATH, SWR_CACHE_SIZE);
useEffect(() => {
initCache(params.project || '');
initCache(cacheId);
}, []);
const { data, error, mutate, isLoading } = useSWR<SearchFeaturesSchema>(

View File

@ -16,9 +16,9 @@ export type SearchChangeRequestsParams = {
/**
* The number of change requests to skip when returning a page. By default it is set to 0.
*/
offset?: string;
offset?: number;
/**
* The number of change requests to return in a page. By default it is set to 50. The maximum is 1000.
*/
limit?: string;
limit?: number;
};