184 lines
6.8 KiB
TypeScript
184 lines
6.8 KiB
TypeScript
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
Typography,
|
|
Box,
|
|
LinearProgress,
|
|
IconButton,
|
|
Tooltip,
|
|
Chip,
|
|
useTheme,
|
|
alpha,
|
|
Grid,
|
|
Divider,
|
|
} from '@mui/material'
|
|
import {
|
|
TrendingUp as ScaleUpIcon,
|
|
TrendingDown as ScaleDownIcon,
|
|
Settings as SettingsIcon,
|
|
CheckCircle as OnlineIcon,
|
|
Sync as ScalingIcon,
|
|
Error as ErrorIcon,
|
|
} from '@mui/icons-material'
|
|
import { ResponsiveContainer, AreaChart, Area } from 'recharts'
|
|
import { PillarStats } from '../../hooks/usePillars'
|
|
|
|
interface PillarCardProps {
|
|
stats: PillarStats
|
|
onScale?: (pillar: string, action: 'up' | 'down') => void
|
|
}
|
|
|
|
const mockChartData = [
|
|
{ val: 40 }, { val: 30 }, { val: 45 }, { val: 60 }, { val: 55 }, { val: 70 }, { val: 85 }
|
|
]
|
|
|
|
export default function PillarCard({ stats, onScale }: PillarCardProps) {
|
|
const theme = useTheme()
|
|
const isScaling = stats.is_scaling
|
|
const hasSuggestion = stats.suggestion && stats.suggestion.action !== 'none'
|
|
|
|
return (
|
|
<Card
|
|
sx={{
|
|
height: '100%',
|
|
position: 'relative',
|
|
overflow: 'hidden',
|
|
background: `linear-gradient(135deg, ${alpha(theme.palette.background.paper, 0.9)} 0%, ${alpha(theme.palette.background.paper, 0.7)} 100%)`,
|
|
backdropFilter: 'blur(10px)',
|
|
border: `1px solid ${alpha(theme.palette.divider, 0.1)}`,
|
|
transition: 'transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out',
|
|
'&:hover': {
|
|
transform: 'translateY(-4px)',
|
|
boxShadow: `0 8px 24px ${alpha(theme.palette.common.black, 0.3)}`,
|
|
border: `1px solid ${alpha(theme.palette.primary.main, 0.3)}`,
|
|
}
|
|
}}
|
|
>
|
|
{/* Scaling Animation Overlay */}
|
|
{isScaling && (
|
|
<LinearProgress
|
|
sx={{
|
|
position: 'absolute',
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
height: 4,
|
|
'& .MuiLinearProgress-bar': {
|
|
backgroundImage: `linear-gradient(90deg, ${theme.palette.primary.main}, ${theme.palette.secondary.main})`
|
|
}
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
<CardContent sx={{ p: 2.5 }}>
|
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 2 }}>
|
|
<Box>
|
|
<Typography variant="overline" color="text.secondary" sx={{ fontWeight: 'bold', letterSpacing: 1 }}>
|
|
{stats.pillar}
|
|
</Typography>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', mt: 0.5 }}>
|
|
<Typography variant="h4" sx={{ fontWeight: 800, mr: 1 }}>
|
|
{stats.active_count}
|
|
</Typography>
|
|
<Typography variant="body2" color="text.secondary">
|
|
/ {stats.node_count} nodes
|
|
</Typography>
|
|
</Box>
|
|
</Box>
|
|
<Chip
|
|
icon={isScaling ? <ScalingIcon sx={{ animation: 'spin 2s linear infinite' }} /> : <OnlineIcon />}
|
|
label={isScaling ? 'Scaling' : 'Online'}
|
|
size="small"
|
|
color={isScaling ? 'primary' : 'success'}
|
|
variant="outlined"
|
|
sx={{
|
|
fontWeight: 'bold',
|
|
'@keyframes spin': {
|
|
'0%': { transform: 'rotate(0deg)' },
|
|
'100%': { transform: 'rotate(360deg)' }
|
|
}
|
|
}}
|
|
/>
|
|
</Box>
|
|
|
|
{/* Mini Sparkline */}
|
|
<Box sx={{ height: 60, width: '100%', mb: 2, opacity: 0.8 }}>
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
<AreaChart data={mockChartData}>
|
|
<defs>
|
|
<linearGradient id={`grad-${stats.pillar}`} x1="0" y1="0" x2="0" y2="1">
|
|
<stop offset="5%" stopColor={theme.palette.primary.main} stopOpacity={0.3}/>
|
|
<stop offset="95%" stopColor={theme.palette.primary.main} stopOpacity={0}/>
|
|
</linearGradient>
|
|
</defs>
|
|
<Area
|
|
type="monotone"
|
|
dataKey="val"
|
|
stroke={theme.palette.primary.main}
|
|
strokeWidth={2}
|
|
fillOpacity={1}
|
|
fill={`url(#grad-${stats.pillar})`}
|
|
/>
|
|
</AreaChart>
|
|
</ResponsiveContainer>
|
|
</Box>
|
|
|
|
{/* Metrics Gauges */}
|
|
<Grid container spacing={2} sx={{ mb: 2 }}>
|
|
<Grid item xs={6}>
|
|
<Typography variant="caption" color="text.secondary" display="block">CPU Load</Typography>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
<LinearProgress
|
|
variant="determinate"
|
|
value={stats.metrics?.cpu_usage || 12}
|
|
sx={{ flexGrow: 1, height: 6, borderRadius: 3 }}
|
|
color={ (stats.metrics?.cpu_usage || 12) > 80 ? 'error' : 'primary'}
|
|
/>
|
|
<Typography variant="caption" sx={{ minWidth: 25 }}>{stats.metrics?.cpu_usage || 12}%</Typography>
|
|
</Box>
|
|
</Grid>
|
|
<Grid item xs={6}>
|
|
<Typography variant="caption" color="text.secondary" display="block">Memory</Typography>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
<LinearProgress
|
|
variant="determinate"
|
|
value={stats.metrics?.memory_usage || 45}
|
|
sx={{ flexGrow: 1, height: 6, borderRadius: 3 }}
|
|
color="secondary"
|
|
/>
|
|
<Typography variant="caption" sx={{ minWidth: 25 }}>{stats.metrics?.memory_usage || 45}%</Typography>
|
|
</Box>
|
|
</Grid>
|
|
</Grid>
|
|
|
|
{/* Suggestion & Actions */}
|
|
<Divider sx={{ my: 1.5, opacity: 0.5 }} />
|
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
{hasSuggestion ? (
|
|
<Tooltip title={stats.suggestion?.reason}>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, color: theme.palette.warning.main }}>
|
|
<ErrorIcon fontSize="inherit" />
|
|
<Typography variant="caption" sx={{ fontWeight: 'bold' }}>
|
|
Sug: {stats.suggestion?.action === 'scale_up' ? '+' : '-'}{Math.abs(stats.suggestion!.target_count - stats.node_count)} nodes
|
|
</Typography>
|
|
</Box>
|
|
</Tooltip>
|
|
) : (
|
|
<Typography variant="caption" color="text.secondary">Optimal Performance</Typography>
|
|
)}
|
|
|
|
<Box>
|
|
<IconButton size="small" onClick={() => onScale?.(stats.pillar, 'down')} disabled={isScaling || stats.node_count <= 1}>
|
|
<ScaleDownIcon fontSize="small" />
|
|
</IconButton>
|
|
<IconButton size="small" color="primary" onClick={() => onScale?.(stats.pillar, 'up')} disabled={isScaling}>
|
|
<ScaleUpIcon fontSize="small" />
|
|
</IconButton>
|
|
</Box>
|
|
</Box>
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|