1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-04-24 01:18:01 +02: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']; environments: IProject['environments'];
loading: boolean; loading: boolean;
onChange: () => void; onChange: () => void;
total?: number;
} }
const staticColumns = ['Select', 'Actions', 'name', 'favorite']; const staticColumns = ['Select', 'Actions', 'name', 'favorite'];
@ -86,6 +87,7 @@ export const ProjectFeatureToggles = ({
loading, loading,
environments: newEnvironments = [], environments: newEnvironments = [],
onChange, onChange,
total,
}: IProjectFeatureTogglesProps) => { }: IProjectFeatureTogglesProps) => {
const { classes: styles } = useStyles(); const { classes: styles } = useStyles();
const theme = useTheme(); const theme = useTheme();
@ -492,7 +494,7 @@ export const ProjectFeatureToggles = ({
<PageHeader <PageHeader
titleElement={ titleElement={
showTitle showTitle
? `Feature toggles (${rows.length})` ? `Feature toggles (${total || rows.length})`
: null : null
} }
actions={ actions={

View File

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

View File

@ -4,20 +4,30 @@ import { IFeatureToggleListItem } from 'interfaces/featureToggle';
import { formatApiPath } from 'utils/formatPath'; import { formatApiPath } from 'utils/formatPath';
import handleErrorResponses from '../httpErrorResponseHandler'; import handleErrorResponses from '../httpErrorResponseHandler';
type IFeatureSearchResponse = { features: IFeatureToggleListItem[] }; type IFeatureSearchResponse = {
features: IFeatureToggleListItem[];
nextCursor: string;
total: number;
};
interface IUseFeatureSearchOutput { interface IUseFeatureSearchOutput {
features: IFeatureSearchResponse; features: IFeatureToggleListItem[];
total: number;
nextCursor: string;
loading: boolean; loading: boolean;
error: string; error: string;
refetch: () => void; refetch: () => void;
} }
const fallbackFeatures: { features: IFeatureToggleListItem[]; total: number } = const fallbackData: {
{ features: IFeatureToggleListItem[];
features: [], total: number;
total: 0, nextCursor: string;
}; } = {
features: [],
total: 0,
nextCursor: '',
};
export const useFeatureSearch = ( export const useFeatureSearch = (
cursor: string, cursor: string,
@ -35,16 +45,24 @@ export const useFeatureSearch = (
mutate(); mutate();
}, [mutate]); }, [mutate]);
const returnData = data || fallbackData;
return { return {
features: data || fallbackFeatures, ...returnData,
loading: !error && !data, loading: !error && !data,
error, error,
refetch, 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 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 fetcher = () => {
const path = formatApiPath(KEY); const path = formatApiPath(KEY);
@ -52,7 +70,15 @@ const getFeatureSearchFetcher = (projectId: string, cursor: string) => {
method: 'GET', method: 'GET',
}) })
.then(handleErrorResponses('Feature search')) .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 { return {

View File

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