1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-20 00:08:02 +01:00

feat: infinite scroll API trigger (#5242)

This commit is contained in:
Mateusz Kwasniewski 2023-11-01 15:56:06 +01:00 committed by GitHub
parent d074254b61
commit bc66fb649f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 76 additions and 19 deletions

View File

@ -73,6 +73,7 @@ interface IProjectFeatureTogglesProps {
environments: IProject['environments'];
loading: boolean;
onChange: () => void;
total?: number;
}
const staticColumns = ['Select', 'Actions', 'name', 'favorite'];
@ -86,6 +87,7 @@ export const ProjectFeatureToggles = ({
loading,
environments: newEnvironments = [],
onChange,
total,
}: IProjectFeatureTogglesProps) => {
const { classes: styles } = useStyles();
const theme = useTheme();
@ -492,7 +494,7 @@ export const ProjectFeatureToggles = ({
<PageHeader
titleElement={
showTitle
? `Feature toggles (${rows.length})`
? `Feature toggles (${total || rows.length})`
: null
}
actions={

View File

@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import React, { useEffect, useState } from 'react';
import useProject, {
useProjectNameOrId,
} from 'hooks/api/getters/useProject/useProject';
@ -12,7 +12,9 @@ import { useLastViewedProject } from 'hooks/useLastViewedProject';
import { ProjectStats } from './ProjectStats/ProjectStats';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { useUiFlag } from 'hooks/useUiFlag';
import { useFeatureSearch } from '../../../hooks/api/getters/useFeatureSearch/useFeatureSearch';
import { useFeatureSearch } from 'hooks/api/getters/useFeatureSearch/useFeatureSearch';
import { useOnVisible } from 'hooks/useOnVisible';
import { IFeatureToggleListItem } from 'interfaces/featureToggle';
const refreshInterval = 15 * 1000;
@ -40,14 +42,27 @@ const InfiniteProjectOverview = () => {
const { project, loading: projectLoading } = useProject(projectId, {
refreshInterval,
});
const [nextCursor, setNextCursor] = useState('');
const [cursor, setCursor] = useState('');
const {
features: searchFeatures,
nextCursor,
total,
refetch,
loading,
} = useFeatureSearch(nextCursor, projectId, { refreshInterval });
} = useFeatureSearch(cursor, projectId, { refreshInterval });
const { members, features, health, description, environments, stats } =
project;
const fetchNextPageRef = useOnVisible<HTMLDivElement>(() => {
if (!loading && nextCursor !== cursor && nextCursor !== '') {
setCursor(nextCursor);
}
});
const [dataList, setDataList] = useState<IFeatureToggleListItem[]>([]);
useEffect(() => {
setDataList((prev) => [...prev, ...searchFeatures]);
}, [JSON.stringify(searchFeatures)]);
return (
<StyledContainer>
@ -63,12 +78,18 @@ const InfiniteProjectOverview = () => {
<ProjectStats stats={project.stats} />
<StyledProjectToggles>
<ProjectFeatureToggles
key={loading ? 'loading' : 'ready'}
features={searchFeatures.features}
key={
loading && dataList.length === 0
? 'loading'
: 'ready'
}
features={dataList}
environments={environments}
loading={loading}
loading={loading && dataList.length === 0}
onChange={refetch}
total={total}
/>
<div ref={fetchNextPageRef} />
</StyledProjectToggles>
</StyledContentContainer>
</StyledContainer>

View File

@ -4,20 +4,30 @@ import { IFeatureToggleListItem } from 'interfaces/featureToggle';
import { formatApiPath } from 'utils/formatPath';
import handleErrorResponses from '../httpErrorResponseHandler';
type IFeatureSearchResponse = { features: IFeatureToggleListItem[] };
type IFeatureSearchResponse = {
features: IFeatureToggleListItem[];
nextCursor: string;
total: number;
};
interface IUseFeatureSearchOutput {
features: IFeatureSearchResponse;
features: IFeatureToggleListItem[];
total: number;
nextCursor: string;
loading: boolean;
error: string;
refetch: () => void;
}
const fallbackFeatures: { features: IFeatureToggleListItem[]; total: number } =
{
features: [],
total: 0,
};
const fallbackData: {
features: IFeatureToggleListItem[];
total: number;
nextCursor: string;
} = {
features: [],
total: 0,
nextCursor: '',
};
export const useFeatureSearch = (
cursor: string,
@ -35,16 +45,24 @@ export const useFeatureSearch = (
mutate();
}, [mutate]);
const returnData = data || fallbackData;
return {
features: data || fallbackFeatures,
...returnData,
loading: !error && !data,
error,
refetch,
};
};
// temporary experiment
const getQueryParam = (queryParam: string, path: string | null) => {
const url = new URL(path || '', 'https://getunleash.io');
const params = new URLSearchParams(url.search);
return params.get(queryParam) || '';
};
const getFeatureSearchFetcher = (projectId: string, cursor: string) => {
const KEY = `api/admin/search/features?projectId=${projectId}&cursor=${cursor}`;
const KEY = `api/admin/search/features?projectId=${projectId}&cursor=${cursor}&limit=25`;
const fetcher = () => {
const path = formatApiPath(KEY);
@ -52,7 +70,15 @@ const getFeatureSearchFetcher = (projectId: string, cursor: string) => {
method: 'GET',
})
.then(handleErrorResponses('Feature search'))
.then((res) => res.json());
.then(async (res) => {
const json = await res.json();
// TODO: try using Link as key
const nextCursor = getQueryParam(
'cursor',
res.headers.get('link'),
);
return { ...json, nextCursor };
});
};
return {

View File

@ -536,6 +536,14 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
features: IFeatureOverview[];
total: number;
}> {
let environmentCount = 1;
if (projectId) {
const rows = await this.db('project_environments')
.count('* as environmentCount')
.where('project_id', projectId);
environmentCount = Number(rows[0].environmentCount);
}
let query = this.db('features');
if (projectId) {
query = query.where({ project: projectId });
@ -679,7 +687,7 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
.countDistinct({ total: 'features.name' })
.first();
query = query.select(selectColumns).limit(limit);
query = query.select(selectColumns).limit(limit * environmentCount);
const rows = await query;
if (rows.length > 0) {