All files / src/components/features/admin/monitoring/performance/PerformanceMetricCard index.tsx

100% Statements 164/164
96.66% Branches 29/30
100% Functions 4/4
100% Lines 164/164

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 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 1651x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 66x 66x 64x 64x 3x 1x 1x 1x 1x 1x 1x 1x 1x 5x 5x 5x 2x 2x 2x 2x 2x 5x 2x 2x 2x 2x 2x 5x 1x 1x 1x 1x 1x 5x 5x 1x 1x 1x 1x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 66x 66x 126x 126x 126x 5x 5x 3x 3x 3x 3x 2x 3x 3x 2x 5x 5x 126x 126x 126x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 123x 123x 123x 123x 123x 123x 123x 123x 126x 126x 126x 126x 126x 126x 126x 2x 126x 126x 126x 126x 107x 126x 126x 5x 5x 5x 5x 126x 126x 126x 126x 126x  
'use client';
 
import React from 'react';
import { cn } from '@/lib/core';
 
export interface PerformanceMetricCardProps {
  /** Title of the metric */
  title: string;
  /** Value to display */
  value: string | number;
  /** Unit suffix (e.g., 'ms', '%', 'req/s') */
  unit?: string;
  /** Subtitle or description */
  subtitle?: string;
  /** Threshold configuration for color coding */
  thresholds?: {
    /** Value below which is considered good (green) */
    good: number;
    /** Value below which is considered warning (yellow), above is critical (red) */
    warning: number;
  };
  /** Current numeric value for threshold comparison */
  numericValue?: number;
  /** Force a specific color class */
  valueClassName?: string;
  /** Custom icon to display */
  icon?: React.ReactNode;
  /** Trend indicator */
  trend?: {
    direction: 'up' | 'down' | 'stable';
    value: string;
    isPositive?: boolean;
  };
  /** Additional CSS classes */
  className?: string;
  /** Loading state */
  isLoading?: boolean;
}
 
/**
 * Get color class based on value and thresholds
 */
function getThresholdColor(value: number, thresholds: { good: number; warning: number }): string {
  if (value <= thresholds.good) {
    return 'text-green-600 dark:text-green-400';
  }
  if (value <= thresholds.warning) {
    return 'text-yellow-600 dark:text-yellow-400';
  }
  return 'text-red-600 dark:text-red-400';
}
 
/**
 * Get trend icon component
 */
function TrendIcon({ direction }: { direction: 'up' | 'down' | 'stable' }) {
  switch (direction) {
    case 'up':
      return (
        <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
          <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 10l7-7m0 0l7 7m-7-7v18" />
        </svg>
      );
    case 'down':
      return (
        <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
          <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 14l-7 7m0 0l-7-7m7 7V3" />
        </svg>
      );
    default:
      return (
        <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
          <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 12h14" />
        </svg>
      );
  }
}
 
/**
 * PerformanceMetricCard - Displays a single performance metric with optional thresholds and trends
 */
export function PerformanceMetricCard({
  title,
  value,
  unit,
  subtitle,
  thresholds,
  numericValue,
  valueClassName,
  icon,
  trend,
  className,
  isLoading = false,
}: PerformanceMetricCardProps) {
  // Determine value color
  let computedValueClass = valueClassName || 'text-gray-900 dark:text-white';
  if (!valueClassName && thresholds && numericValue !== undefined) {
    computedValueClass = getThresholdColor(numericValue, thresholds);
  }
 
  // Determine trend color
  const getTrendColor = () => {
    if (!trend) return '';
    if (trend.isPositive === undefined) {
      // Default: up is bad (red), down is good (green) for response times
      return trend.direction === 'down'
        ? 'text-green-600 dark:text-green-400'
        : trend.direction === 'up'
          ? 'text-red-600 dark:text-red-400'
          : 'text-gray-500 dark:text-gray-400';
    }
    return trend.isPositive
      ? 'text-green-600 dark:text-green-400'
      : 'text-red-600 dark:text-red-400';
  };
 
  if (isLoading) {
    return (
      <div
        className={cn(
          'bg-white dark:bg-gray-800 rounded-lg shadow p-4 animate-pulse',
          className
        )}
        data-testid="performance-metric-card"
        aria-busy="true"
      >
        <div className="h-4 w-24 bg-gray-200 dark:bg-gray-700 rounded mb-2" />
        <div className="h-8 w-16 bg-gray-200 dark:bg-gray-700 rounded mb-1" />
        <div className="h-3 w-20 bg-gray-200 dark:bg-gray-700 rounded" />
      </div>
    );
  }
 
  return (
    <div
      className={cn('bg-white dark:bg-gray-800 rounded-lg shadow p-4', className)}
      data-testid="performance-metric-card"
    >
      <div className="flex items-start justify-between">
        <p className="text-sm font-medium text-gray-500 dark:text-gray-400">{title}</p>
        {icon && <div className="text-gray-400 dark:text-gray-500">{icon}</div>}
      </div>
      <div className="flex items-baseline gap-1 mt-1">
        <p className={cn('text-2xl font-bold', computedValueClass)}>
          {value}
        </p>
        {unit && (
          <span className="text-sm text-gray-500 dark:text-gray-400">{unit}</span>
        )}
      </div>
      <div className="flex items-center justify-between mt-1">
        {subtitle && (
          <p className="text-xs text-gray-400 dark:text-gray-500">{subtitle}</p>
        )}
        {trend && (
          <div className={cn('flex items-center gap-1 text-xs', getTrendColor())}>
            <TrendIcon direction={trend.direction} />
            <span>{trend.value}</span>
          </div>
        )}
      </div>
    </div>
  );
}