mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-09-29 22:34:33 +00:00
171 lines
4.1 KiB
TypeScript
171 lines
4.1 KiB
TypeScript
import type {SerializedStyles, Theme} from '@emotion/react';
|
|
import styled from '@emotion/styled';
|
|
import {AnimatePresence, motion} from 'framer-motion';
|
|
|
|
import testableTransition from 'sentry/utils/testableTransition';
|
|
import theme from 'sentry/utils/theme';
|
|
|
|
type TextProps = {
|
|
percent: number;
|
|
theme: Theme;
|
|
textCss?: Props['textCss'];
|
|
};
|
|
|
|
type Props = React.HTMLAttributes<SVGSVGElement> & {
|
|
value: number;
|
|
/**
|
|
* Apply a micro animation when the text value changes
|
|
*/
|
|
animateText?: boolean;
|
|
/**
|
|
* The color of the ring background
|
|
*/
|
|
backgroundColor?: string;
|
|
/**
|
|
* The width of the progress ring bar
|
|
*/
|
|
barWidth?: number;
|
|
maxValue?: number;
|
|
minValue?: number;
|
|
/**
|
|
* The color of the ring bar. A function may be provided to compute the color
|
|
* based on the percent value filled of the progress bar.
|
|
*/
|
|
progressColor?: string;
|
|
/**
|
|
* Endcaps on the progress bar
|
|
*/
|
|
progressEndcaps?: React.SVGAttributes<SVGCircleElement>['strokeLinecap'];
|
|
size?: number;
|
|
/**
|
|
* Text to display in the center of the ring
|
|
*/
|
|
text?: React.ReactNode;
|
|
/**
|
|
* The css to apply to the center text. A function may be provided to compute
|
|
* styles based on the state of the progress bar.
|
|
*/
|
|
textCss?: (p: TextProps) => SerializedStyles;
|
|
};
|
|
|
|
const Text = styled('div')<Omit<TextProps, 'theme'>>`
|
|
position: absolute;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
height: 100%;
|
|
width: 100%;
|
|
color: ${p => p.theme.chartLabel};
|
|
font-size: ${p => p.theme.fontSizeExtraSmall};
|
|
transition: color 100ms;
|
|
${p => p.textCss?.(p)}
|
|
`;
|
|
|
|
const AnimatedText = motion(Text);
|
|
|
|
AnimatedText.defaultProps = {
|
|
initial: {opacity: 0, y: -10},
|
|
animate: {opacity: 1, y: 0},
|
|
exit: {opacity: 0, y: 10},
|
|
transition: testableTransition(),
|
|
};
|
|
|
|
function ProgressRing({
|
|
value,
|
|
minValue = 0,
|
|
maxValue = 100,
|
|
size = 20,
|
|
barWidth = 3,
|
|
text,
|
|
textCss,
|
|
animateText = false,
|
|
progressColor = theme.green300,
|
|
backgroundColor = theme.gray200,
|
|
progressEndcaps,
|
|
...p
|
|
}: Props) {
|
|
const radius = size / 2 - barWidth / 2;
|
|
const circumference = 2 * Math.PI * radius;
|
|
|
|
const boundedValue = Math.min(Math.max(value, minValue), maxValue);
|
|
const progress = (boundedValue - minValue) / (maxValue - minValue);
|
|
const percent = progress * 100;
|
|
const progressOffset = (1 - progress) * circumference;
|
|
|
|
const TextComponent = animateText ? AnimatedText : Text;
|
|
|
|
let textNode = (
|
|
<TextComponent key={text?.toString()} {...{textCss, percent}}>
|
|
{text}
|
|
</TextComponent>
|
|
);
|
|
|
|
textNode = animateText ? (
|
|
<AnimatePresence initial={false}>{textNode}</AnimatePresence>
|
|
) : (
|
|
textNode
|
|
);
|
|
|
|
return (
|
|
<RingSvg
|
|
role="img"
|
|
height={radius * 2 + barWidth}
|
|
width={radius * 2 + barWidth}
|
|
{...p}
|
|
>
|
|
<RingBackground
|
|
r={radius}
|
|
barWidth={barWidth}
|
|
cx={radius + barWidth / 2}
|
|
cy={radius + barWidth / 2}
|
|
color={backgroundColor}
|
|
/>
|
|
<RingBar
|
|
strokeDashoffset={progressOffset}
|
|
strokeLinecap={progressEndcaps}
|
|
circumference={circumference}
|
|
r={radius}
|
|
barWidth={barWidth}
|
|
cx={radius + barWidth / 2}
|
|
cy={radius + barWidth / 2}
|
|
color={progressColor}
|
|
/>
|
|
<foreignObject height="100%" width="100%">
|
|
{text !== undefined && textNode}
|
|
</foreignObject>
|
|
</RingSvg>
|
|
);
|
|
}
|
|
|
|
const RingSvg = styled('svg')`
|
|
position: relative;
|
|
`;
|
|
|
|
const RingBackground = styled('circle')<{barWidth: number; color: string}>`
|
|
fill: none;
|
|
stroke: ${p => p.color};
|
|
stroke-width: ${p => p.barWidth}px;
|
|
transition: stroke 100ms;
|
|
`;
|
|
|
|
const RingBar = styled('circle')<{
|
|
barWidth: number;
|
|
circumference: number;
|
|
color: string;
|
|
}>`
|
|
fill: none;
|
|
stroke: ${p => p.color};
|
|
stroke-width: ${p => p.barWidth}px;
|
|
stroke-dasharray: ${p => p.circumference} ${p => p.circumference};
|
|
transform: rotate(-90deg);
|
|
transform-origin: 50% 50%;
|
|
transition:
|
|
stroke-dashoffset 200ms,
|
|
stroke 100ms;
|
|
`;
|
|
|
|
export default ProgressRing;
|
|
|
|
// We export components to allow for css selectors
|
|
export {RingBackground, RingBar, Text as RingText};
|