diff --git a/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx b/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx index 708b0fa509..b6d71b15a2 100644 --- a/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx +++ b/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx @@ -72,6 +72,7 @@ interface IProjectFeatureTogglesProps { features: IProject['features']; environments: IProject['environments']; loading: boolean; + onChange: () => void; } const staticColumns = ['Select', 'Actions', 'name', 'favorite']; @@ -84,6 +85,7 @@ export const ProjectFeatureToggles = ({ features, loading, environments: newEnvironments = [], + onChange, }: IProjectFeatureTogglesProps) => { const { classes: styles } = useStyles(); const theme = useTheme(); @@ -118,7 +120,6 @@ export const ProjectFeatureToggles = ({ ? [{ environment: 'a' }, { environment: 'b' }, { environment: 'c' }] : newEnvironments, ); - const { refetch } = useProject(projectId); const { isFavoritesPinned, sortTypes, onChangeIsFavoritePinned } = usePinnedFavorites( searchParams.has('favorites') @@ -140,9 +141,9 @@ export const ProjectFeatureToggles = ({ } else { await favorite(projectId, feature.name); } - refetch(); + onChange(); }, - [projectId, refetch], + [projectId, onChange], ); const showTagsColumn = useMemo( @@ -263,7 +264,7 @@ export const ProjectFeatureToggles = ({ projectId, name, isChangeRequestEnabled, - refetch, + onChange, onFeatureToggle, ); @@ -617,16 +618,14 @@ export const ProjectFeatureToggles = ({ isOpen={Boolean(featureStaleDialogState.featureId)} onClose={() => { setFeatureStaleDialogState({}); - refetch(); + onChange(); }} featureId={featureStaleDialogState.featureId || ''} projectId={projectId} /> { - refetch(); - }} + onConfirm={onChange} onClose={() => { setFeatureArchiveState(undefined); }} diff --git a/frontend/src/component/project/Project/ProjectOverview.tsx b/frontend/src/component/project/Project/ProjectOverview.tsx index 804f7cb1a8..473a9b5648 100644 --- a/frontend/src/component/project/Project/ProjectOverview.tsx +++ b/frontend/src/component/project/Project/ProjectOverview.tsx @@ -1,4 +1,4 @@ -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import useProject, { useProjectNameOrId, } from 'hooks/api/getters/useProject/useProject'; @@ -12,6 +12,7 @@ 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'; const refreshInterval = 15 * 1000; @@ -34,10 +35,50 @@ const StyledContentContainer = styled(Box)(() => ({ minWidth: 0, })); +const InfiniteProjectOverview = () => { + const projectId = useRequiredPathParam('projectId'); + const { project, loading: projectLoading } = useProject(projectId, { + refreshInterval, + }); + const [nextCursor, setNextCursor] = useState(''); + const { + features: searchFeatures, + refetch, + loading, + } = useFeatureSearch(nextCursor, projectId, { refreshInterval }); + const { members, features, health, description, environments, stats } = + project; + + return ( + + + + + + + + + + ); +}; + const ProjectOverview = () => { const projectId = useRequiredPathParam('projectId'); const projectName = useProjectNameOrId(projectId); - const { project, loading } = useProject(projectId, { + const { project, loading, refetch } = useProject(projectId, { refreshInterval, }); const { members, features, health, description, environments, stats } = @@ -45,11 +86,14 @@ const ProjectOverview = () => { usePageTitle(`Project overview – ${projectName}`); const { setLastViewed } = useLastViewedProject(); const featureSwitchRefactor = useUiFlag('featureSwitchRefactor'); + const featureSearchFrontend = useUiFlag('featureSearchFrontend'); useEffect(() => { setLastViewed(projectId); }, [projectId, setLastViewed]); + if (featureSearchFrontend) return ; + return ( { features={features} environments={environments} loading={loading} + onChange={refetch} /> )} elseShow={() => ( diff --git a/frontend/src/hooks/api/getters/useFeatureSearch/useFeatureSearch.ts b/frontend/src/hooks/api/getters/useFeatureSearch/useFeatureSearch.ts index ac2b0cc288..58bc6e785b 100644 --- a/frontend/src/hooks/api/getters/useFeatureSearch/useFeatureSearch.ts +++ b/frontend/src/hooks/api/getters/useFeatureSearch/useFeatureSearch.ts @@ -13,14 +13,18 @@ interface IUseFeatureSearchOutput { refetch: () => void; } -const fallbackFeatures: { features: IFeatureToggleListItem[] } = { - features: [], -}; +const fallbackFeatures: { features: IFeatureToggleListItem[]; total: number } = + { + features: [], + total: 0, + }; export const useFeatureSearch = ( + cursor: string, + projectId = '', options: SWRConfiguration = {}, ): IUseFeatureSearchOutput => { - const { KEY, fetcher } = getFeatureSearchFetcher(); + const { KEY, fetcher } = getFeatureSearchFetcher(projectId, cursor); const { data, error, mutate } = useSWR( KEY, fetcher, @@ -39,9 +43,11 @@ export const useFeatureSearch = ( }; }; -const getFeatureSearchFetcher = () => { +const getFeatureSearchFetcher = (projectId: string, cursor: string) => { + const KEY = `api/admin/search/features?projectId=${projectId}&cursor=${cursor}`; + const fetcher = () => { - const path = formatApiPath(`api/admin/search/features`); + const path = formatApiPath(KEY); return fetch(path, { method: 'GET', }) @@ -49,8 +55,6 @@ const getFeatureSearchFetcher = () => { .then((res) => res.json()); }; - const KEY = `api/admin/search/features`; - return { fetcher, KEY, diff --git a/frontend/src/interfaces/uiConfig.ts b/frontend/src/interfaces/uiConfig.ts index f794921889..6ba4265313 100644 --- a/frontend/src/interfaces/uiConfig.ts +++ b/frontend/src/interfaces/uiConfig.ts @@ -71,6 +71,8 @@ export type UiFlags = { playgroundImprovements?: boolean; featureSwitchRefactor?: boolean; scheduledConfigurationChanges?: boolean; + featureSearchAPI?: boolean; + featureSearchFrontend?: boolean; }; export interface IVersionInfo { diff --git a/src/lib/__snapshots__/create-config.test.ts.snap b/src/lib/__snapshots__/create-config.test.ts.snap index ee02a65605..4fe95a3cd2 100644 --- a/src/lib/__snapshots__/create-config.test.ts.snap +++ b/src/lib/__snapshots__/create-config.test.ts.snap @@ -86,6 +86,7 @@ exports[`should create default config 1`] = ` "embedProxyFrontend": true, "featureNamingPattern": false, "featureSearchAPI": false, + "featureSearchFrontend": false, "featureSwitchRefactor": false, "featuresExportImport": true, "filterInvalidClientMetrics": false, diff --git a/src/lib/types/experimental.ts b/src/lib/types/experimental.ts index 97bea90ee4..29d81c82ba 100644 --- a/src/lib/types/experimental.ts +++ b/src/lib/types/experimental.ts @@ -36,6 +36,7 @@ export type IFlagKey = | 'playgroundImprovements' | 'featureSwitchRefactor' | 'featureSearchAPI' + | 'featureSearchFrontend' | 'scheduledConfigurationChanges'; export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>; @@ -168,6 +169,10 @@ const flags: IFlags = { process.env.UNLEASH_EXPERIMENTAL_FEATURE_SEARCH_API, false, ), + featureSearchFrontend: parseEnvVarBoolean( + process.env.UNLEASH_EXPERIMENTAL_FEATURE_SEARCH_FRONTEND, + false, + ), scheduledConfigurationChanges: parseEnvVarBoolean( process.env.UNLEASH_EXPERIMENTAL_SCHEDULED_CONFIGURATION_CHANGES, false, diff --git a/src/server-dev.ts b/src/server-dev.ts index c18991adba..16bb08e93d 100644 --- a/src/server-dev.ts +++ b/src/server-dev.ts @@ -49,6 +49,7 @@ process.nextTick(async () => { playgroundImprovements: true, featureSwitchRefactor: true, featureSearchAPI: true, + featureSearchFrontend: false, }, }, authentication: {