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;
|