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