mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: Fetch backend api data insights (#6622)
This commit is contained in:
		
							parent
							
								
									ae921aed69
								
							
						
					
					
						commit
						f0e5d075a7
					
				| @ -22,11 +22,23 @@ const setupOssApi = () => { | ||||
|     }); | ||||
| }; | ||||
| 
 | ||||
| const changeRequests = { | ||||
|     applied: 0, | ||||
|     total: 0, | ||||
|     approved: 0, | ||||
|     scheduled: 0, | ||||
|     reviewRequired: 0, | ||||
|     rejected: 0, | ||||
| }; | ||||
| 
 | ||||
| test('Show enterprise hints', async () => { | ||||
|     setupOssApi(); | ||||
|     render( | ||||
|         <Routes> | ||||
|             <Route path={'/projects/:projectId'} element={<ChangeRequests />} /> | ||||
|             <Route | ||||
|                 path={'/projects/:projectId'} | ||||
|                 element={<ChangeRequests changeRequests={changeRequests} />} | ||||
|             /> | ||||
|         </Routes>, | ||||
|         { | ||||
|             route: '/projects/default', | ||||
| @ -40,7 +52,10 @@ test('Show change requests info', async () => { | ||||
|     setupEnterpriseApi(); | ||||
|     render( | ||||
|         <Routes> | ||||
|             <Route path={'/projects/:projectId'} element={<ChangeRequests />} /> | ||||
|             <Route | ||||
|                 path={'/projects/:projectId'} | ||||
|                 element={<ChangeRequests changeRequests={changeRequests} />} | ||||
|             /> | ||||
|         </Routes>, | ||||
|         { | ||||
|             route: '/projects/default', | ||||
|  | ||||
| @ -4,6 +4,8 @@ import { Link } from 'react-router-dom'; | ||||
| import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | ||||
| import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; | ||||
| import { PremiumFeature } from 'component/common/PremiumFeature/PremiumFeature'; | ||||
| import type { ProjectInsightsSchemaChangeRequests } from '../../../../../openapi'; | ||||
| import type { FC } from 'react'; | ||||
| 
 | ||||
| const Container = styled(Box)(({ theme }) => ({ | ||||
|     display: 'flex', | ||||
| @ -81,15 +83,15 @@ const BigNumber = styled(Typography)(({ theme }) => ({ | ||||
|     color: theme.palette.text.primary, | ||||
| })); | ||||
| 
 | ||||
| export const ChangeRequests = () => { | ||||
| export const ChangeRequests: FC<{ | ||||
|     changeRequests: ProjectInsightsSchemaChangeRequests; | ||||
| }> = ({ changeRequests }) => { | ||||
|     const projectId = useRequiredPathParam('projectId'); | ||||
|     const { isOss, isPro } = useUiConfig(); | ||||
| 
 | ||||
|     const toBeApplied = 12; | ||||
|     const toBeReviewed = 3; | ||||
|     const total = 32; | ||||
|     const applied = 28; | ||||
|     const rejected = 4; | ||||
|     const { total, applied, rejected, reviewRequired, scheduled, approved } = | ||||
|         changeRequests; | ||||
|     const toBeApplied = scheduled + approved; | ||||
| 
 | ||||
|     if (isOss() || isPro()) { | ||||
|         return ( | ||||
| @ -109,7 +111,7 @@ export const ChangeRequests = () => { | ||||
|                 <KeyboardArrowRight /> | ||||
|             </ChangeRequestNavigation> | ||||
| 
 | ||||
|             <BoxesContainer> | ||||
|             <BoxesContainer data-loading> | ||||
|                 <OpenBox> | ||||
|                     <ChangeRequestNavigation | ||||
|                         to={`/projects/${projectId}/change-requests`} | ||||
| @ -123,7 +125,7 @@ export const ChangeRequests = () => { | ||||
|                     </ApplyBox> | ||||
|                     <ReviewBox> | ||||
|                         <span>To be reviewed</span> | ||||
|                         <MediumNumber>{toBeReviewed}</MediumNumber> | ||||
|                         <MediumNumber>{reviewRequired}</MediumNumber> | ||||
|                     </ReviewBox> | ||||
|                 </OpenBox> | ||||
|                 <NumberBox> | ||||
|  | ||||
| @ -24,7 +24,14 @@ test('Show outdated SDKs and apps using them', async () => { | ||||
|     }); | ||||
|     render( | ||||
|         <Routes> | ||||
|             <Route path={'/projects/:projectId'} element={<FlagTypesUsed />} /> | ||||
|             <Route | ||||
|                 path={'/projects/:projectId'} | ||||
|                 element={ | ||||
|                     <FlagTypesUsed | ||||
|                         featureTypeCounts={[{ type: 'release', count: 57 }]} | ||||
|                     /> | ||||
|                 } | ||||
|             /> | ||||
|         </Routes>, | ||||
|         { | ||||
|             route: '/projects/default', | ||||
|  | ||||
| @ -1,10 +1,9 @@ | ||||
| import { useMemo } from 'react'; | ||||
| import { type FC, useMemo } from 'react'; | ||||
| import { styled, type SvgIconTypeMap, Typography } from '@mui/material'; | ||||
| import { getFeatureTypeIcons } from 'utils/getFeatureTypeIcons'; | ||||
| 
 | ||||
| import type { OverridableComponent } from '@mui/material/OverridableComponent'; | ||||
| import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | ||||
| import useProjectOverview from 'hooks/api/getters/useProjectOverview/useProjectOverview'; | ||||
| import type { FeatureTypeCountSchema } from '../../../../../openapi'; | ||||
| 
 | ||||
| export const StyledProjectInfoWidgetContainer = styled('div')(({ theme }) => ({ | ||||
|     margin: '0', | ||||
| @ -64,12 +63,9 @@ const FlagTypesRow = ({ type, Icon, count }: IFlagTypeRowProps) => { | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| export const FlagTypesUsed = () => { | ||||
|     const projectId = useRequiredPathParam('projectId'); | ||||
|     const { project } = useProjectOverview(projectId); | ||||
| 
 | ||||
|     const { featureTypeCounts } = project; | ||||
| 
 | ||||
| export const FlagTypesUsed: FC<{ | ||||
|     featureTypeCounts: FeatureTypeCountSchema[]; | ||||
| }> = ({ featureTypeCounts }) => { | ||||
|     const featureTypeStats = useMemo(() => { | ||||
|         const release = | ||||
|             featureTypeCounts.find( | ||||
|  | ||||
| @ -2,6 +2,8 @@ import { ProjectHealthChart } from './ProjectHealthChart'; | ||||
| import { Alert, Box, styled, Typography, useTheme } from '@mui/material'; | ||||
| import { Link } from 'react-router-dom'; | ||||
| import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | ||||
| import type { ProjectInsightsSchemaHealth } from '../../../../../openapi'; | ||||
| import type { FC } from 'react'; | ||||
| 
 | ||||
| const Dot = styled('span', { | ||||
|     shouldForwardProp: (prop) => prop !== 'color', | ||||
| @ -36,13 +38,12 @@ const StatusWithDot = styled(Box)(({ theme }) => ({ | ||||
|     gap: theme.spacing(1), | ||||
| })); | ||||
| 
 | ||||
| export const ProjectHealth = () => { | ||||
| export const ProjectHealth: FC<{ health: ProjectInsightsSchemaHealth }> = ({ | ||||
|     health, | ||||
| }) => { | ||||
|     const theme = useTheme(); | ||||
|     const projectId = useRequiredPathParam('projectId'); | ||||
|     const active = 15; | ||||
|     const stale = 10; | ||||
|     const potentiallyStale = 3; | ||||
|     const health = 93; | ||||
|     const { staleCount, potentiallyStaleCount, activeCount, rating } = health; | ||||
| 
 | ||||
|     return ( | ||||
|         <Container> | ||||
| @ -52,6 +53,7 @@ export const ProjectHealth = () => { | ||||
|                 flags | ||||
|             </Alert> | ||||
|             <Box | ||||
|                 data-loading | ||||
|                 sx={(theme) => ({ | ||||
|                     display: 'flex', | ||||
|                     gap: theme.spacing(4), | ||||
| @ -59,10 +61,10 @@ export const ProjectHealth = () => { | ||||
|                 })} | ||||
|             > | ||||
|                 <ProjectHealthChart | ||||
|                     active={active} | ||||
|                     stale={stale} | ||||
|                     potentiallyStale={potentiallyStale} | ||||
|                     health={health} | ||||
|                     active={activeCount} | ||||
|                     stale={staleCount} | ||||
|                     potentiallyStale={potentiallyStaleCount} | ||||
|                     health={rating} | ||||
|                 /> | ||||
|                 <FlagCounts> | ||||
|                     <Box> | ||||
| @ -70,7 +72,7 @@ export const ProjectHealth = () => { | ||||
|                             <Dot color={theme.palette.success.border} /> | ||||
|                             <Box sx={{ fontWeight: 'bold' }}>Active</Box> | ||||
|                         </StatusWithDot> | ||||
|                         <FlagsCount>{active} feature flags</FlagsCount> | ||||
|                         <FlagsCount>{activeCount} feature flags</FlagsCount> | ||||
|                     </Box> | ||||
|                     <Box> | ||||
|                         <StatusWithDot> | ||||
| @ -81,7 +83,7 @@ export const ProjectHealth = () => { | ||||
|                             <Link to='/feature-toggle-type'>(configure)</Link> | ||||
|                         </StatusWithDot> | ||||
|                         <FlagsCount> | ||||
|                             {potentiallyStale} feature flags | ||||
|                             {potentiallyStaleCount} feature flags | ||||
|                         </FlagsCount> | ||||
|                     </Box> | ||||
|                     <Box> | ||||
| @ -92,7 +94,7 @@ export const ProjectHealth = () => { | ||||
|                                 (view flags) | ||||
|                             </Link> | ||||
|                         </StatusWithDot> | ||||
|                         <FlagsCount>{stale} feature flags</FlagsCount> | ||||
|                         <FlagsCount>{staleCount} feature flags</FlagsCount> | ||||
|                     </Box> | ||||
|                 </FlagCounts> | ||||
|             </Box> | ||||
|  | ||||
| @ -4,6 +4,9 @@ import { LeadTimeForChanges } from './LeadTimeForChanges/LeadTimeForChanges'; | ||||
| import { ProjectHealth } from './ProjectHealth/ProjectHealth'; | ||||
| import { FlagTypesUsed } from './FlagTypesUsed/FlagTypesUsed'; | ||||
| import { ProjectInsightsStats } from './ProjectInsightsStats/ProjectInsightsStats'; | ||||
| import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | ||||
| import { useProjectInsights } from 'hooks/api/getters/useProjectInsights/useProjectInsights'; | ||||
| import useLoading from 'hooks/useLoading'; | ||||
| 
 | ||||
| const Container = styled(Box)(({ theme }) => ({ | ||||
|     backgroundColor: theme.palette.background.paper, | ||||
| @ -33,37 +36,31 @@ const NarrowContainer = styled(Container)(() => ({ | ||||
|     gridColumn: 'span 2', | ||||
| })); | ||||
| 
 | ||||
| const statsData = { | ||||
|     stats: { | ||||
|         archivedCurrentWindow: 5, | ||||
|         archivedPastWindow: 3, | ||||
|         avgTimeToProdCurrentWindow: 2.5, | ||||
|         createdCurrentWindow: 7, | ||||
|         createdPastWindow: 4, | ||||
|         projectActivityCurrentWindow: 10, | ||||
|         projectActivityPastWindow: 8, | ||||
|         projectMembersAddedCurrentWindow: 2, | ||||
|     }, | ||||
| }; | ||||
| 
 | ||||
| export const ProjectInsights = () => { | ||||
|     const projectId = useRequiredPathParam('projectId'); | ||||
|     const { data, loading } = useProjectInsights(projectId); | ||||
| 
 | ||||
|     const ref = useLoading(loading); | ||||
| 
 | ||||
|     return ( | ||||
|         <Grid> | ||||
|         <Grid ref={ref}> | ||||
|             <FullWidthContainer> | ||||
|                 <ProjectInsightsStats {...statsData} /> | ||||
|                 <ProjectInsightsStats stats={data.stats} /> | ||||
|             </FullWidthContainer> | ||||
|             <MediumWideContainer> | ||||
|                 <ProjectHealth /> | ||||
|                 <ProjectHealth health={data.health} /> | ||||
|             </MediumWideContainer> | ||||
|             <WideContainer> | ||||
|                 <LeadTimeForChanges /> | ||||
|             </WideContainer> | ||||
|             <NarrowContainer> | ||||
|                 <FlagTypesUsed /> | ||||
|                 <FlagTypesUsed featureTypeCounts={data.featureTypeCounts} /> | ||||
|             </NarrowContainer> | ||||
|             <NarrowContainer>Project members</NarrowContainer> | ||||
|             <WideContainer> | ||||
|                 <ChangeRequests /> | ||||
|                 {data.changeRequests && ( | ||||
|                     <ChangeRequests changeRequests={data.changeRequests} /> | ||||
|                 )} | ||||
|             </WideContainer> | ||||
|         </Grid> | ||||
|     ); | ||||
|  | ||||
| @ -0,0 +1,71 @@ | ||||
| import { fetcher, useApiGetter } from '../useApiGetter/useApiGetter'; | ||||
| import type { ProjectInsightsSchema } from '../../../../openapi'; | ||||
| import { formatApiPath } from 'utils/formatPath'; | ||||
| 
 | ||||
| const path = (projectId: string) => `api/admin/projects/${projectId}/insights`; | ||||
| 
 | ||||
| const placeholderData: ProjectInsightsSchema = { | ||||
|     stats: { | ||||
|         avgTimeToProdCurrentWindow: 0, | ||||
|         createdCurrentWindow: 0, | ||||
|         createdPastWindow: 0, | ||||
|         archivedCurrentWindow: 0, | ||||
|         archivedPastWindow: 0, | ||||
|         projectActivityCurrentWindow: 0, | ||||
|         projectActivityPastWindow: 0, | ||||
|         projectMembersAddedCurrentWindow: 0, | ||||
|     }, | ||||
|     featureTypeCounts: [ | ||||
|         { | ||||
|             type: 'experiment', | ||||
|             count: 0, | ||||
|         }, | ||||
|         { | ||||
|             type: 'permission', | ||||
|             count: 0, | ||||
|         }, | ||||
|         { | ||||
|             type: 'release', | ||||
|             count: 0, | ||||
|         }, | ||||
|     ], | ||||
|     leadTime: { | ||||
|         projectAverage: 0, | ||||
|         features: [ | ||||
|             { name: 'feature1', timeToProduction: 0 }, | ||||
|             { name: 'feature2', timeToProduction: 0 }, | ||||
|             { name: 'feature3', timeToProduction: 0 }, | ||||
|             { name: 'feature4', timeToProduction: 0 }, | ||||
|             { name: 'feature5', timeToProduction: 2 }, | ||||
|         ], | ||||
|     }, | ||||
|     health: { | ||||
|         rating: 0, | ||||
|         activeCount: 0, | ||||
|         potentiallyStaleCount: 0, | ||||
|         staleCount: 0, | ||||
|     }, | ||||
|     members: { | ||||
|         active: 0, | ||||
|         inactive: 0, | ||||
|         totalPreviousMonth: 0, | ||||
|     }, | ||||
|     changeRequests: { | ||||
|         total: 0, | ||||
|         applied: 0, | ||||
|         approved: 0, | ||||
|         rejected: 0, | ||||
|         reviewRequired: 0, | ||||
|         scheduled: 0, | ||||
|     }, | ||||
| }; | ||||
| 
 | ||||
| export const useProjectInsights = (projectId: string) => { | ||||
|     const projectPath = formatApiPath(path(projectId)); | ||||
|     const { data, refetch, loading, error } = | ||||
|         useApiGetter<ProjectInsightsSchema>(projectPath, () => | ||||
|             fetcher(projectPath, 'Outdated SDKs'), | ||||
|         ); | ||||
| 
 | ||||
|     return { data: data || placeholderData, refetch, loading, error }; | ||||
| }; | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user