diff --git a/frontend/src/component/project/Project/ProjectInfo/ToggleTypesWidget.tsx b/frontend/src/component/project/Project/ProjectInfo/FlagTypesWidget.tsx similarity index 73% rename from frontend/src/component/project/Project/ProjectInfo/ToggleTypesWidget.tsx rename to frontend/src/component/project/Project/ProjectInfo/FlagTypesWidget.tsx index ff052ba82827..f0e0cc35a9a5 100644 --- a/frontend/src/component/project/Project/ProjectInfo/ToggleTypesWidget.tsx +++ b/frontend/src/component/project/Project/ProjectInfo/FlagTypesWidget.tsx @@ -1,6 +1,5 @@ import { useMemo } from 'react'; import { styled, SvgIconTypeMap } from '@mui/material'; -import type { IFeatureToggleListItem } from 'interfaces/featureToggle'; import { getFeatureTypeIcons } from 'utils/getFeatureTypeIcons'; import { StyledCount, @@ -8,9 +7,10 @@ import { StyledWidgetTitle, } from './ProjectInfo.styles'; import { OverridableComponent } from '@mui/material/OverridableComponent'; +import { FeatureTypeCount } from 'interfaces/project'; -export interface IToggleTypesWidgetProps { - features: IFeatureToggleListItem[]; +export interface IFlagTypesWidgetProps { + featureTypeCounts: FeatureTypeCount[]; } const StyledTypeCount = styled(StyledCount)(({ theme }) => ({ @@ -53,23 +53,34 @@ const ToggleTypesRow = ({ type, Icon, count }: IToggleTypeRowProps) => { ); }; -export const ToggleTypesWidget = ({ features }: IToggleTypesWidgetProps) => { +export const FlagTypesWidget = ({ + featureTypeCounts, +}: IFlagTypesWidgetProps) => { const featureTypeStats = useMemo(() => { const release = - features?.filter((feature) => feature.type === 'release').length || - 0; + featureTypeCounts.find( + (featureType) => featureType.type === 'release', + )?.count || 0; + const experiment = - features?.filter((feature) => feature.type === 'experiment') - .length || 0; + featureTypeCounts.find( + (featureType) => featureType.type === 'experiment', + )?.count || 0; + const operational = - features?.filter((feature) => feature.type === 'operational') - .length || 0; + featureTypeCounts.find( + (featureType) => featureType.type === 'operational', + )?.count || 0; + const kill = - features?.filter((feature) => feature.type === 'kill-switch') - .length || 0; + featureTypeCounts.find( + (featureType) => featureType.type === 'kill-switch', + )?.count || 0; + const permission = - features?.filter((feature) => feature.type === 'permission') - .length || 0; + featureTypeCounts.find( + (featureType) => featureType.type === 'permission', + )?.count || 0; return { release, @@ -78,7 +89,7 @@ export const ToggleTypesWidget = ({ features }: IToggleTypesWidgetProps) => { 'kill-switch': kill, permission, }; - }, [features]); + }, [featureTypeCounts]); return ( { const { isEnterprise } = useUiConfig(); @@ -97,7 +97,7 @@ const ProjectInfo = ({ /> } /> - + ); diff --git a/frontend/src/component/project/Project/ProjectOverview.tsx b/frontend/src/component/project/Project/ProjectOverview.tsx index e3c702c66a75..d2debe4d8613 100644 --- a/frontend/src/component/project/Project/ProjectOverview.tsx +++ b/frontend/src/component/project/Project/ProjectOverview.tsx @@ -20,6 +20,8 @@ import { } from './ProjectFeatureToggles/PaginatedProjectFeatureToggles'; import { useTableState } from 'hooks/useTableState'; +import useProjectOverview from 'hooks/api/getters/useProjectOverview/useProjectOverview'; +import { FeatureTypeCount } from '../../../interfaces/project'; const refreshInterval = 15 * 1000; @@ -49,7 +51,7 @@ const PaginatedProjectOverview: FC<{ storageKey?: string; }> = ({ fullWidth, storageKey = 'project-overview' }) => { const projectId = useRequiredPathParam('projectId'); - const { project, loading: projectLoading } = useProject(projectId, { + const { project } = useProjectOverview(projectId, { refreshInterval, }); @@ -84,8 +86,14 @@ const PaginatedProjectOverview: FC<{ }, ); - const { members, features, health, description, environments, stats } = - project; + const { + members, + featureTypeCounts, + health, + description, + environments, + stats, + } = project; return ( @@ -94,7 +102,7 @@ const PaginatedProjectOverview: FC<{ description={description} memberCount={members} health={health} - features={features} + featureTypeCounts={featureTypeCounts} stats={stats} /> @@ -140,6 +148,21 @@ const ProjectOverview = () => { if (featureSearchFrontend) return ; + const featureTypeCounts = features.reduce( + (acc: FeatureTypeCount[], feature) => { + const existingEntry = acc.find( + (entry) => entry.type === feature.type, + ); + if (existingEntry) { + existingEntry.count += 1; + } else { + acc.push({ type: feature.type, count: 1 }); + } + return acc; + }, + [], + ); + return ( { description={description} memberCount={members} health={health} - features={features} + featureTypeCounts={featureTypeCounts} stats={stats} /> diff --git a/frontend/src/component/project/Project/ProjectSettings/ProjectApiAccess/ProjectApiAccess.tsx b/frontend/src/component/project/Project/ProjectSettings/ProjectApiAccess/ProjectApiAccess.tsx index 660f636da29b..d48c03b5495e 100644 --- a/frontend/src/component/project/Project/ProjectSettings/ProjectApiAccess/ProjectApiAccess.tsx +++ b/frontend/src/component/project/Project/ProjectSettings/ProjectApiAccess/ProjectApiAccess.tsx @@ -24,10 +24,11 @@ import { ActionCell } from 'component/common/Table/cells/ActionCell/ActionCell'; import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; import useProjectApiTokensApi from 'hooks/api/actions/useProjectApiTokensApi/useProjectApiTokensApi'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import { useProjectOverviewNameOrId } from 'hooks/api/getters/useProjectOverview/useProjectOverview'; export const ProjectApiAccess = () => { const projectId = useRequiredPathParam('projectId'); - const projectName = useProjectNameOrId(projectId); + const projectName = useProjectOverviewNameOrId(projectId); const { hasAccess } = useContext(AccessContext); const { tokens, diff --git a/frontend/src/component/project/Project/ProjectSettings/ProjectDefaultStrategySettings/ProjectDefaultStrategySettings.tsx b/frontend/src/component/project/Project/ProjectSettings/ProjectDefaultStrategySettings/ProjectDefaultStrategySettings.tsx index 7c8d25cde3ad..7cf7ed64972c 100644 --- a/frontend/src/component/project/Project/ProjectSettings/ProjectDefaultStrategySettings/ProjectDefaultStrategySettings.tsx +++ b/frontend/src/component/project/Project/ProjectSettings/ProjectDefaultStrategySettings/ProjectDefaultStrategySettings.tsx @@ -1,8 +1,5 @@ import React, { useContext } from 'react'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; -import useProject, { - useProjectNameOrId, -} from 'hooks/api/getters/useProject/useProject'; import AccessContext from 'contexts/AccessContext'; import { usePageTitle } from 'hooks/usePageTitle'; import { PageContent } from 'component/common/PageContent/PageContent'; @@ -11,17 +8,20 @@ import { UPDATE_PROJECT } from 'component/providers/AccessProvider/permissions'; import { Alert, styled } from '@mui/material'; import ProjectEnvironment from './ProjectEnvironment/ProjectEnvironment'; import { Route, Routes, useNavigate } from 'react-router-dom'; -import { SidebarModal } from '../../../../common/SidebarModal/SidebarModal'; +import { SidebarModal } from 'component/common/SidebarModal/SidebarModal'; import EditDefaultStrategy from './ProjectEnvironment/ProjectEnvironmentDefaultStrategy/EditDefaultStrategy'; +import useProjectOverview, { + useProjectOverviewNameOrId, +} from 'hooks/api/getters/useProjectOverview/useProjectOverview'; const StyledAlert = styled(Alert)(({ theme }) => ({ marginBottom: theme.spacing(4), })); export const ProjectDefaultStrategySettings = () => { const projectId = useRequiredPathParam('projectId'); - const projectName = useProjectNameOrId(projectId); + const projectName = useProjectOverviewNameOrId(projectId); const { hasAccess } = useContext(AccessContext); - const { project } = useProject(projectId); + const { project } = useProjectOverview(projectId); const navigate = useNavigate(); usePageTitle(`Project default strategy configuration – ${projectName}`); diff --git a/frontend/src/component/project/Project/ProjectSettings/Settings/Settings.tsx b/frontend/src/component/project/Project/ProjectSettings/Settings/Settings.tsx index 8f9abe3dc9b6..6a2cbae9d97a 100644 --- a/frontend/src/component/project/Project/ProjectSettings/Settings/Settings.tsx +++ b/frontend/src/component/project/Project/ProjectSettings/Settings/Settings.tsx @@ -6,14 +6,14 @@ import AccessContext from 'contexts/AccessContext'; import { UPDATE_PROJECT } from 'component/providers/AccessProvider/permissions'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import { usePageTitle } from 'hooks/usePageTitle'; -import { useProjectNameOrId } from 'hooks/api/getters/useProject/useProject'; import EditProject from './EditProject/EditProject'; import { PremiumFeature } from 'component/common/PremiumFeature/PremiumFeature'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; +import { useProjectOverviewNameOrId } from 'hooks/api/getters/useProjectOverview/useProjectOverview'; export const Settings = () => { const projectId = useRequiredPathParam('projectId'); - const projectName = useProjectNameOrId(projectId); + const projectName = useProjectOverviewNameOrId(projectId); const { hasAccess } = useContext(AccessContext); const { isOss } = useUiConfig(); usePageTitle(`Project configuration – ${projectName}`); diff --git a/frontend/src/hooks/api/getters/useProject/useProject.ts b/frontend/src/hooks/api/getters/useProject/useProject.ts index 8961958bba59..5c9cd71281ca 100644 --- a/frontend/src/hooks/api/getters/useProject/useProject.ts +++ b/frontend/src/hooks/api/getters/useProject/useProject.ts @@ -26,6 +26,10 @@ const fallbackProject: IProject = { }, }; +/** + * @deprecated It is recommended to use useProjectOverview instead, unless you need project features. + * In that case, we should create a project features endpoint and use that instead if features needed. + */ const useProject = (id: string, options: SWRConfiguration = {}) => { const { KEY, fetcher } = getProjectFetcher(id); const { data, error, mutate } = useSWR(KEY, fetcher, options); @@ -41,7 +45,10 @@ const useProject = (id: string, options: SWRConfiguration = {}) => { refetch, }; }; - +/** + * @deprecated It is recommended to use useProjectOverviewNameOrId instead, unless you need project features. + * In that case, we probably should create a project features endpoint and use that instead if features needed. + */ export const useProjectNameOrId = (id: string): string => { return useProject(id).project.name || id; }; diff --git a/frontend/src/hooks/api/getters/useProjectOverview/getProjectOverviewFetcher.ts b/frontend/src/hooks/api/getters/useProjectOverview/getProjectOverviewFetcher.ts new file mode 100644 index 000000000000..51324e9f3bb9 --- /dev/null +++ b/frontend/src/hooks/api/getters/useProjectOverview/getProjectOverviewFetcher.ts @@ -0,0 +1,20 @@ +import { formatApiPath } from 'utils/formatPath'; +import handleErrorResponses from '../httpErrorResponseHandler'; + +export const getProjectOverviewFetcher = (id: string) => { + const fetcher = () => { + const path = formatApiPath(`api/admin/projects/${id}/overview`); + return fetch(path, { + method: 'GET', + }) + .then(handleErrorResponses('Project overview')) + .then((res) => res.json()); + }; + + const KEY = `api/admin/projects/${id}/overview`; + + return { + fetcher, + KEY, + }; +}; diff --git a/frontend/src/hooks/api/getters/useProjectOverview/useProjectOverview.ts b/frontend/src/hooks/api/getters/useProjectOverview/useProjectOverview.ts new file mode 100644 index 000000000000..ad76d42f4b2a --- /dev/null +++ b/frontend/src/hooks/api/getters/useProjectOverview/useProjectOverview.ts @@ -0,0 +1,53 @@ +import useSWR, { SWRConfiguration } from 'swr'; +import { useCallback } from 'react'; +import { getProjectOverviewFetcher } from './getProjectOverviewFetcher'; +import { IProjectOverview } from 'interfaces/project'; + +const fallbackProject: IProjectOverview = { + featureTypeCounts: [], + environments: [], + name: '', + health: 0, + members: 0, + version: '1', + description: 'Default', + favorite: false, + mode: 'open', + defaultStickiness: 'default', + stats: { + archivedCurrentWindow: 0, + archivedPastWindow: 0, + avgTimeToProdCurrentWindow: 0, + createdCurrentWindow: 0, + createdPastWindow: 0, + projectActivityCurrentWindow: 0, + projectActivityPastWindow: 0, + projectMembersAddedCurrentWindow: 0, + }, +}; + +const useProjectOverview = (id: string, options: SWRConfiguration = {}) => { + const { KEY, fetcher } = getProjectOverviewFetcher(id); + const { data, error, mutate } = useSWR( + KEY, + fetcher, + options, + ); + + const refetch = useCallback(() => { + mutate(); + }, [mutate]); + + return { + project: data || fallbackProject, + loading: !error && !data, + error, + refetch, + }; +}; + +export const useProjectOverviewNameOrId = (id: string): string => { + return useProjectOverview(id).project.name || id; +}; + +export default useProjectOverview; diff --git a/frontend/src/interfaces/project.ts b/frontend/src/interfaces/project.ts index b17f6fb86dd1..80a84053de41 100644 --- a/frontend/src/interfaces/project.ts +++ b/frontend/src/interfaces/project.ts @@ -21,6 +21,11 @@ export type FeatureNamingType = { description: string; }; +export type FeatureTypeCount = { + type: string; + count: number; +}; + export interface IProject { id?: string; members: number; @@ -38,6 +43,23 @@ export interface IProject { featureNaming?: FeatureNamingType; } +export interface IProjectOverview { + id?: string; + members: number; + version: string; + name: string; + description?: string; + environments: Array; + health: number; + stats: ProjectStatsSchema; + featureTypeCounts: FeatureTypeCount[]; + favorite: boolean; + mode: ProjectMode; + defaultStickiness: string; + featureLimit?: number; + featureNaming?: FeatureNamingType; +} + export interface IProjectHealthReport extends IProject { staleCount: number; potentiallyStaleCount: number;