From edbd71ac15404471995945285f8d96dbc8d2e5e4 Mon Sep 17 00:00:00 2001 From: Mateusz Kwasniewski Date: Thu, 22 Feb 2024 14:37:08 +0100 Subject: [PATCH] feat: paginated hook for applications list (#6315) --- .../PaginatedApplicationList.tsx | 23 +++++++++--- .../StrategyDetails/StrategyDetails.tsx | 5 ++- .../useApplications/useApplications.ts | 35 ++++++++++++------- .../useFeatureSearch/useFeatureSearch.ts | 16 ++------- .../useProjectApplications.ts | 2 ++ frontend/src/hooks/useClearSWRCache.ts | 14 ++++++++ 6 files changed, 61 insertions(+), 34 deletions(-) create mode 100644 frontend/src/hooks/useClearSWRCache.ts diff --git a/frontend/src/component/application/ApplicationList/PaginatedApplicationList.tsx b/frontend/src/component/application/ApplicationList/PaginatedApplicationList.tsx index 96296518ad..9a88ab9c4b 100644 --- a/frontend/src/component/application/ApplicationList/PaginatedApplicationList.tsx +++ b/frontend/src/component/application/ApplicationList/PaginatedApplicationList.tsx @@ -13,12 +13,18 @@ import { IconCell } from 'component/common/Table/cells/IconCell/IconCell'; import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell'; import { ApplicationUsageCell } from './ApplicationUsageCell/ApplicationUsageCell'; import { ApplicationSchema } from '../../../openapi'; -import { NumberParam, StringParam, withDefault } from 'use-query-params'; +import { + encodeQueryParams, + NumberParam, + StringParam, + withDefault, +} from 'use-query-params'; import { DEFAULT_PAGE_LIMIT } from 'hooks/api/getters/useProjectApplications/useProjectApplications'; import { usePersistentTableState } from 'hooks/usePersistentTableState'; import { createColumnHelper, useReactTable } from '@tanstack/react-table'; import { withTableState } from 'utils/withTableState'; import useLoading from 'hooks/useLoading'; +import mapValues from 'lodash.mapvalues'; const renderNoApplications = () => ( <> @@ -42,9 +48,6 @@ const renderNoApplications = () => ( const columnHelper = createColumnHelper(); export const PaginatedApplicationList = () => { - const { applications: data, loading } = useApplications(); - const total = 1000; - const stateConfig = { offset: withDefault(NumberParam, 0), limit: withDefault(NumberParam, DEFAULT_PAGE_LIMIT), @@ -56,11 +59,21 @@ export const PaginatedApplicationList = () => { `applications-table`, stateConfig, ); + const { + applications: data, + total, + loading, + } = useApplications( + mapValues(encodeQueryParams(stateConfig, tableState), (value) => + value ? `${value}` : undefined, + ), + ); const columns = useMemo( () => [ columnHelper.accessor('icon', { id: 'Icon', + header: () => '', cell: ({ row: { original: { icon }, @@ -74,6 +87,7 @@ export const PaginatedApplicationList = () => { } /> ), + enableSorting: false, }), columnHelper.accessor('appName', { header: 'Name', @@ -93,6 +107,7 @@ export const PaginatedApplicationList = () => { columnHelper.accessor('usage', { header: 'Project(environment)', meta: { width: '50%' }, + enableSorting: false, cell: ({ row: { original }, }: { diff --git a/frontend/src/component/strategies/StrategyView/StrategyDetails/StrategyDetails.tsx b/frontend/src/component/strategies/StrategyView/StrategyDetails/StrategyDetails.tsx index ec80f4cc6f..dc6688565c 100644 --- a/frontend/src/component/strategies/StrategyView/StrategyDetails/StrategyDetails.tsx +++ b/frontend/src/component/strategies/StrategyView/StrategyDetails/StrategyDetails.tsx @@ -13,12 +13,11 @@ import { ConditionallyRender } from 'component/common/ConditionallyRender/Condit import styles from '../../strategies.module.scss'; import { TogglesLinkList } from 'component/strategies/TogglesLinkList/TogglesLinkList'; import { IStrategy, IStrategyParameter } from 'interfaces/strategy'; -import { IApplication } from 'interfaces/application'; -import { FeatureSchema } from 'openapi'; +import { ApplicationSchema, FeatureSchema } from 'openapi'; interface IStrategyDetailsProps { strategy: IStrategy; - applications: IApplication[]; + applications: ApplicationSchema[]; toggles: FeatureSchema[]; } diff --git a/frontend/src/hooks/api/getters/useApplications/useApplications.ts b/frontend/src/hooks/api/getters/useApplications/useApplications.ts index f8e0071f9e..365c81a4e5 100644 --- a/frontend/src/hooks/api/getters/useApplications/useApplications.ts +++ b/frontend/src/hooks/api/getters/useApplications/useApplications.ts @@ -1,40 +1,49 @@ import useSWR, { mutate, SWRConfiguration } from 'swr'; -import { useState, useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { formatApiPath } from 'utils/formatPath'; import handleErrorResponses from '../httpErrorResponseHandler'; -import { IApplication } from 'interfaces/application'; +import { ApplicationsSchema, GetApplicationsParams } from '../../../../openapi'; +import { useClearSWRCache } from '../../../useClearSWRCache'; -const path = formatApiPath('api/admin/metrics/applications'); - -interface IUseApplicationsOutput { - applications: IApplication[]; +interface IUseApplicationsOutput extends ApplicationsSchema { refetchApplications: () => void; loading: boolean; error?: Error; - APPLICATIONS_CACHE_KEY: string; } +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(path, { + return fetch(formatApiPath(KEY), { method: 'GET', }) .then(handleErrorResponses('Applications data')) .then((res) => res.json()); }; - const APPLICATIONS_CACHE_KEY = 'api/admin/metrics/applications'; - - const { data, error } = useSWR(APPLICATIONS_CACHE_KEY, fetcher, { + const { data, error } = useSWR(KEY, fetcher, { ...options, }); const [loading, setLoading] = useState(!error && !data); const refetchApplications = () => { - mutate(APPLICATIONS_CACHE_KEY); + mutate(KEY); }; useEffect(() => { @@ -43,10 +52,10 @@ const useApplications = ( return { applications: data?.applications || [], + total: data?.total || 0, error, loading, refetchApplications, - APPLICATIONS_CACHE_KEY, }; }; diff --git a/frontend/src/hooks/api/getters/useFeatureSearch/useFeatureSearch.ts b/frontend/src/hooks/api/getters/useFeatureSearch/useFeatureSearch.ts index a9e56b4eba..2eb09cae9c 100644 --- a/frontend/src/hooks/api/getters/useFeatureSearch/useFeatureSearch.ts +++ b/frontend/src/hooks/api/getters/useFeatureSearch/useFeatureSearch.ts @@ -1,8 +1,9 @@ -import useSWR, { SWRConfiguration, useSWRConfig } from 'swr'; +import useSWR, { SWRConfiguration } from 'swr'; import { useCallback, useEffect } from 'react'; import { formatApiPath } from 'utils/formatPath'; import handleErrorResponses from '../httpErrorResponseHandler'; import { SearchFeaturesParams, SearchFeaturesSchema } from 'openapi'; +import { useClearSWRCache } from 'hooks/useClearSWRCache'; type UseFeatureSearchOutput = { loading: boolean; @@ -26,19 +27,6 @@ const fallbackData: SearchFeaturesSchema = { 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 = {}; diff --git a/frontend/src/hooks/api/getters/useProjectApplications/useProjectApplications.ts b/frontend/src/hooks/api/getters/useProjectApplications/useProjectApplications.ts index 03ffb714e3..02f488310c 100644 --- a/frontend/src/hooks/api/getters/useProjectApplications/useProjectApplications.ts +++ b/frontend/src/hooks/api/getters/useProjectApplications/useProjectApplications.ts @@ -6,6 +6,7 @@ import { GetProjectApplicationsParams, ProjectApplicationsSchema, } from 'openapi'; +import { useClearSWRCache } from 'hooks/useClearSWRCache'; type UseProjectApplicationsOutput = { loading: boolean; @@ -66,6 +67,7 @@ const getProjectApplicationsFetcher = ( ), ).toString(); const KEY = `${getPrefixKey(projectId)}${urlSearchParams}`; + useClearSWRCache(KEY, getPrefixKey(projectId)); const fetcher = () => { const path = formatApiPath(KEY); return fetch(path, { diff --git a/frontend/src/hooks/useClearSWRCache.ts b/frontend/src/hooks/useClearSWRCache.ts new file mode 100644 index 0000000000..359ac102c0 --- /dev/null +++ b/frontend/src/hooks/useClearSWRCache.ts @@ -0,0 +1,14 @@ +import { useSWRConfig } from 'swr'; + +/** + 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` + */ +export 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), + ); +};