1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-05-22 01:16:07 +02:00

feat: paginated hook for applications list (#6315)

This commit is contained in:
Mateusz Kwasniewski 2024-02-22 14:37:08 +01:00 committed by GitHub
parent fb63f21d8a
commit edbd71ac15
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 61 additions and 34 deletions

View File

@ -13,12 +13,18 @@ import { IconCell } from 'component/common/Table/cells/IconCell/IconCell';
import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell'; import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
import { ApplicationUsageCell } from './ApplicationUsageCell/ApplicationUsageCell'; import { ApplicationUsageCell } from './ApplicationUsageCell/ApplicationUsageCell';
import { ApplicationSchema } from '../../../openapi'; 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 { DEFAULT_PAGE_LIMIT } from 'hooks/api/getters/useProjectApplications/useProjectApplications';
import { usePersistentTableState } from 'hooks/usePersistentTableState'; import { usePersistentTableState } from 'hooks/usePersistentTableState';
import { createColumnHelper, useReactTable } from '@tanstack/react-table'; import { createColumnHelper, useReactTable } from '@tanstack/react-table';
import { withTableState } from 'utils/withTableState'; import { withTableState } from 'utils/withTableState';
import useLoading from 'hooks/useLoading'; import useLoading from 'hooks/useLoading';
import mapValues from 'lodash.mapvalues';
const renderNoApplications = () => ( const renderNoApplications = () => (
<> <>
@ -42,9 +48,6 @@ const renderNoApplications = () => (
const columnHelper = createColumnHelper<ApplicationSchema>(); const columnHelper = createColumnHelper<ApplicationSchema>();
export const PaginatedApplicationList = () => { export const PaginatedApplicationList = () => {
const { applications: data, loading } = useApplications();
const total = 1000;
const stateConfig = { const stateConfig = {
offset: withDefault(NumberParam, 0), offset: withDefault(NumberParam, 0),
limit: withDefault(NumberParam, DEFAULT_PAGE_LIMIT), limit: withDefault(NumberParam, DEFAULT_PAGE_LIMIT),
@ -56,11 +59,21 @@ export const PaginatedApplicationList = () => {
`applications-table`, `applications-table`,
stateConfig, stateConfig,
); );
const {
applications: data,
total,
loading,
} = useApplications(
mapValues(encodeQueryParams(stateConfig, tableState), (value) =>
value ? `${value}` : undefined,
),
);
const columns = useMemo( const columns = useMemo(
() => [ () => [
columnHelper.accessor('icon', { columnHelper.accessor('icon', {
id: 'Icon', id: 'Icon',
header: () => '',
cell: ({ cell: ({
row: { row: {
original: { icon }, original: { icon },
@ -74,6 +87,7 @@ export const PaginatedApplicationList = () => {
} }
/> />
), ),
enableSorting: false,
}), }),
columnHelper.accessor('appName', { columnHelper.accessor('appName', {
header: 'Name', header: 'Name',
@ -93,6 +107,7 @@ export const PaginatedApplicationList = () => {
columnHelper.accessor('usage', { columnHelper.accessor('usage', {
header: 'Project(environment)', header: 'Project(environment)',
meta: { width: '50%' }, meta: { width: '50%' },
enableSorting: false,
cell: ({ cell: ({
row: { original }, row: { original },
}: { }: {

View File

@ -13,12 +13,11 @@ import { ConditionallyRender } from 'component/common/ConditionallyRender/Condit
import styles from '../../strategies.module.scss'; import styles from '../../strategies.module.scss';
import { TogglesLinkList } from 'component/strategies/TogglesLinkList/TogglesLinkList'; import { TogglesLinkList } from 'component/strategies/TogglesLinkList/TogglesLinkList';
import { IStrategy, IStrategyParameter } from 'interfaces/strategy'; import { IStrategy, IStrategyParameter } from 'interfaces/strategy';
import { IApplication } from 'interfaces/application'; import { ApplicationSchema, FeatureSchema } from 'openapi';
import { FeatureSchema } from 'openapi';
interface IStrategyDetailsProps { interface IStrategyDetailsProps {
strategy: IStrategy; strategy: IStrategy;
applications: IApplication[]; applications: ApplicationSchema[];
toggles: FeatureSchema[]; toggles: FeatureSchema[];
} }

View File

@ -1,40 +1,49 @@
import useSWR, { mutate, SWRConfiguration } from 'swr'; import useSWR, { mutate, SWRConfiguration } from 'swr';
import { useState, useEffect } from 'react'; import { useEffect, useState } from 'react';
import { formatApiPath } from 'utils/formatPath'; import { formatApiPath } from 'utils/formatPath';
import handleErrorResponses from '../httpErrorResponseHandler'; 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 extends ApplicationsSchema {
interface IUseApplicationsOutput {
applications: IApplication[];
refetchApplications: () => void; refetchApplications: () => void;
loading: boolean; loading: boolean;
error?: Error; error?: Error;
APPLICATIONS_CACHE_KEY: string;
} }
const PREFIX_KEY = 'api/admin/metrics/applications?';
const useApplications = ( const useApplications = (
params: GetApplicationsParams = {},
options: SWRConfiguration = {}, options: SWRConfiguration = {},
): IUseApplicationsOutput => { ): 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 () => { const fetcher = async () => {
return fetch(path, { return fetch(formatApiPath(KEY), {
method: 'GET', method: 'GET',
}) })
.then(handleErrorResponses('Applications data')) .then(handleErrorResponses('Applications data'))
.then((res) => res.json()); .then((res) => res.json());
}; };
const APPLICATIONS_CACHE_KEY = 'api/admin/metrics/applications'; const { data, error } = useSWR(KEY, fetcher, {
const { data, error } = useSWR(APPLICATIONS_CACHE_KEY, fetcher, {
...options, ...options,
}); });
const [loading, setLoading] = useState(!error && !data); const [loading, setLoading] = useState(!error && !data);
const refetchApplications = () => { const refetchApplications = () => {
mutate(APPLICATIONS_CACHE_KEY); mutate(KEY);
}; };
useEffect(() => { useEffect(() => {
@ -43,10 +52,10 @@ const useApplications = (
return { return {
applications: data?.applications || [], applications: data?.applications || [],
total: data?.total || 0,
error, error,
loading, loading,
refetchApplications, refetchApplications,
APPLICATIONS_CACHE_KEY,
}; };
}; };

View File

@ -1,8 +1,9 @@
import useSWR, { SWRConfiguration, useSWRConfig } from 'swr'; import useSWR, { SWRConfiguration } from 'swr';
import { useCallback, useEffect } from 'react'; import { useCallback, useEffect } from 'react';
import { formatApiPath } from 'utils/formatPath'; import { formatApiPath } from 'utils/formatPath';
import handleErrorResponses from '../httpErrorResponseHandler'; import handleErrorResponses from '../httpErrorResponseHandler';
import { SearchFeaturesParams, SearchFeaturesSchema } from 'openapi'; import { SearchFeaturesParams, SearchFeaturesSchema } from 'openapi';
import { useClearSWRCache } from 'hooks/useClearSWRCache';
type UseFeatureSearchOutput = { type UseFeatureSearchOutput = {
loading: boolean; loading: boolean;
@ -26,19 +27,6 @@ const fallbackData: SearchFeaturesSchema = {
const PREFIX_KEY = 'api/admin/search/features?'; 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 createFeatureSearch = () => {
const internalCache: InternalCache = {}; const internalCache: InternalCache = {};

View File

@ -6,6 +6,7 @@ import {
GetProjectApplicationsParams, GetProjectApplicationsParams,
ProjectApplicationsSchema, ProjectApplicationsSchema,
} from 'openapi'; } from 'openapi';
import { useClearSWRCache } from 'hooks/useClearSWRCache';
type UseProjectApplicationsOutput = { type UseProjectApplicationsOutput = {
loading: boolean; loading: boolean;
@ -66,6 +67,7 @@ const getProjectApplicationsFetcher = (
), ),
).toString(); ).toString();
const KEY = `${getPrefixKey(projectId)}${urlSearchParams}`; const KEY = `${getPrefixKey(projectId)}${urlSearchParams}`;
useClearSWRCache(KEY, getPrefixKey(projectId));
const fetcher = () => { const fetcher = () => {
const path = formatApiPath(KEY); const path = formatApiPath(KEY);
return fetch(path, { return fetch(path, {

View File

@ -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),
);
};