import {SerializedStyles} from '@emotion/react'; import styled from '@emotion/styled'; import {AnimatePresence, motion} from 'framer-motion'; import testableTransition from 'sentry/utils/testableTransition'; import theme, {Theme} from 'sentry/utils/theme'; type TextProps = { percent: number; theme: Theme; textCss?: Props['textCss']; }; type Props = React.HTMLAttributes & { 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['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')>` 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.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(), }; const 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 = ( {text} ); textNode = animateText ? ( {textNode} ) : ( textNode ); return ( {text !== undefined && textNode} ); }; 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};