mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-20 00:08:02 +01:00
feat: flag types used for project insights (#6607)
![image](https://github.com/Unleash/unleash/assets/964450/b9eecb3e-eb02-4cc7-96eb-033c010734c0)
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>
|
||||||
|
<WideContainer>
|
||||||
<LeadTimeForChanges />
|
<LeadTimeForChanges />
|
||||||
<ToggleTypesUsedCard>Toggle types used</ToggleTypesUsedCard>
|
</WideContainer>
|
||||||
<ProjectMembersCard>Project members</ProjectMembersCard>
|
<NarrowContainer>
|
||||||
<ChangeRequestsCard>
|
<FlagTypesUsed />
|
||||||
|
</NarrowContainer>
|
||||||
|
<NarrowContainer>Project members</NarrowContainer>
|
||||||
|
<WideContainer>
|
||||||
<ChangeRequests />
|
<ChangeRequests />
|
||||||
</ChangeRequestsCard>
|
</WideContainer>
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user