All files / src/components/common WidgetContainer.tsx

100% Statements 11/11
94.44% Branches 34/36
100% Functions 1/1
100% Lines 11/11

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                                                12x                         444x     444x           444x 444x     444x 444x 2x       444x 2x                                     442x                                          
import React from 'react';
 
export interface WidgetContainerProps {
  title: string;
  children: React.ReactNode;
  isLoading?: boolean;
  loading?: boolean; // For backward compatibility
  error?: string | null | Error; // Accept Error objects too
  className?: string;
  testId?: string;
  errorContent?: React.ReactNode;
  icon?: string | React.ReactNode;
  actions?: React.ReactNode;
}
 
/**
 * Container component for dashboard widgets
 * 
 * ## Business Perspective
 * 
 * This component provides a consistent presentation for all security dashboard
 * widgets, with standardized loading, error states, and styling. Consistency
 * in presentation helps users navigate security information more effectively. 🎨
 */
export const WidgetContainer: React.FC<WidgetContainerProps> = ({
  title,
  children,
  isLoading = false,
  loading = false, // Accept loading prop for backward compatibility
  error = null,
  className = '',
  testId,
  errorContent,
  icon,
  actions
}) => {
  // For backward compatibility - support older code using "loading" prop
  const isLoadingState = isLoading || loading;
  
  // Create unique test IDs for different widget states
  const containerTestId = error 
    ? `widget-container-error${testId ? `-${testId}` : ''}` 
    : isLoadingState 
      ? `widget-container-loading-container${testId ? `-${testId}` : ''}` 
      : `widget-container${testId ? `-${testId}` : ''}`;
  
  const spinnerTestId = `widget-spinner${testId ? `-${testId}` : ''}`;
  const errorTestId = `test-widget-error${testId ? `-${testId}` : ''}`;
 
  // Convert Error objects to strings
  let errorMessage: string | null = null;
  if (error !== null) {
    errorMessage = error instanceof Error ? error.message : String(error);
  }
 
  // Handle error state
  if (errorMessage) {
    return (
      <div className={`widget-container widget-error border border-red-300 rounded-lg shadow-sm ${className}`} data-testid={containerTestId}>
        <div className="widget-header bg-red-50 dark:bg-red-900 dark:bg-opacity-20 px-4 py-3 border-b border-red-200 dark:border-red-800 rounded-t-lg">
          <h3 className="text-lg font-medium text-red-800 dark:text-red-300 flex items-center">
            <span className="mr-2">⚠️</span>
            {title}
          </h3>
        </div>
        <div className="widget-body p-4 bg-white dark:bg-gray-900">
          <div className="text-red-600 dark:text-red-400" data-testid={errorTestId}>
            {errorMessage}
          </div>
          {errorContent && <div>{errorContent}</div>}
        </div>
      </div>
    );
  }
 
  // Handle loading or normal state
  return (
    <div className={`widget-container border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm ${className}`} data-testid={containerTestId}>
      <div className="widget-header bg-gray-50 dark:bg-gray-800 px-4 py-3 border-b border-gray-200 dark:border-gray-700 rounded-t-lg flex justify-between items-center">
        <h3 className="text-lg font-medium text-gray-800 dark:text-gray-200 flex items-center">
          {icon && <span className="mr-2">{icon}</span>}
          {title}
        </h3>
        {actions && <div className="widget-actions">{actions}</div>}
      </div>
      <div className={`widget-body p-4 bg-white dark:bg-gray-900 rounded-b-lg ${isLoadingState ? 'flex items-center justify-center min-h-[100px]' : ''}`}>
        {isLoadingState ? (
          <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500" data-testid={spinnerTestId} />
        ) : (
          children
        )}
      </div>
    </div>
  );
};
 
export default WidgetContainer;