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 { useMemo } from 'react';
|
||||||
import { styled, SvgIconTypeMap } from '@mui/material';
|
import { styled, SvgIconTypeMap } from '@mui/material';
|
||||||
import type { IFeatureToggleListItem } from 'interfaces/featureToggle';
|
|
||||||
import { getFeatureTypeIcons } from 'utils/getFeatureTypeIcons';
|
import { getFeatureTypeIcons } from 'utils/getFeatureTypeIcons';
|
||||||
import {
|
import {
|
||||||
StyledCount,
|
StyledCount,
|
||||||
@ -8,9 +7,10 @@ import {
|
|||||||
StyledWidgetTitle,
|
StyledWidgetTitle,
|
||||||
} from './ProjectInfo.styles';
|
} from './ProjectInfo.styles';
|
||||||
import { OverridableComponent } from '@mui/material/OverridableComponent';
|
import { OverridableComponent } from '@mui/material/OverridableComponent';
|
||||||
|
import { FeatureTypeCount } from 'interfaces/project';
|
||||||
|
|
||||||
export interface IToggleTypesWidgetProps {
|
export interface IFlagTypesWidgetProps {
|
||||||
features: IFeatureToggleListItem[];
|
featureTypeCounts: FeatureTypeCount[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const StyledTypeCount = styled(StyledCount)(({ theme }) => ({
|
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 featureTypeStats = useMemo(() => {
|
||||||
const release =
|
const release =
|
||||||
features?.filter((feature) => feature.type === 'release').length ||
|
featureTypeCounts.find(
|
||||||
0;
|
(featureType) => featureType.type === 'release',
|
||||||
|
)?.count || 0;
|
||||||
|
|
||||||
const experiment =
|
const experiment =
|
||||||
features?.filter((feature) => feature.type === 'experiment')
|
featureTypeCounts.find(
|
||||||
.length || 0;
|
(featureType) => featureType.type === 'experiment',
|
||||||
|
)?.count || 0;
|
||||||
|
|
||||||
const operational =
|
const operational =
|
||||||
features?.filter((feature) => feature.type === 'operational')
|
featureTypeCounts.find(
|
||||||
.length || 0;
|
(featureType) => featureType.type === 'operational',
|
||||||
|
)?.count || 0;
|
||||||
|
|
||||||
const kill =
|
const kill =
|
||||||
features?.filter((feature) => feature.type === 'kill-switch')
|
featureTypeCounts.find(
|
||||||
.length || 0;
|
(featureType) => featureType.type === 'kill-switch',
|
||||||
|
)?.count || 0;
|
||||||
|
|
||||||
const permission =
|
const permission =
|
||||||
features?.filter((feature) => feature.type === 'permission')
|
featureTypeCounts.find(
|
||||||
.length || 0;
|
(featureType) => featureType.type === 'permission',
|
||||||
|
)?.count || 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
release,
|
release,
|
||||||
@ -78,7 +89,7 @@ export const ToggleTypesWidget = ({ features }: IToggleTypesWidgetProps) => {
|
|||||||
'kill-switch': kill,
|
'kill-switch': kill,
|
||||||
permission,
|
permission,
|
||||||
};
|
};
|
||||||
}, [features]);
|
}, [featureTypeCounts]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledProjectInfoWidgetContainer
|
<StyledProjectInfoWidgetContainer
|
@ -1,21 +1,21 @@
|
|||||||
import { Box, styled, useMediaQuery, useTheme } from '@mui/material';
|
import { Box, styled, useMediaQuery, useTheme } from '@mui/material';
|
||||||
import type { ProjectStatsSchema } from 'openapi/models/projectStatsSchema';
|
import type { ProjectStatsSchema } from 'openapi/models/projectStatsSchema';
|
||||||
import type { IFeatureToggleListItem } from 'interfaces/featureToggle';
|
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import { DEFAULT_PROJECT_ID } from 'hooks/api/getters/useDefaultProject/useDefaultProjectId';
|
import { DEFAULT_PROJECT_ID } from 'hooks/api/getters/useDefaultProject/useDefaultProjectId';
|
||||||
import { HealthWidget } from './HealthWidget';
|
import { HealthWidget } from './HealthWidget';
|
||||||
import { ToggleTypesWidget } from './ToggleTypesWidget';
|
import { FlagTypesWidget } from './FlagTypesWidget';
|
||||||
import { MetaWidget } from './MetaWidget';
|
import { MetaWidget } from './MetaWidget';
|
||||||
import { ProjectMembersWidget } from './ProjectMembersWidget';
|
import { ProjectMembersWidget } from './ProjectMembersWidget';
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
import { ChangeRequestsWidget } from './ChangeRequestsWidget';
|
import { ChangeRequestsWidget } from './ChangeRequestsWidget';
|
||||||
import { flexRow } from 'themes/themeStyles';
|
import { flexRow } from 'themes/themeStyles';
|
||||||
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
||||||
|
import { FeatureTypeCount } from 'interfaces/project';
|
||||||
|
|
||||||
interface IProjectInfoProps {
|
interface IProjectInfoProps {
|
||||||
id: string;
|
id: string;
|
||||||
memberCount: number;
|
memberCount: number;
|
||||||
features: IFeatureToggleListItem[];
|
featureTypeCounts: FeatureTypeCount[];
|
||||||
health: number;
|
health: number;
|
||||||
description?: string;
|
description?: string;
|
||||||
stats: ProjectStatsSchema;
|
stats: ProjectStatsSchema;
|
||||||
@ -42,7 +42,7 @@ const ProjectInfo = ({
|
|||||||
description,
|
description,
|
||||||
memberCount,
|
memberCount,
|
||||||
health,
|
health,
|
||||||
features,
|
featureTypeCounts,
|
||||||
stats,
|
stats,
|
||||||
}: IProjectInfoProps) => {
|
}: IProjectInfoProps) => {
|
||||||
const { isEnterprise } = useUiConfig();
|
const { isEnterprise } = useUiConfig();
|
||||||
@ -97,7 +97,7 @@ const ProjectInfo = ({
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<ToggleTypesWidget features={features} />
|
<FlagTypesWidget featureTypeCounts={featureTypeCounts} />
|
||||||
</StyledProjectInfoSidebarContainer>
|
</StyledProjectInfoSidebarContainer>
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
|
@ -20,6 +20,8 @@ import {
|
|||||||
} from './ProjectFeatureToggles/PaginatedProjectFeatureToggles';
|
} from './ProjectFeatureToggles/PaginatedProjectFeatureToggles';
|
||||||
|
|
||||||
import { useTableState } from 'hooks/useTableState';
|
import { useTableState } from 'hooks/useTableState';
|
||||||
|
import useProjectOverview from 'hooks/api/getters/useProjectOverview/useProjectOverview';
|
||||||
|
import { FeatureTypeCount } from '../../../interfaces/project';
|
||||||
|
|
||||||
const refreshInterval = 15 * 1000;
|
const refreshInterval = 15 * 1000;
|
||||||
|
|
||||||
@ -49,7 +51,7 @@ const PaginatedProjectOverview: FC<{
|
|||||||
storageKey?: string;
|
storageKey?: string;
|
||||||
}> = ({ fullWidth, storageKey = 'project-overview' }) => {
|
}> = ({ fullWidth, storageKey = 'project-overview' }) => {
|
||||||
const projectId = useRequiredPathParam('projectId');
|
const projectId = useRequiredPathParam('projectId');
|
||||||
const { project, loading: projectLoading } = useProject(projectId, {
|
const { project } = useProjectOverview(projectId, {
|
||||||
refreshInterval,
|
refreshInterval,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -84,8 +86,14 @@ const PaginatedProjectOverview: FC<{
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const { members, features, health, description, environments, stats } =
|
const {
|
||||||
project;
|
members,
|
||||||
|
featureTypeCounts,
|
||||||
|
health,
|
||||||
|
description,
|
||||||
|
environments,
|
||||||
|
stats,
|
||||||
|
} = project;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer key={projectId}>
|
<StyledContainer key={projectId}>
|
||||||
@ -94,7 +102,7 @@ const PaginatedProjectOverview: FC<{
|
|||||||
description={description}
|
description={description}
|
||||||
memberCount={members}
|
memberCount={members}
|
||||||
health={health}
|
health={health}
|
||||||
features={features}
|
featureTypeCounts={featureTypeCounts}
|
||||||
stats={stats}
|
stats={stats}
|
||||||
/>
|
/>
|
||||||
<StyledContentContainer>
|
<StyledContentContainer>
|
||||||
@ -140,6 +148,21 @@ const ProjectOverview = () => {
|
|||||||
|
|
||||||
if (featureSearchFrontend) return <PaginatedProjectOverview />;
|
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 (
|
return (
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
<ProjectInfo
|
<ProjectInfo
|
||||||
@ -147,7 +170,7 @@ const ProjectOverview = () => {
|
|||||||
description={description}
|
description={description}
|
||||||
memberCount={members}
|
memberCount={members}
|
||||||
health={health}
|
health={health}
|
||||||
features={features}
|
featureTypeCounts={featureTypeCounts}
|
||||||
stats={stats}
|
stats={stats}
|
||||||
/>
|
/>
|
||||||
<StyledContentContainer>
|
<StyledContentContainer>
|
||||||
|
@ -24,10 +24,11 @@ import { ActionCell } from 'component/common/Table/cells/ActionCell/ActionCell';
|
|||||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||||
import useProjectApiTokensApi from 'hooks/api/actions/useProjectApiTokensApi/useProjectApiTokensApi';
|
import useProjectApiTokensApi from 'hooks/api/actions/useProjectApiTokensApi/useProjectApiTokensApi';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { useProjectOverviewNameOrId } from 'hooks/api/getters/useProjectOverview/useProjectOverview';
|
||||||
|
|
||||||
export const ProjectApiAccess = () => {
|
export const ProjectApiAccess = () => {
|
||||||
const projectId = useRequiredPathParam('projectId');
|
const projectId = useRequiredPathParam('projectId');
|
||||||
const projectName = useProjectNameOrId(projectId);
|
const projectName = useProjectOverviewNameOrId(projectId);
|
||||||
const { hasAccess } = useContext(AccessContext);
|
const { hasAccess } = useContext(AccessContext);
|
||||||
const {
|
const {
|
||||||
tokens,
|
tokens,
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
import React, { useContext } from 'react';
|
import React, { useContext } from 'react';
|
||||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
import useProject, {
|
|
||||||
useProjectNameOrId,
|
|
||||||
} from 'hooks/api/getters/useProject/useProject';
|
|
||||||
import AccessContext from 'contexts/AccessContext';
|
import AccessContext from 'contexts/AccessContext';
|
||||||
import { usePageTitle } from 'hooks/usePageTitle';
|
import { usePageTitle } from 'hooks/usePageTitle';
|
||||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
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 { Alert, styled } from '@mui/material';
|
||||||
import ProjectEnvironment from './ProjectEnvironment/ProjectEnvironment';
|
import ProjectEnvironment from './ProjectEnvironment/ProjectEnvironment';
|
||||||
import { Route, Routes, useNavigate } from 'react-router-dom';
|
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 EditDefaultStrategy from './ProjectEnvironment/ProjectEnvironmentDefaultStrategy/EditDefaultStrategy';
|
||||||
|
import useProjectOverview, {
|
||||||
|
useProjectOverviewNameOrId,
|
||||||
|
} from 'hooks/api/getters/useProjectOverview/useProjectOverview';
|
||||||
|
|
||||||
const StyledAlert = styled(Alert)(({ theme }) => ({
|
const StyledAlert = styled(Alert)(({ theme }) => ({
|
||||||
marginBottom: theme.spacing(4),
|
marginBottom: theme.spacing(4),
|
||||||
}));
|
}));
|
||||||
export const ProjectDefaultStrategySettings = () => {
|
export const ProjectDefaultStrategySettings = () => {
|
||||||
const projectId = useRequiredPathParam('projectId');
|
const projectId = useRequiredPathParam('projectId');
|
||||||
const projectName = useProjectNameOrId(projectId);
|
const projectName = useProjectOverviewNameOrId(projectId);
|
||||||
const { hasAccess } = useContext(AccessContext);
|
const { hasAccess } = useContext(AccessContext);
|
||||||
const { project } = useProject(projectId);
|
const { project } = useProjectOverview(projectId);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
usePageTitle(`Project default strategy configuration – ${projectName}`);
|
usePageTitle(`Project default strategy configuration – ${projectName}`);
|
||||||
|
|
||||||
|
@ -6,14 +6,14 @@ import AccessContext from 'contexts/AccessContext';
|
|||||||
import { UPDATE_PROJECT } from 'component/providers/AccessProvider/permissions';
|
import { UPDATE_PROJECT } from 'component/providers/AccessProvider/permissions';
|
||||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
import { usePageTitle } from 'hooks/usePageTitle';
|
import { usePageTitle } from 'hooks/usePageTitle';
|
||||||
import { useProjectNameOrId } from 'hooks/api/getters/useProject/useProject';
|
|
||||||
import EditProject from './EditProject/EditProject';
|
import EditProject from './EditProject/EditProject';
|
||||||
import { PremiumFeature } from 'component/common/PremiumFeature/PremiumFeature';
|
import { PremiumFeature } from 'component/common/PremiumFeature/PremiumFeature';
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
import { useProjectOverviewNameOrId } from 'hooks/api/getters/useProjectOverview/useProjectOverview';
|
||||||
|
|
||||||
export const Settings = () => {
|
export const Settings = () => {
|
||||||
const projectId = useRequiredPathParam('projectId');
|
const projectId = useRequiredPathParam('projectId');
|
||||||
const projectName = useProjectNameOrId(projectId);
|
const projectName = useProjectOverviewNameOrId(projectId);
|
||||||
const { hasAccess } = useContext(AccessContext);
|
const { hasAccess } = useContext(AccessContext);
|
||||||
const { isOss } = useUiConfig();
|
const { isOss } = useUiConfig();
|
||||||
usePageTitle(`Project configuration – ${projectName}`);
|
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 useProject = (id: string, options: SWRConfiguration = {}) => {
|
||||||
const { KEY, fetcher } = getProjectFetcher(id);
|
const { KEY, fetcher } = getProjectFetcher(id);
|
||||||
const { data, error, mutate } = useSWR<IProject>(KEY, fetcher, options);
|
const { data, error, mutate } = useSWR<IProject>(KEY, fetcher, options);
|
||||||
@ -41,7 +45,10 @@ const useProject = (id: string, options: SWRConfiguration = {}) => {
|
|||||||
refetch,
|
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 => {
|
export const useProjectNameOrId = (id: string): string => {
|
||||||
return useProject(id).project.name || id;
|
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;
|
description: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type FeatureTypeCount = {
|
||||||
|
type: string;
|
||||||
|
count: number;
|
||||||
|
};
|
||||||
|
|
||||||
export interface IProject {
|
export interface IProject {
|
||||||
id?: string;
|
id?: string;
|
||||||
members: number;
|
members: number;
|
||||||
@ -38,6 +43,23 @@ export interface IProject {
|
|||||||
featureNaming?: FeatureNamingType;
|
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 {
|
export interface IProjectHealthReport extends IProject {
|
||||||
staleCount: number;
|
staleCount: number;
|
||||||
potentiallyStaleCount: number;
|
potentiallyStaleCount: number;
|
||||||
|
Loading…
Reference in New Issue
Block a user