1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-06-14 01:16:17 +02:00

feat: update project overview endpoint (#5518)

1. Created new hook for endpoint
2. Start removing useProject hook, when features not needed.
This commit is contained in:
Jaanus Sellin 2023-12-01 20:00:35 +02:00 committed by GitHub
parent 87f03ea088
commit a299885e22
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 172 additions and 35 deletions

View File

@ -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 (
<StyledProjectInfoWidgetContainer

View File

@ -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;
@ -42,7 +42,7 @@ const ProjectInfo = ({
description,
memberCount,
health,
features,
featureTypeCounts,
stats,
}: IProjectInfoProps) => {
const { isEnterprise } = useUiConfig();
@ -97,7 +97,7 @@ const ProjectInfo = ({
/>
}
/>
<ToggleTypesWidget features={features} />
<FlagTypesWidget featureTypeCounts={featureTypeCounts} />
</StyledProjectInfoSidebarContainer>
</aside>
);

View File

@ -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 (
<StyledContainer key={projectId}>
@ -94,7 +102,7 @@ const PaginatedProjectOverview: FC<{
description={description}
memberCount={members}
health={health}
features={features}
featureTypeCounts={featureTypeCounts}
stats={stats}
/>
<StyledContentContainer>
@ -140,6 +148,21 @@ 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
@ -147,7 +170,7 @@ const ProjectOverview = () => {
description={description}
memberCount={members}
health={health}
features={features}
featureTypeCounts={featureTypeCounts}
stats={stats}
/>
<StyledContentContainer>

View File

@ -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,

View File

@ -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}`);

View File

@ -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}`);

View File

@ -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);
@ -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;
};

View File

@ -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,
};
};

View File

@ -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;

View File

@ -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<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;