diff --git a/frontend/src/components/dashboard/StatCard.tsx b/frontend/src/components/dashboard/StatCard.tsx new file mode 100644 index 000000000..286c18e3c --- /dev/null +++ b/frontend/src/components/dashboard/StatCard.tsx @@ -0,0 +1,216 @@ +import React from 'react'; +import { motion, Variants } from 'framer-motion'; +import { Link } from 'react-router-dom'; +import { ChevronUp, ChevronDown, FileText, Activity } from 'lucide-react'; + +interface StatCardProps { + title: string; + value: number | string; + icon: React.ElementType; + change?: number; + iconColor: 'blue' | 'green' | 'purple' | 'amber' | 'gray'; + isContext?: boolean; + link?: string; + variants?: Variants; +} + +const defaultVariants: Variants = { + initial: { opacity: 0, y: 20 }, + animate: { opacity: 1, y: 0, transition: { duration: 0.5, ease: 'easeOut' } }, + exit: { opacity: 0, y: -10, transition: { duration: 0.3 } }, +}; + +// Get colors for gradient based on the icon color type +const getGradient = (iconColor: string) => { + switch (iconColor) { + case 'blue': + return 'bg-gradient-to-br from-blue-500/10 to-indigo-600/5 dark:from-blue-900/20 dark:to-indigo-900/10'; + case 'green': + return 'bg-gradient-to-br from-emerald-500/10 to-green-600/5 dark:from-emerald-900/20 dark:to-green-900/10'; + case 'purple': + return 'bg-gradient-to-br from-violet-500/10 to-purple-600/5 dark:from-violet-900/20 dark:to-purple-900/10'; + case 'amber': + return 'bg-gradient-to-br from-amber-500/10 to-orange-600/5 dark:from-amber-900/20 dark:to-orange-900/10'; + case 'gray': + default: + return 'bg-gradient-to-br from-gray-600/15 to-gray-700/10 dark:from-gray-800/30 dark:to-gray-900/20'; + } +}; + +// Get colors for the icon container +const getIconGradient = (iconColor: string) => { + switch (iconColor) { + case 'blue': + return 'bg-gradient-to-br from-blue-500 to-indigo-600 dark:from-blue-400 dark:to-indigo-500'; + case 'green': + return 'bg-gradient-to-br from-emerald-500 to-green-600 dark:from-emerald-400 dark:to-green-500'; + case 'purple': + return 'bg-gradient-to-br from-violet-500 to-purple-600 dark:from-violet-400 dark:to-purple-500'; + case 'amber': + return 'bg-gradient-to-br from-amber-500 to-orange-600 dark:from-amber-400 dark:to-orange-500'; + case 'gray': + default: + return 'bg-gradient-to-br from-gray-500 to-gray-600 dark:from-gray-400 dark:to-gray-500'; + } +}; + +interface CardLinkWrapperProps { + children?: React.ReactNode; + link?: string; +} + +const CardLinkWrapper: React.FC = ({ children, link }) => { + return link ? ( + + {children} + + ) : ( +
{children}
+ ); +}; + +// Get indicator component based on card type +const getIndicator = (title: string) => { + if (title === 'Total Clusters') { + return ( +
+ {[0.4, 0.7, 1, 0.6, 0.8].map((height, i) => ( + + ))} +
+ ); + } + + if (title === 'Active Clusters') { + return ( +
+
+ {[...Array(3)].map((_, i) => ( + + ))} +
+
+ ); + } + + if (title === 'Binding Policies') { + return ( +
+
+ +
+ ); + } + + if (title === 'Current Context') { + return ( +
+
+
+
+ +
+
+ ); + } + + return null; +}; +const StatCard: React.FC = ({ + title, + value, + icon: Icon, + change, + iconColor, + isContext = false, + link, + variants = defaultVariants, +}) => { + // Determine if change is positive, negative or neutral + const isPositive = typeof change === 'number' && change > 0; + const isNegative = typeof change === 'number' && change < 0; + + return ( + + + {/* Decorative background elements for visual interest without animation loops */} +
+
+ +
+
+
+ {React.createElement(Icon, { size: 18 })} +
+ + {title} + +
+
+ +
+
+
+

+ {value} +

+ {isContext && ( +
+ )} +
+ {change !== undefined && ( +
+ {isPositive && } + {isNegative && } + + {Math.abs(change)}% {isPositive ? 'increase' : isNegative ? 'decrease' : 'change'} + +
+ )} +
+ + {/* Static visual indicators that don't use infinite animation loops */} + {getIndicator(title)} +
+
+
+ ); +}; + +export default StatCard; diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 71401aa38..9ab8f3799 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -18,8 +18,6 @@ import { Clock, Cpu, HardDrive, - ChevronUp, - ChevronDown, BarChart3, ClipboardList, Shield, @@ -35,6 +33,7 @@ import { useUserActivityQuery, useDeletedUsersActivityQuery, } from '../hooks/queries/useUserActivityQuery.ts'; +import StatCard from '../components/dashboard/StatCard.tsx'; // Lazy load the ClusterDetailDialog component const ClusterDetailDialog = lazy( @@ -240,202 +239,6 @@ const itemAnimationVariant: Variants = { exit: { opacity: 0, y: -10, transition: { duration: 0.3 } }, }; -// Enhanced modern stat component with advanced UI -const StatCard = ({ - title, - value, - icon: Icon, - change, - iconColor, - isContext = false, - link, -}: { - title: string; - value: number | string; - icon: React.ElementType; - change?: number; - iconColor: string; - isContext?: boolean; - link?: string; -}) => { - // Determine if change is positive, negative or neutral - const isPositive = typeof change === 'number' && change > 0; - const isNegative = typeof change === 'number' && change < 0; - - // Get colors for gradient based on the icon color type - const getGradient = () => { - if (iconColor.includes('blue')) { - return 'bg-gradient-to-br from-blue-500/10 to-indigo-600/5 dark:from-blue-900/20 dark:to-indigo-900/10'; - } else if (iconColor.includes('green')) { - return 'bg-gradient-to-br from-emerald-500/10 to-green-600/5 dark:from-emerald-900/20 dark:to-green-900/10'; - } else if (iconColor.includes('purple')) { - return 'bg-gradient-to-br from-violet-500/10 to-purple-600/5 dark:from-violet-900/20 dark:to-purple-900/10'; - } else if (iconColor.includes('amber')) { - return 'bg-gradient-to-br from-amber-500/10 to-orange-600/5 dark:from-amber-900/20 dark:to-orange-900/10'; - } else { - return 'bg-gradient-to-br from-gray-500/5 to-gray-600/5 dark:from-gray-800/20 dark:to-gray-900/10'; - } - }; - - // Get colors for the icon container - const getIconGradient = () => { - if (iconColor.includes('blue')) { - return 'bg-gradient-to-br from-blue-500 to-indigo-600 dark:from-blue-400 dark:to-indigo-500'; - } else if (iconColor.includes('green')) { - return 'bg-gradient-to-br from-emerald-500 to-green-600 dark:from-emerald-400 dark:to-green-500'; - } else if (iconColor.includes('purple')) { - return 'bg-gradient-to-br from-violet-500 to-purple-600 dark:from-violet-400 dark:to-purple-500'; - } else if (iconColor.includes('amber')) { - return 'bg-gradient-to-br from-amber-500 to-orange-600 dark:from-amber-400 dark:to-orange-500'; - } else { - return 'bg-gradient-to-br from-gray-500 to-gray-600 dark:from-gray-400 dark:to-gray-500'; - } - }; - - interface CardLinkWrapperProps { - children?: React.ReactNode; - link?: string; - } - - const CardLinkWrapper: React.FC = ({ children, link }) => { - return link ? ( - - {children} - - ) : ( -
{children}
- ); - }; - - // Get indicator component based on card type - const getIndicator = () => { - if (title === 'Total Clusters') { - return ( -
- {[0.4, 0.7, 1, 0.6, 0.8].map((height, i) => ( - - ))} -
- ); - } - - if (title === 'Active Clusters') { - return ( -
-
- {[...Array(3)].map((_, i) => ( - - ))} -
-
- ); - } - - if (title === 'Binding Policies') { - return ( -
-
- -
- ); - } - - if (title === 'Current Context') { - return ( -
-
-
-
- -
-
- ); - } - - return null; - }; - - return ( - - - {/* Decorative background elements for visual interest without animation loops */} -
-
- -
-
-
- {React.createElement(Icon, { size: 18 })} -
- - {title} - -
-
- -
-
-
-

- {value} -

- {isContext && ( -
- )} -
- {change !== undefined && ( -
- {isPositive && } - {isNegative && } - - {Math.abs(change)}% {isPositive ? 'increase' : isNegative ? 'decrease' : 'change'} - -
- )} -
- - {/* Static visual indicators that don't use infinite animation loops */} - {getIndicator()} -
-
-
- ); -}; - // Enhanced overview card component const OverviewCard = ({ title, @@ -1208,28 +1011,28 @@ const K8sInfo = () => { title={t('clusters.dashboard.stats.totalClusters')} value={stats.totalClusters} icon={Server} - iconColor="bg-blue-100 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400" + iconColor="blue" link={'/its'} /> diff --git a/frontend/src/pages/PluginManager.tsx b/frontend/src/pages/PluginManager.tsx index a08f1fed6..1b09dcde8 100644 --- a/frontend/src/pages/PluginManager.tsx +++ b/frontend/src/pages/PluginManager.tsx @@ -20,12 +20,14 @@ import { HiOutlineRocketLaunch, } from 'react-icons/hi2'; import { Link } from 'react-router-dom'; +import { Puzzle, CheckCircle, XCircle } from 'lucide-react'; import { usePlugins } from '../plugins/PluginLoader'; import { PluginAPI } from '../plugins/PluginAPI'; import useTheme from '../stores/themeStore'; import getThemeStyles from '../lib/theme-utils'; import FeedbackModel from '../components/plugin/FeedbackModel'; import toast from 'react-hot-toast'; +import StatCard from '../components/dashboard/StatCard'; interface Plugin { name: string; @@ -225,6 +227,11 @@ export const PluginManager: React.FC = () => { ); } + // Calculate plugin stats once to avoid repeated filter operations + const totalPlugins = availablePlugins.length; + const activePlugins = availablePlugins.filter(p => p.enabled).length; + const inactivePlugins = availablePlugins.filter(p => !p.enabled).length; + return (
{/* Header */} @@ -271,43 +278,25 @@ export const PluginManager: React.FC = () => {
{/* Stats */} -
- {[ - { - label: t('plugins.list.total'), - value: availablePlugins.length, - color: themeStyles.colors.text.primary, - }, - { - label: t('plugins.list.active'), - value: availablePlugins.filter(p => p.enabled).length, - color: themeStyles.colors.status.success, - }, - { - label: t('plugins.list.inactive'), - value: availablePlugins.filter(p => !p.enabled).length, - color: themeStyles.colors.text.secondary, - }, - ].map((stat, index) => ( - - - {stat.value} - - - {stat.label} - - - ))} +
+ + +