All files / src/components/charts SecurityRiskScore.tsx

100% Statements 66/66
100% Branches 13/13
100% Functions 1/1
100% Lines 66/66

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 1171x                                                                           1x 45x 45x 45x 45x 45x 45x   45x 45x 45x 41x 45x     45x 45x 45x 7x 45x     45x 45x 45x   45x 45x 45x 45x   45x 45x   45x 45x 45x 45x 45x 45x 45x 45x 45x   45x 45x 45x 45x 45x 45x 45x 45x 45x 45x 45x 45x 45x 45x 45x 45x 45x 45x   45x 45x 45x 45x 45x 45x 45x   45x 45x 45x   45x   1x  
import React, { useMemo } from "react";
 
interface SecurityRiskScoreProps {
  /**
   * The security score to display (0-100)
   */
  score: number;
 
  /**
   * Maximum possible score (defaults to 100)
   */
  maxScore?: number;
 
  /**
   * Label to display under the score
   */
  label: string;
 
  /**
   * Optional CSS class name
   */
  className?: string;
 
  /**
   * Optional test ID for automated testing
   */
  testId?: string;
}
 
/**
 * Displays a circular gauge chart showing a security risk score
 *
 * ## Business Perspective
 *
 * This component provides a quantitative measure of security posture in an
 * easy-to-understand format. The numerical score and color-coding help
 * business stakeholders quickly gauge security maturity. 📈
 */
export function SecurityRiskScore({
  score,
  maxScore = 100,
  label,
  className = "",
  testId,
}: SecurityRiskScoreProps): React.ReactElement {
  // Normalize score as a percentage
  const normalizedScore = useMemo(() => {
    if (score <= 0) return 0;
    if (score >= maxScore) return 100;
    return (score / maxScore) * 100;
  }, [score, maxScore]);
 
  // Determine color based on score range
  const scoreColor = useMemo(() => {
    if (normalizedScore >= 75) return "text-green-500 dark:text-green-400";
    if (normalizedScore >= 50) return "text-yellow-500 dark:text-yellow-400";
    return "text-red-500 dark:text-red-400";
  }, [normalizedScore]);
 
  // Calculate the stroke dash offset for the circular progress
  const circumference = 2 * Math.PI * 28; // 2Ï€r where r=28
  const strokeDashArray = `${circumference}`;
  const strokeDashOffset = ((100 - normalizedScore) / 100) * circumference;
 
  return (
    <div
      className={`flex flex-col items-center ${className}`}
      data-testid={testId}
    >
      <div className="relative">
        <svg width="64" height="64" viewBox="0 0 64 64">
          {/* Background circle */}
          <circle
            cx="32"
            cy="32"
            r="28"
            fill="none"
            stroke="#e5e7eb"
            strokeWidth="8"
            className="dark:stroke-gray-700"
          />
          {/* Score indicator */}
          <circle
            cx="32"
            cy="32"
            r="28"
            fill="none"
            strokeLinecap="round"
            stroke="currentColor"
            strokeWidth="8"
            strokeDasharray={strokeDashArray}
            strokeDashoffset={strokeDashOffset}
            transform="rotate(-90 32 32)"
            className={scoreColor}
          />
        </svg>
        <div className="absolute inset-0 flex items-center justify-center">
          <span
            className={`text-lg font-bold ${scoreColor}`}
            data-testid={`${testId}-value`}
          >
            {Math.round(normalizedScore)}
          </span>
        </div>
      </div>
      <span
        className="text-xs text-gray-600 dark:text-gray-400 mt-1"
        data-testid={`${testId}-label`}
      >
        {label}
      </span>
    </div>
  );
}
 
export default SecurityRiskScore;