1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-03-18 00:19:49 +01:00

feat: combine health and status widgets (#8782)

This PR combines the health and status widgets into a single widget. It
adds a new row with information on your unhealthy flags.

The stat prettifies large numbers to avoid overflows for very large
numbers. To keep it in line with the SVG, I've extracted some of the
constants so we can share them for size calculation.


![image](https://github.com/user-attachments/assets/0b66b6ba-b88c-412e-838f-bd5e7867cdc3)

When it folds, it uses the "space-around" algorithm to make the two
stats occupy their own positions:

![image](https://github.com/user-attachments/assets/eb3a4f0e-31fa-4895-ba12-c6f910112204)

The number uses a custom background color in dark mode because elevation
1 and 2 are the same there:

![image](https://github.com/user-attachments/assets/fa405c33-f38a-4fea-bf20-e1d9bf21bb1c)
This commit is contained in:
Thomas Heartman 2024-11-18 12:01:08 +01:00 committed by GitHub
parent e712a65dff
commit fb9c754008
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 85 additions and 72 deletions

View File

@ -4,6 +4,12 @@ import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { useProjectStatus } from 'hooks/api/getters/useProjectStatus/useProjectStatus';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { HealthGridTile } from './ProjectHealthGrid.styles';
import { PrettifyLargeNumber } from 'component/common/PrettifyLargeNumber/PrettifyLargeNumber';
const ChartRadius = 40;
const ChartStrokeWidth = 13;
const ChartTotalWidth = ChartRadius * 2 + ChartStrokeWidth;
const ChartContainerWidth = 100;
const TextContainer = styled('div')(({ theme }) => ({
display: 'flex',
@ -13,14 +19,14 @@ const TextContainer = styled('div')(({ theme }) => ({
const ChartRow = styled('div')(({ theme }) => ({
display: 'flex',
alignItems: 'center',
alignItems: 'flex-start',
gap: theme.spacing(2),
}));
const SVGWrapper = styled('div')(({ theme }) => ({
flex: 'none',
height: 85,
width: 100,
width: ChartContainerWidth,
position: 'relative',
}));
@ -28,16 +34,67 @@ const StyledSVG = styled('svg')({
position: 'absolute',
});
const BigText = styled('span')(({ theme }) => ({
fontSize: theme.typography.h1.fontSize,
}));
const UnhealthyStatContainer = styled('div')(({ theme }) => ({
flex: 'none',
display: 'grid',
placeItems: 'center',
width: ChartContainerWidth,
}));
const UnhealthyStatText = styled('p')(({ theme }) => ({
fontSize: theme.typography.body2.fontSize,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
borderRadius: '50%',
backgroundColor:
theme.mode === 'light'
? theme.palette.background.elevation2
: '#302E42', // in dark mode, elevation2 and elevation1 are the same color. This is an alternative
width: ChartTotalWidth,
height: ChartTotalWidth,
overflow: 'hidden',
}));
const UnhealthyFlagBox = ({ flagCount }: { flagCount: number }) => {
const flagWord = flagCount === 1 ? 'flag' : 'flags';
return (
<UnhealthyStatContainer>
<UnhealthyStatText>
<BigText>
<PrettifyLargeNumber
value={flagCount}
threshold={1000}
precision={1}
/>
</BigText>
<span>unhealthy</span>
<span>{flagWord}</span>
</UnhealthyStatText>
</UnhealthyStatContainer>
);
};
const Wrapper = styled(HealthGridTile)(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-around',
gap: theme.spacing(2),
}));
export const ProjectHealth = () => {
const projectId = useRequiredPathParam('projectId');
const {
data: { averageHealth },
data: { averageHealth, staleFlags },
} = useProjectStatus(projectId);
const { isOss } = useUiConfig();
const theme = useTheme();
const radius = 40;
const strokeWidth = 13;
const circumference = 2 * Math.PI * radius;
const circumference = 2 * Math.PI * ChartRadius; //
const gapLength = 0.3;
const filledLength = 1 - gapLength;
@ -52,27 +109,27 @@ export const ProjectHealth = () => {
: theme.palette.success.border;
return (
<HealthGridTile>
<Wrapper>
<ChartRow>
<SVGWrapper>
<StyledSVG viewBox='0 0 100 100'>
<circle
cx='50'
cy='50'
r={radius}
r={ChartRadius}
fill='none'
stroke={theme.palette.grey[300]}
strokeWidth={strokeWidth}
strokeWidth={ChartStrokeWidth}
strokeDasharray={`${filledLength * circumference} ${gapLength * circumference}`}
strokeDashoffset={offset * circumference}
/>
<circle
cx='50'
cy='50'
r={radius}
r={ChartRadius}
fill='none'
stroke={healthColor}
strokeWidth={strokeWidth}
strokeWidth={ChartStrokeWidth}
strokeDasharray={`${healthLength} ${circumference - healthLength}`}
strokeDashoffset={offset * circumference}
/>
@ -82,7 +139,7 @@ export const ProjectHealth = () => {
textAnchor='middle'
dominantBaseline='middle'
fill={theme.palette.text.primary}
fontSize='24px'
fontSize={theme.typography.h1.fontSize}
>
{averageHealth}%
</text>
@ -100,6 +157,19 @@ export const ProjectHealth = () => {
)}
</TextContainer>
</ChartRow>
</HealthGridTile>
<ChartRow>
<UnhealthyFlagBox flagCount={staleFlags.total} />
<TextContainer>
<Typography variant='body2'>
To keep your project healthy, archive stale feature
flags and remove code from your code base to reduce
technical debt.
</Typography>
<Link to={`/projects/${projectId}?state=IS%3Astale`}>
View unhealthy flags
</Link>
</TextContainer>
</ChartRow>
</Wrapper>
);
};

View File

@ -1,7 +1,7 @@
import { styled } from '@mui/material';
export const HealthGridTile = styled('article')(({ theme }) => ({
backgroundColor: theme.palette.neutral.light,
backgroundColor: theme.palette.background.elevation1,
padding: theme.spacing(3),
borderRadius: theme.shape.borderRadiusExtraLarge,
}));

View File

@ -1,6 +1,5 @@
import { ProjectHealth } from './ProjectHealth';
import { styled } from '@mui/material';
import { StaleFlags } from './StaleFlags';
import { ProjectResources } from './ProjectResources';
const onNarrowGrid = (css: object) => ({
@ -18,7 +17,7 @@ const HealthGrid = styled('div')(({ theme }) => ({
display: 'grid',
gridTemplateAreas: `
"health resources"
"stale resources"
"health resources"
`,
gridTemplateColumns: 'repeat(2, minmax(300px, 1fr))',
gap: theme.spacing(1, 2),
@ -45,9 +44,6 @@ export const ProjectHealthGrid = () => {
<Tile gridArea='health'>
<ProjectHealth />
</Tile>
<Tile gridArea='stale'>
<StaleFlags />
</Tile>
<Tile gridArea='resources'>
<ProjectResources />
</Tile>

View File

@ -1,53 +0,0 @@
import { Typography } from '@mui/material';
import { styled } from '@mui/material';
import { PrettifyLargeNumber } from 'component/common/PrettifyLargeNumber/PrettifyLargeNumber';
import { useProjectStatus } from 'hooks/api/getters/useProjectStatus/useProjectStatus';
import useLoading from 'hooks/useLoading';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import type { FC } from 'react';
import { Link } from 'react-router-dom';
import { HealthGridTile } from './ProjectHealthGrid.styles';
const Wrapper = styled(HealthGridTile)(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(1),
}));
const BigText = styled('span')(({ theme }) => ({
fontSize: `calc(2 * ${theme.typography.body1.fontSize})`,
lineHeight: 0,
}));
const BigNumber: FC<{ value?: number }> = ({ value }) => {
return (
<BigText data-loading-stale-flags>
<PrettifyLargeNumber
value={value ?? 0}
threshold={1000}
precision={1}
/>
</BigText>
);
};
export const StaleFlags = () => {
const projectId = useRequiredPathParam('projectId');
const { data, loading } = useProjectStatus(projectId);
const loadingRef = useLoading(loading, '[data-loading-stale-flags=true]');
return (
<Wrapper ref={loadingRef}>
<Typography component='h4'>
<BigNumber value={data?.staleFlags.total ?? 0} />{' '}
<Link to={`/projects/${projectId}?state=IS%3Astale`}>
stale flags
</Link>
</Typography>
<Typography variant='body2'>
Remember to archive your stale feature flags to keep the project
healthy
</Typography>
</Wrapper>
);
};