Files
madbase/control-plane-ui/src/components/Dashboard/PillarCard.tsx
Vlad Durnea a66d908eff
Some checks failed
CI / podman-build (push) Has been cancelled
CI / rust (push) Has been cancelled
chore: full stack stability and migration fixes, plus react UI progress
2026-03-18 09:01:38 +02:00

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>
)
}