mirror of
https://github.com/Unleash/unleash.git
synced 2025-09-28 17:55:15 +02: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