mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: flag types used for project insights (#6607)

This commit is contained in:
		
							parent
							
								
									84005e27cc
								
							
						
					
					
						commit
						407b348a45
					
				| @ -35,6 +35,9 @@ const resolveDoraMetrics = (input: number) => { | |||||||
|     } |     } | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | /** | ||||||
|  |  * @Deprecated in favor of LeadTimeForChanges.tsx | ||||||
|  |  */ | ||||||
| export const ProjectDoraMetrics = () => { | export const ProjectDoraMetrics = () => { | ||||||
|     const projectId = useRequiredPathParam('projectId'); |     const projectId = useRequiredPathParam('projectId'); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -52,7 +52,9 @@ const ToggleTypesRow = ({ type, Icon, count }: IToggleTypeRowProps) => { | |||||||
|         </StyledParagraphGridRow> |         </StyledParagraphGridRow> | ||||||
|     ); |     ); | ||||||
| }; | }; | ||||||
| 
 | /** | ||||||
|  |  * @Deprecated in favor of FlagTypesUsed.tsx | ||||||
|  |  */ | ||||||
| export const FlagTypesWidget = ({ | export const FlagTypesWidget = ({ | ||||||
|     featureTypeCounts, |     featureTypeCounts, | ||||||
| }: IFlagTypesWidgetProps) => { | }: IFlagTypesWidgetProps) => { | ||||||
|  | |||||||
| @ -0,0 +1,36 @@ | |||||||
|  | import { screen } from '@testing-library/react'; | ||||||
|  | import { render } from 'utils/testRenderer'; | ||||||
|  | import { testServerRoute, testServerSetup } from 'utils/testServer'; | ||||||
|  | import type { ProjectOverviewSchema } from 'openapi'; | ||||||
|  | import { Route, Routes } from 'react-router-dom'; | ||||||
|  | import { FlagTypesUsed } from './FlagTypesUsed'; | ||||||
|  | 
 | ||||||
|  | const server = testServerSetup(); | ||||||
|  | 
 | ||||||
|  | const setupApi = (overview: ProjectOverviewSchema) => { | ||||||
|  |     testServerRoute(server, '/api/admin/projects/default/overview', overview); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | test('Show outdated SDKs and apps using them', async () => { | ||||||
|  |     setupApi({ | ||||||
|  |         name: 'default', | ||||||
|  |         version: 2, | ||||||
|  |         featureTypeCounts: [ | ||||||
|  |             { | ||||||
|  |                 type: 'release', | ||||||
|  |                 count: 57, | ||||||
|  |             }, | ||||||
|  |         ], | ||||||
|  |     }); | ||||||
|  |     render( | ||||||
|  |         <Routes> | ||||||
|  |             <Route path={'/projects/:projectId'} element={<FlagTypesUsed />} /> | ||||||
|  |         </Routes>, | ||||||
|  |         { | ||||||
|  |             route: '/projects/default', | ||||||
|  |         }, | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     await screen.findByText('Release'); | ||||||
|  |     await screen.findByText('57'); | ||||||
|  | }); | ||||||
| @ -0,0 +1,125 @@ | |||||||
|  | import { 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'; | ||||||
|  | 
 | ||||||
|  | export const StyledProjectInfoWidgetContainer = styled('div')(({ theme }) => ({ | ||||||
|  |     margin: '0', | ||||||
|  |     [theme.breakpoints.down('md')]: { | ||||||
|  |         display: 'flex', | ||||||
|  |         flexDirection: 'column', | ||||||
|  |         position: 'relative', | ||||||
|  |     }, | ||||||
|  | })); | ||||||
|  | 
 | ||||||
|  | export const StyledWidgetTitle = styled(Typography)(({ theme }) => ({ | ||||||
|  |     marginBottom: theme.spacing(2.5), | ||||||
|  | })); | ||||||
|  | 
 | ||||||
|  | export const StyledCount = styled('span')(({ theme }) => ({ | ||||||
|  |     fontSize: theme.typography.h2.fontSize, | ||||||
|  |     fontWeight: 'bold', | ||||||
|  |     color: theme.palette.text.primary, | ||||||
|  | })); | ||||||
|  | 
 | ||||||
|  | const StyledTypeCount = styled(StyledCount)(({ theme }) => ({ | ||||||
|  |     marginLeft: 'auto', | ||||||
|  |     fontWeight: theme.typography.fontWeightRegular, | ||||||
|  |     color: theme.palette.text.secondary, | ||||||
|  | })); | ||||||
|  | 
 | ||||||
|  | interface IFlagTypeRowProps { | ||||||
|  |     type: string; | ||||||
|  |     Icon: OverridableComponent<SvgIconTypeMap>; | ||||||
|  |     count: number; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const StyledParagraphGridRow = styled('div')(({ theme }) => ({ | ||||||
|  |     display: 'flex', | ||||||
|  |     gap: theme.spacing(1.5), | ||||||
|  |     width: '100%', | ||||||
|  |     margin: theme.spacing(1, 0), | ||||||
|  |     fontSize: theme.fontSizes.smallBody, | ||||||
|  |     color: theme.palette.text.secondary, | ||||||
|  |     alignItems: 'center', | ||||||
|  |     [theme.breakpoints.down('md')]: { | ||||||
|  |         margin: 0, | ||||||
|  |     }, | ||||||
|  | })); | ||||||
|  | 
 | ||||||
|  | const FlagTypesRow = ({ type, Icon, count }: IFlagTypeRowProps) => { | ||||||
|  |     const getTitleText = (str: string) => { | ||||||
|  |         return str.charAt(0).toUpperCase() + str.slice(1).replace('-', ' '); | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     return ( | ||||||
|  |         <StyledParagraphGridRow data-loading> | ||||||
|  |             <Icon fontSize='small' data-loading /> | ||||||
|  |             <div>{getTitleText(type)}</div> | ||||||
|  |             <StyledTypeCount>{count}</StyledTypeCount> | ||||||
|  |         </StyledParagraphGridRow> | ||||||
|  |     ); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const FlagTypesUsed = () => { | ||||||
|  |     const projectId = useRequiredPathParam('projectId'); | ||||||
|  |     const { project } = useProjectOverview(projectId); | ||||||
|  | 
 | ||||||
|  |     const { featureTypeCounts } = project; | ||||||
|  | 
 | ||||||
|  |     const featureTypeStats = useMemo(() => { | ||||||
|  |         const release = | ||||||
|  |             featureTypeCounts.find( | ||||||
|  |                 (featureType) => featureType.type === 'release', | ||||||
|  |             )?.count || 0; | ||||||
|  | 
 | ||||||
|  |         const experiment = | ||||||
|  |             featureTypeCounts.find( | ||||||
|  |                 (featureType) => featureType.type === 'experiment', | ||||||
|  |             )?.count || 0; | ||||||
|  | 
 | ||||||
|  |         const operational = | ||||||
|  |             featureTypeCounts.find( | ||||||
|  |                 (featureType) => featureType.type === 'operational', | ||||||
|  |             )?.count || 0; | ||||||
|  | 
 | ||||||
|  |         const kill = | ||||||
|  |             featureTypeCounts.find( | ||||||
|  |                 (featureType) => featureType.type === 'kill-switch', | ||||||
|  |             )?.count || 0; | ||||||
|  | 
 | ||||||
|  |         const permission = | ||||||
|  |             featureTypeCounts.find( | ||||||
|  |                 (featureType) => featureType.type === 'permission', | ||||||
|  |             )?.count || 0; | ||||||
|  | 
 | ||||||
|  |         return { | ||||||
|  |             release, | ||||||
|  |             experiment, | ||||||
|  |             operational, | ||||||
|  |             'kill-switch': kill, | ||||||
|  |             permission, | ||||||
|  |         }; | ||||||
|  |     }, [featureTypeCounts]); | ||||||
|  | 
 | ||||||
|  |     return ( | ||||||
|  |         <StyledProjectInfoWidgetContainer> | ||||||
|  |             <StyledWidgetTitle variant='h3' data-loading> | ||||||
|  |                 Flag types used | ||||||
|  |             </StyledWidgetTitle> | ||||||
|  |             {Object.keys(featureTypeStats).map((type) => ( | ||||||
|  |                 <FlagTypesRow | ||||||
|  |                     type={type} | ||||||
|  |                     key={type} | ||||||
|  |                     Icon={getFeatureTypeIcons(type)} | ||||||
|  |                     count={ | ||||||
|  |                         featureTypeStats[type as keyof typeof featureTypeStats] | ||||||
|  |                     } | ||||||
|  |                 /> | ||||||
|  |             ))} | ||||||
|  |         </StyledProjectInfoWidgetContainer> | ||||||
|  |     ); | ||||||
|  | }; | ||||||
| @ -33,7 +33,7 @@ test('Show outdated SDKs and apps using them', async () => { | |||||||
|         }, |         }, | ||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|     await screen.findByText('Lead time for changes (per release toggle)'); |     await screen.findByText('Lead time for changes (per release flag)'); | ||||||
|     await screen.findByText('ABCD'); |     await screen.findByText('ABCD'); | ||||||
|     await screen.findByText('57 days'); |     await screen.findByText('57 days'); | ||||||
|     await screen.findByText('Low'); |     await screen.findByText('Low'); | ||||||
|  | |||||||
| @ -17,15 +17,11 @@ import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColum | |||||||
| import theme from 'themes/theme'; | import theme from 'themes/theme'; | ||||||
| 
 | 
 | ||||||
| const Container = styled(Box)(({ theme }) => ({ | const Container = styled(Box)(({ theme }) => ({ | ||||||
|     gridColumn: 'span 6', |  | ||||||
|     backgroundColor: theme.palette.background.paper, |  | ||||||
|     padding: theme.spacing(3), |  | ||||||
|     display: 'flex', |     display: 'flex', | ||||||
|     flexDirection: 'column', |     flexDirection: 'column', | ||||||
|     gap: theme.spacing(2), |     gap: theme.spacing(2), | ||||||
|     borderRadius: theme.shape.borderRadiusLarge, |  | ||||||
|     overflowY: 'auto', |     overflowY: 'auto', | ||||||
|     maxHeight: theme.spacing(100), |     maxHeight: theme.spacing(45), | ||||||
| })); | })); | ||||||
| 
 | 
 | ||||||
| const resolveDoraMetrics = (input: number) => { | const resolveDoraMetrics = (input: number) => { | ||||||
| @ -96,7 +92,7 @@ export const LeadTimeForChanges = () => { | |||||||
|                 align: 'center', |                 align: 'center', | ||||||
|                 Cell: ({ row: { original } }: any) => ( |                 Cell: ({ row: { original } }: any) => ( | ||||||
|                     <Tooltip |                     <Tooltip | ||||||
|                         title='The time from the feature toggle of type release was created until it was turned on in a production environment' |                         title='The time from the feature flag of type release was created until it was turned on in a production environment' | ||||||
|                         arrow |                         arrow | ||||||
|                     > |                     > | ||||||
|                         <Box |                         <Box | ||||||
| @ -110,7 +106,7 @@ export const LeadTimeForChanges = () => { | |||||||
|                         </Box> |                         </Box> | ||||||
|                     </Tooltip> |                     </Tooltip> | ||||||
|                 ), |                 ), | ||||||
|                 width: 200, |                 width: 220, | ||||||
|                 disableGlobalFilter: true, |                 disableGlobalFilter: true, | ||||||
|                 disableSortBy: true, |                 disableSortBy: true, | ||||||
|             }, |             }, | ||||||
| @ -222,7 +218,7 @@ export const LeadTimeForChanges = () => { | |||||||
|     return ( |     return ( | ||||||
|         <Container> |         <Container> | ||||||
|             <Typography variant='h3'> |             <Typography variant='h3'> | ||||||
|                 Lead time for changes (per release toggle) |                 Lead time for changes (per release flag) | ||||||
|             </Typography> |             </Typography> | ||||||
|             <Table {...getTableProps()}> |             <Table {...getTableProps()}> | ||||||
|                 <SortableTableHeader headerGroups={headerGroups} /> |                 <SortableTableHeader headerGroups={headerGroups} /> | ||||||
|  | |||||||
| @ -2,6 +2,7 @@ import { Box, styled } from '@mui/material'; | |||||||
| import { ChangeRequests } from './ChangeRequests/ChangeRequests'; | import { ChangeRequests } from './ChangeRequests/ChangeRequests'; | ||||||
| import { LeadTimeForChanges } from './LeadTimeForChanges/LeadTimeForChanges'; | import { LeadTimeForChanges } from './LeadTimeForChanges/LeadTimeForChanges'; | ||||||
| import { ProjectHealth } from './ProjectHealth/ProjectHealth'; | import { ProjectHealth } from './ProjectHealth/ProjectHealth'; | ||||||
|  | import { FlagTypesUsed } from './FlagTypesUsed/FlagTypesUsed'; | ||||||
| 
 | 
 | ||||||
| const Container = styled(Box)(({ theme }) => ({ | const Container = styled(Box)(({ theme }) => ({ | ||||||
|     backgroundColor: theme.palette.background.paper, |     backgroundColor: theme.palette.background.paper, | ||||||
| @ -15,42 +16,42 @@ const Grid = styled(Box)(({ theme }) => ({ | |||||||
|     gridTemplateColumns: 'repeat(10, 1fr)', |     gridTemplateColumns: 'repeat(10, 1fr)', | ||||||
| })); | })); | ||||||
| 
 | 
 | ||||||
| const Overview = styled(Box)(({ theme }) => ({ | const FullWidthContainer = styled(Container)(() => ({ | ||||||
|     gridColumn: '1 / -1', |     gridColumn: '1 / -1', | ||||||
| })); | })); | ||||||
| 
 | 
 | ||||||
| const HealthCard = styled(Container)(({ theme }) => ({ | const WideContainer = styled(Container)(() => ({ | ||||||
|  |     gridColumn: 'span 6', | ||||||
|  | })); | ||||||
|  | 
 | ||||||
|  | const MediumWideContainer = styled(Container)(() => ({ | ||||||
|     gridColumn: 'span 4', |     gridColumn: 'span 4', | ||||||
| })); | })); | ||||||
| 
 | 
 | ||||||
| const ToggleTypesUsedCard = styled(Container)(({ theme }) => ({ | const NarrowContainer = styled(Container)(() => ({ | ||||||
|     gridColumn: 'span 2', |     gridColumn: 'span 2', | ||||||
| })); | })); | ||||||
| 
 | 
 | ||||||
| const ProjectMembersCard = styled(Container)(({ theme }) => ({ |  | ||||||
|     gridColumn: 'span 2', |  | ||||||
| })); |  | ||||||
| 
 |  | ||||||
| const ChangeRequestsCard = styled(Container)(({ theme }) => ({ |  | ||||||
|     gridColumn: 'span 6', |  | ||||||
| })); |  | ||||||
| 
 |  | ||||||
| export const ProjectInsights = () => { | export const ProjectInsights = () => { | ||||||
|     return ( |     return ( | ||||||
|         <Grid> |         <Grid> | ||||||
|             <Overview> |             <FullWidthContainer> | ||||||
|                 Total changes / avg time to production / feature flags /stale |                 Total changes / avg time to production / feature flags /stale | ||||||
|                 flags |                 flags | ||||||
|             </Overview> |             </FullWidthContainer> | ||||||
|             <HealthCard> |             <MediumWideContainer> | ||||||
|                 <ProjectHealth /> |                 <ProjectHealth /> | ||||||
|             </HealthCard> |             </MediumWideContainer> | ||||||
|             <LeadTimeForChanges /> |             <WideContainer> | ||||||
|             <ToggleTypesUsedCard>Toggle types used</ToggleTypesUsedCard> |                 <LeadTimeForChanges /> | ||||||
|             <ProjectMembersCard>Project members</ProjectMembersCard> |             </WideContainer> | ||||||
|             <ChangeRequestsCard> |             <NarrowContainer> | ||||||
|  |                 <FlagTypesUsed /> | ||||||
|  |             </NarrowContainer> | ||||||
|  |             <NarrowContainer>Project members</NarrowContainer> | ||||||
|  |             <WideContainer> | ||||||
|                 <ChangeRequests /> |                 <ChangeRequests /> | ||||||
|             </ChangeRequestsCard> |             </WideContainer> | ||||||
|         </Grid> |         </Grid> | ||||||
|     ); |     ); | ||||||
| }; | }; | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user