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:
parent
87f03ea088
commit
a299885e22
@ -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
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
|
@ -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}`);
|
||||
|
||||
|
@ -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}`);
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
@ -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;
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user