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:
parent
d074254b61
commit
bc66fb649f
@ -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={
|
||||
|
@ -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>
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
Loading…
Reference in New Issue
Block a user