Skip to content

Commit

Permalink
feat: update project overview endpoint (#5518)
Browse files Browse the repository at this point in the history
1. Created new hook for endpoint
2. Start removing useProject hook, when features not needed.
  • Loading branch information
sjaanus authored Dec 1, 2023
1 parent 87f03ea commit a299885
Show file tree
Hide file tree
Showing 10 changed files with 172 additions and 35 deletions.
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { useMemo } from 'react';
import { styled, SvgIconTypeMap } from '@mui/material';
import type { IFeatureToggleListItem } from 'interfaces/featureToggle';
import { getFeatureTypeIcons } from 'utils/getFeatureTypeIcons';
import {
StyledCount,
StyledProjectInfoWidgetContainer,
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 }) => ({
Expand Down Expand Up @@ -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,
Expand All @@ -78,7 +89,7 @@ export const ToggleTypesWidget = ({ features }: IToggleTypesWidgetProps) => {
'kill-switch': kill,
permission,
};
}, [features]);
}, [featureTypeCounts]);

return (
<StyledProjectInfoWidgetContainer
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import { Box, styled, useMediaQuery, useTheme } from '@mui/material';
import type { ProjectStatsSchema } from 'openapi/models/projectStatsSchema';
import type { IFeatureToggleListItem } from 'interfaces/featureToggle';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { DEFAULT_PROJECT_ID } from 'hooks/api/getters/useDefaultProject/useDefaultProjectId';
import { HealthWidget } from './HealthWidget';
import { ToggleTypesWidget } from './ToggleTypesWidget';
import { FlagTypesWidget } from './FlagTypesWidget';
import { MetaWidget } from './MetaWidget';
import { ProjectMembersWidget } from './ProjectMembersWidget';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { ChangeRequestsWidget } from './ChangeRequestsWidget';
import { flexRow } from 'themes/themeStyles';
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
import { FeatureTypeCount } from 'interfaces/project';

interface IProjectInfoProps {
id: string;
memberCount: number;
features: IFeatureToggleListItem[];
featureTypeCounts: FeatureTypeCount[];
health: number;
description?: string;
stats: ProjectStatsSchema;
Expand All @@ -42,7 +42,7 @@ const ProjectInfo = ({
description,
memberCount,
health,
features,
featureTypeCounts,
stats,
}: IProjectInfoProps) => {
const { isEnterprise } = useUiConfig();
Expand Down Expand Up @@ -97,7 +97,7 @@ const ProjectInfo = ({
/>
}
/>
<ToggleTypesWidget features={features} />
<FlagTypesWidget featureTypeCounts={featureTypeCounts} />
</StyledProjectInfoSidebarContainer>
</aside>
);
Expand Down
33 changes: 28 additions & 5 deletions frontend/src/component/project/Project/ProjectOverview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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,
});

Expand Down Expand Up @@ -84,8 +86,14 @@ const PaginatedProjectOverview: FC<{
},
);

const { members, features, health, description, environments, stats } =
project;
const {
members,
featureTypeCounts,
health,
description,
environments,
stats,
} = project;

return (
<StyledContainer key={projectId}>
Expand All @@ -94,7 +102,7 @@ const PaginatedProjectOverview: FC<{
description={description}
memberCount={members}
health={health}
features={features}
featureTypeCounts={featureTypeCounts}
stats={stats}
/>
<StyledContentContainer>
Expand Down Expand Up @@ -140,14 +148,29 @@ const ProjectOverview = () => {

if (featureSearchFrontend) return <PaginatedProjectOverview />;

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 (
<StyledContainer>
<ProjectInfo
id={projectId}
description={description}
memberCount={members}
health={health}
features={features}
featureTypeCounts={featureTypeCounts}
stats={stats}
/>
<StyledContentContainer>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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}`);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
Expand Down
9 changes: 8 additions & 1 deletion frontend/src/hooks/api/getters/useProject/useProject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<IProject>(KEY, fetcher, options);
Expand All @@ -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;
};
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
};
};
Original file line number Diff line number Diff line change
@@ -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<IProjectOverview>(
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;
22 changes: 22 additions & 0 deletions frontend/src/interfaces/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ export type FeatureNamingType = {
description: string;
};

export type FeatureTypeCount = {
type: string;
count: number;
};

export interface IProject {
id?: string;
members: number;
Expand All @@ -38,6 +43,23 @@ export interface IProject {
featureNaming?: FeatureNamingType;
}

export interface IProjectOverview {
id?: string;
members: number;
version: string;
name: string;
description?: string;
environments: Array<ProjectEnvironmentType>;
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;
Expand Down

0 comments on commit a299885

Please sign in to comment.