From d1e93228a3e4d2a4ca5570ca969b1b91123c3137 Mon Sep 17 00:00:00 2001 From: Mateusz Kwasniewski Date: Mon, 26 Feb 2024 11:24:41 +0100 Subject: [PATCH] refactor: paginated data hook (#6333) --- .../ProjectApplications.tsx | 5 +- .../useApplications/useApplications.ts | 66 ++------------ .../usePaginatedData.test.tsx | 40 +++++++++ .../usePaginatedData/usePaginatedData.ts | 54 ++++++++++++ .../useProjectApplications.ts | 88 +++---------------- 5 files changed, 114 insertions(+), 139 deletions(-) create mode 100644 frontend/src/hooks/api/getters/usePaginatedData/usePaginatedData.test.tsx create mode 100644 frontend/src/hooks/api/getters/usePaginatedData/usePaginatedData.ts diff --git a/frontend/src/component/project/ProjectApplications/ProjectApplications.tsx b/frontend/src/component/project/ProjectApplications/ProjectApplications.tsx index ec50be69ad..703a3935a7 100644 --- a/frontend/src/component/project/ProjectApplications/ProjectApplications.tsx +++ b/frontend/src/component/project/ProjectApplications/ProjectApplications.tsx @@ -22,8 +22,8 @@ import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell'; import { ProjectApplicationSchema } from '../../../openapi'; import mapValues from 'lodash.mapvalues'; import { - useProjectApplications, DEFAULT_PAGE_LIMIT, + useProjectApplications, } from 'hooks/api/getters/useProjectApplications/useProjectApplications'; import { StringArrayCell } from 'component/common/Table/cells/StringArrayCell'; import { SdkCell } from './SdkCell'; @@ -51,12 +51,11 @@ export const ProjectApplications = () => { applications = [], total, loading, - refetch: refetchApplications, } = useProjectApplications( - projectId, mapValues(encodeQueryParams(stateConfig, tableState), (value) => value ? `${value}` : undefined, ), + projectId, ); const setSearchValue = (query = '') => { diff --git a/frontend/src/hooks/api/getters/useApplications/useApplications.ts b/frontend/src/hooks/api/getters/useApplications/useApplications.ts index 365c81a4e5..40804bc39e 100644 --- a/frontend/src/hooks/api/getters/useApplications/useApplications.ts +++ b/frontend/src/hooks/api/getters/useApplications/useApplications.ts @@ -1,62 +1,10 @@ -import useSWR, { mutate, SWRConfiguration } from 'swr'; -import { useEffect, useState } from 'react'; -import { formatApiPath } from 'utils/formatPath'; -import handleErrorResponses from '../httpErrorResponseHandler'; -import { ApplicationsSchema, GetApplicationsParams } from '../../../../openapi'; -import { useClearSWRCache } from '../../../useClearSWRCache'; +import { ApplicationsSchema } from '../../../../openapi'; +import { createPaginatedHook } from '../usePaginatedData/usePaginatedData'; -interface IUseApplicationsOutput extends ApplicationsSchema { - refetchApplications: () => void; - loading: boolean; - error?: Error; -} - -const PREFIX_KEY = 'api/admin/metrics/applications?'; - -const useApplications = ( - params: GetApplicationsParams = {}, - options: SWRConfiguration = {}, -): IUseApplicationsOutput => { - const urlSearchParams = new URLSearchParams( - Array.from( - Object.entries(params) - .filter(([_, value]) => !!value) - .map(([key, value]) => [key, value.toString()]), - ), - ).toString(); - - const KEY = `${PREFIX_KEY}${urlSearchParams}`; - useClearSWRCache(KEY, PREFIX_KEY); - - const fetcher = async () => { - return fetch(formatApiPath(KEY), { - method: 'GET', - }) - .then(handleErrorResponses('Applications data')) - .then((res) => res.json()); - }; - - const { data, error } = useSWR(KEY, fetcher, { - ...options, - }); - - const [loading, setLoading] = useState(!error && !data); - - const refetchApplications = () => { - mutate(KEY); - }; - - useEffect(() => { - setLoading(!error && !data); - }, [data, error]); - - return { - applications: data?.applications || [], - total: data?.total || 0, - error, - loading, - refetchApplications, - }; -}; +const prefixKey = 'api/admin/metrics/applications?'; +const useApplications = createPaginatedHook( + { applications: [], total: 0 }, + prefixKey, +); export default useApplications; diff --git a/frontend/src/hooks/api/getters/usePaginatedData/usePaginatedData.test.tsx b/frontend/src/hooks/api/getters/usePaginatedData/usePaginatedData.test.tsx new file mode 100644 index 0000000000..01d4a555f8 --- /dev/null +++ b/frontend/src/hooks/api/getters/usePaginatedData/usePaginatedData.test.tsx @@ -0,0 +1,40 @@ +import { testServerRoute, testServerSetup } from 'utils/testServer'; +import { render } from 'utils/testRenderer'; +import { screen } from '@testing-library/react'; +import { createPaginatedHook } from './usePaginatedData'; +import { FC } from 'react'; +import { http, HttpResponse } from 'msw'; + +const server = testServerSetup(); + +const usePaginatedData = createPaginatedHook<{ total: number; items: string }>( + { total: 0, items: 'default' }, + '/api/project/my-project?', +); + +const TestComponent: FC<{ query: string }> = ({ query }) => { + const { items, total } = usePaginatedData({ query }); + + return ( + + {items} ({total}) + + ); +}; + +test('Pass query params to server and return total', async () => { + testServerRoute(server, '/api/admin/ui-config', {}); + server.use( + http.get('/api/project/my-project', ({ request }) => { + const url = new URL(request.url); + return HttpResponse.json({ + items: `result${url.searchParams.get('query')}`, + total: 10, + }); + }), + ); + render(); + + await screen.findByText('default (0)'); + const element = await screen.findByText('resultvalue (10)'); +}); diff --git a/frontend/src/hooks/api/getters/usePaginatedData/usePaginatedData.ts b/frontend/src/hooks/api/getters/usePaginatedData/usePaginatedData.ts new file mode 100644 index 0000000000..1d48e01e5e --- /dev/null +++ b/frontend/src/hooks/api/getters/usePaginatedData/usePaginatedData.ts @@ -0,0 +1,54 @@ +import useSWR, { SWRConfiguration } from 'swr'; +import { formatApiPath } from 'utils/formatPath'; +import handleErrorResponses from '../httpErrorResponseHandler'; +import { useClearSWRCache } from '../../../useClearSWRCache'; + +type GenericSearchOutput = { + loading: boolean; + initialLoad: boolean; + error: string; + total: number; +} & T; + +export function createPaginatedHook( + customFallbackData: T, + defaultPrefixKey = '', +) { + return ( + params: Record = {}, + dynamicPrefixKey: string = '', + options: SWRConfiguration = {}, + ): GenericSearchOutput => { + const urlSearchParams = new URLSearchParams( + Array.from( + Object.entries(params) + .filter(([_, value]) => !!value) + .map(([key, value]) => [key, value.toString()]), + ), + ).toString(); + + const prefix = dynamicPrefixKey || defaultPrefixKey; + const KEY = `${prefix}${urlSearchParams}`; + useClearSWRCache(KEY, prefix); + + const fetcher = async () => { + return fetch(formatApiPath(KEY), { + method: 'GET', + }) + .then(handleErrorResponses('Paginated data')) + .then((res) => res.json()); + }; + + const { data, error, isLoading } = useSWR(KEY, fetcher, { + ...options, + }); + + const returnData = data || customFallbackData; + return { + ...returnData, + total: data?.total || 0, + error, + loading: isLoading, + }; + }; +} diff --git a/frontend/src/hooks/api/getters/useProjectApplications/useProjectApplications.ts b/frontend/src/hooks/api/getters/useProjectApplications/useProjectApplications.ts index 02f488310c..8432595e90 100644 --- a/frontend/src/hooks/api/getters/useProjectApplications/useProjectApplications.ts +++ b/frontend/src/hooks/api/getters/useProjectApplications/useProjectApplications.ts @@ -1,84 +1,18 @@ -import useSWR, { SWRConfiguration } from 'swr'; -import { useCallback } from 'react'; -import { formatApiPath } from 'utils/formatPath'; -import handleErrorResponses from '../httpErrorResponseHandler'; -import { - GetProjectApplicationsParams, - ProjectApplicationsSchema, -} from 'openapi'; -import { useClearSWRCache } from 'hooks/useClearSWRCache'; +import { ProjectApplicationsSchema } from 'openapi'; +import { createPaginatedHook } from '../usePaginatedData/usePaginatedData'; -type UseProjectApplicationsOutput = { - loading: boolean; - error: string; - refetch: () => void; -} & ProjectApplicationsSchema; - -const fallbackData: ProjectApplicationsSchema = { - applications: [], - total: 0, -}; +export const DEFAULT_PAGE_LIMIT = 25; const getPrefixKey = (projectId: string) => { return `api/admin/projects/${projectId}/applications?`; }; +const useParameterizedProjectApplications = + createPaginatedHook({ + applications: [], + total: 0, + }); -const createProjectApplications = () => { - return ( - projectId: string, - params: GetProjectApplicationsParams, - options: SWRConfiguration = {}, - ): UseProjectApplicationsOutput => { - const { KEY, fetcher } = getProjectApplicationsFetcher( - projectId, - params, - ); - - const { data, error, mutate, isLoading } = - useSWR(KEY, fetcher, options); - - const refetch = useCallback(() => { - mutate(); - }, [mutate]); - - const returnData = data || fallbackData; - return { - ...returnData, - loading: isLoading, - error, - refetch, - }; - }; -}; - -export const DEFAULT_PAGE_LIMIT = 25; - -export const useProjectApplications = createProjectApplications(); - -const getProjectApplicationsFetcher = ( +export const useProjectApplications = ( + params: Record, projectId: string, - params: GetProjectApplicationsParams, -) => { - const urlSearchParams = new URLSearchParams( - Array.from( - Object.entries(params) - .filter(([_, value]) => !!value) - .map(([key, value]) => [key, value.toString()]), // TODO: parsing non-string parameters - ), - ).toString(); - const KEY = `${getPrefixKey(projectId)}${urlSearchParams}`; - useClearSWRCache(KEY, getPrefixKey(projectId)); - const fetcher = () => { - const path = formatApiPath(KEY); - return fetch(path, { - method: 'GET', - }) - .then(handleErrorResponses('Feature search')) - .then((res) => res.json()); - }; - - return { - fetcher, - KEY, - }; -}; +) => useParameterizedProjectApplications(params, getPrefixKey(projectId));