Skip to content

Commit

Permalink
refactor: paginated data hook (#6333)
Browse files Browse the repository at this point in the history
  • Loading branch information
kwasniew committed Feb 26, 2024
1 parent 1633722 commit d1e9322
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 141 deletions.
Expand Up @@ -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';
Expand Down Expand Up @@ -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 = '') => {
Expand Down
68 changes: 8 additions & 60 deletions 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';

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,
};
};
import { ApplicationsSchema } from '../../../../openapi';
import { createPaginatedHook } from '../usePaginatedData/usePaginatedData';

const prefixKey = 'api/admin/metrics/applications?';
const useApplications = createPaginatedHook<ApplicationsSchema>(
{ applications: [], total: 0 },
prefixKey,
);

export default useApplications;
@@ -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 (
<span>
{items} ({total})
</span>
);
};

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(<TestComponent query='value' />);

await screen.findByText('default (0)');
const element = await screen.findByText('resultvalue (10)');
});
@@ -0,0 +1,54 @@
import useSWR, { SWRConfiguration } from 'swr';
import { formatApiPath } from 'utils/formatPath';
import handleErrorResponses from '../httpErrorResponseHandler';
import { useClearSWRCache } from '../../../useClearSWRCache';

type GenericSearchOutput<T> = {
loading: boolean;
initialLoad: boolean;
error: string;
total: number;
} & T;

export function createPaginatedHook<T extends { total?: number }>(
customFallbackData: T,
defaultPrefixKey = '',
) {
return (
params: Record<string, any> = {},
dynamicPrefixKey: string = '',
options: SWRConfiguration = {},
): GenericSearchOutput<T> => {
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,
};
};
}
@@ -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 createProjectApplications = () => {
return (
projectId: string,
params: GetProjectApplicationsParams,
options: SWRConfiguration = {},
): UseProjectApplicationsOutput => {
const { KEY, fetcher } = getProjectApplicationsFetcher(
projectId,
params,
);

const { data, error, mutate, isLoading } =
useSWR<ProjectApplicationsSchema>(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 = (
const useParameterizedProjectApplications =
createPaginatedHook<ProjectApplicationsSchema>({
applications: [],
total: 0,
});

export const useProjectApplications = (
params: Record<string, any>,
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));

0 comments on commit d1e9322

Please sign in to comment.