mirror of
https://github.com/Unleash/unleash.git
synced 2024-12-22 19:07:54 +01:00
feat: project health chart (#6594)
This commit is contained in:
parent
c2015c6f33
commit
72758605b0
@ -0,0 +1,113 @@
|
||||
import React from 'react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { ProjectHealthChart } from './ProjectHealthChart';
|
||||
import { render } from 'utils/testRenderer';
|
||||
import { screen } from '@testing-library/react';
|
||||
|
||||
describe('ProjectHealthChart', () => {
|
||||
test('renders correctly with no flags', () => {
|
||||
const { container } = render(
|
||||
<ProjectHealthChart
|
||||
active={0}
|
||||
stale={0}
|
||||
potentiallyStale={0}
|
||||
health={0}
|
||||
/>,
|
||||
);
|
||||
|
||||
const activeCircle = container.querySelector(
|
||||
'circle[data-testid="active-circle"]',
|
||||
);
|
||||
const staleCircle = container.querySelector(
|
||||
'circle[data-testid="stale-circle"]',
|
||||
);
|
||||
const potentiallyStaleCircle = container.querySelector(
|
||||
'circle[data-testid="potentially-stale-circle"]',
|
||||
);
|
||||
|
||||
expect(activeCircle).toBeInTheDocument();
|
||||
expect(staleCircle).not.toBeInTheDocument();
|
||||
expect(potentiallyStaleCircle).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders correctly with 1 active and 0 stale', () => {
|
||||
const { container } = render(
|
||||
<ProjectHealthChart
|
||||
active={1}
|
||||
stale={0}
|
||||
potentiallyStale={0}
|
||||
health={100}
|
||||
/>,
|
||||
);
|
||||
|
||||
const activeCircle = container.querySelector(
|
||||
'circle[data-testid="active-circle"]',
|
||||
);
|
||||
const staleCircle = container.querySelector(
|
||||
'circle[data-testid="stale-circle"]',
|
||||
);
|
||||
const potentiallyStaleCircle = container.querySelector(
|
||||
'circle[data-testid="potentially-stale-circle"]',
|
||||
);
|
||||
|
||||
expect(activeCircle).toBeInTheDocument();
|
||||
expect(staleCircle).not.toBeInTheDocument();
|
||||
expect(potentiallyStaleCircle).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders correctly with 0 active and 1 stale', () => {
|
||||
const { container } = render(
|
||||
<ProjectHealthChart
|
||||
active={0}
|
||||
stale={1}
|
||||
potentiallyStale={0}
|
||||
health={0}
|
||||
/>,
|
||||
);
|
||||
|
||||
const staleCircle = container.querySelector(
|
||||
'circle[data-testid="stale-circle"]',
|
||||
);
|
||||
|
||||
expect(staleCircle).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders correctly with active, stale and potentially stale', () => {
|
||||
const { container } = render(
|
||||
<ProjectHealthChart
|
||||
active={2}
|
||||
stale={1}
|
||||
potentiallyStale={1}
|
||||
health={50}
|
||||
/>,
|
||||
);
|
||||
|
||||
const activeCircle = container.querySelector(
|
||||
'circle[data-testid="active-circle"]',
|
||||
);
|
||||
const staleCircle = container.querySelector(
|
||||
'circle[data-testid="stale-circle"]',
|
||||
);
|
||||
const potentiallyStaleCircle = container.querySelector(
|
||||
'circle[data-testid="potentially-stale-circle"]',
|
||||
);
|
||||
|
||||
expect(activeCircle).toBeInTheDocument();
|
||||
expect(staleCircle).toBeInTheDocument();
|
||||
expect(potentiallyStaleCircle).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders flags count and health', () => {
|
||||
const { container } = render(
|
||||
<ProjectHealthChart
|
||||
active={2}
|
||||
stale={1}
|
||||
potentiallyStale={1}
|
||||
health={50}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByText('3 flags')).toBeInTheDocument();
|
||||
expect(screen.queryByText('50%')).toBeInTheDocument();
|
||||
});
|
||||
});
|
@ -0,0 +1,128 @@
|
||||
import type React from 'react';
|
||||
import { useTheme } from '@mui/material';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
|
||||
interface ProgressComponentProps {
|
||||
active: number;
|
||||
stale: number;
|
||||
potentiallyStale: number;
|
||||
health: number;
|
||||
}
|
||||
|
||||
export const ProjectHealthChart: React.FC<ProgressComponentProps> = ({
|
||||
active,
|
||||
stale,
|
||||
potentiallyStale,
|
||||
health,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const gap = active === 0 || stale === 0 ? 0 : 10;
|
||||
const strokeWidth = 6;
|
||||
const radius = 50 - strokeWidth / 2;
|
||||
const circumference = 2 * Math.PI * radius;
|
||||
const gapAngle = (gap / circumference) * 360;
|
||||
|
||||
const totalCount = active + stale;
|
||||
const activePercentage =
|
||||
totalCount === 0 ? 100 : (active / totalCount) * 100;
|
||||
const stalePercentage = totalCount === 0 ? 0 : (stale / totalCount) * 100;
|
||||
const potentiallyStalePercentage =
|
||||
active === 0 ? 0 : (potentiallyStale / active) * 100;
|
||||
|
||||
const activeLength = (activePercentage / 100) * circumference - gap;
|
||||
const staleLength = (stalePercentage / 100) * circumference - gap;
|
||||
const potentiallyStaleLength =
|
||||
(potentiallyStalePercentage / 100) * activeLength;
|
||||
|
||||
const activeRotation = -90 + gapAngle / 2;
|
||||
const potentiallyStaleRotation =
|
||||
activeRotation +
|
||||
((activeLength - potentiallyStaleLength) / circumference) * 360;
|
||||
const staleRotation =
|
||||
activeRotation + (activeLength / circumference) * 360 + gapAngle;
|
||||
|
||||
const innerRadius = radius / 1.2;
|
||||
|
||||
return (
|
||||
<svg width='170' height='170' viewBox='0 0 100 100'>
|
||||
<title>Project Health Chart</title>
|
||||
<circle
|
||||
data-testid='active-circle'
|
||||
cx='50'
|
||||
cy='50'
|
||||
r={radius}
|
||||
fill='none'
|
||||
stroke={theme.palette.success.border}
|
||||
strokeWidth={strokeWidth}
|
||||
strokeLinecap='round'
|
||||
strokeDasharray={`${activeLength} ${circumference}`}
|
||||
transform={`rotate(${activeRotation} 50 50)`}
|
||||
/>
|
||||
|
||||
<ConditionallyRender
|
||||
condition={potentiallyStale > 0}
|
||||
show={
|
||||
<circle
|
||||
data-testid='potentially-stale-circle'
|
||||
cx='50'
|
||||
cy='50'
|
||||
r={radius}
|
||||
fill='none'
|
||||
stroke={theme.palette.warning.border}
|
||||
strokeWidth={strokeWidth}
|
||||
strokeLinecap='round'
|
||||
strokeDasharray={`${potentiallyStaleLength} ${circumference}`}
|
||||
transform={`rotate(${potentiallyStaleRotation} 50 50)`}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<ConditionallyRender
|
||||
condition={stale > 0}
|
||||
show={
|
||||
<circle
|
||||
data-testid='stale-circle'
|
||||
cx='50'
|
||||
cy='50'
|
||||
r={radius}
|
||||
fill='none'
|
||||
stroke={theme.palette.error.border}
|
||||
strokeWidth={strokeWidth}
|
||||
strokeLinecap='round'
|
||||
strokeDasharray={`${staleLength} ${circumference}`}
|
||||
transform={`rotate(${staleRotation} 50 50)`}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<circle
|
||||
cx='50'
|
||||
cy='50'
|
||||
r={innerRadius}
|
||||
fill={theme.palette.warning.light}
|
||||
/>
|
||||
|
||||
<text
|
||||
x='50%'
|
||||
y='50%'
|
||||
fill='black'
|
||||
fontSize={theme.spacing(2.25)}
|
||||
textAnchor='middle'
|
||||
fontWeight='bold'
|
||||
>
|
||||
{health}%
|
||||
</text>
|
||||
<text
|
||||
x='50%'
|
||||
y='50%'
|
||||
dy='1.5em'
|
||||
fill={theme.palette.text.secondary}
|
||||
fontSize={theme.spacing(1.25)}
|
||||
textAnchor='middle'
|
||||
fontWeight='normal'
|
||||
>
|
||||
{active + stale} flags
|
||||
</text>
|
||||
</svg>
|
||||
);
|
||||
};
|
Loading…
Reference in New Issue
Block a user