chore: full stack stability and migration fixes, plus react UI progress
This commit is contained in:
183
control-plane-ui/src/components/Dashboard/PillarCard.tsx
Normal file
183
control-plane-ui/src/components/Dashboard/PillarCard.tsx
Normal file
@@ -0,0 +1,183 @@
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -23,6 +23,12 @@ import {
|
||||
TrendingUp as ScalingIcon,
|
||||
Favorite as HealthIcon,
|
||||
Settings as SettingsIcon,
|
||||
People as UsersIcon,
|
||||
Folder as StorageIcon,
|
||||
TableChart as DatabaseIcon,
|
||||
Functions as FunctionsIcon,
|
||||
Bolt as RealtimeIcon,
|
||||
Article as LogsIcon,
|
||||
} from '@mui/icons-material'
|
||||
import { useNavigate, useLocation } from 'react-router-dom'
|
||||
|
||||
@@ -30,6 +36,12 @@ const drawerWidth = 240
|
||||
|
||||
const menuItems = [
|
||||
{ text: 'Dashboard', icon: <DashboardIcon />, path: '/' },
|
||||
{ text: 'Users', icon: <UsersIcon />, path: '/auth' },
|
||||
{ text: 'Storage', icon: <StorageIcon />, path: '/storage' },
|
||||
{ text: 'Database', icon: <DatabaseIcon />, path: '/database' },
|
||||
{ text: 'Functions', icon: <FunctionsIcon />, path: '/functions' },
|
||||
{ text: 'Realtime', icon: <RealtimeIcon />, path: '/realtime' },
|
||||
{ text: 'Logs', icon: <LogsIcon />, path: '/logs' },
|
||||
{ text: 'Servers', icon: <ServerIcon />, path: '/servers' },
|
||||
{ text: 'Templates', icon: <TemplateIcon />, path: '/templates' },
|
||||
{ text: 'Providers', icon: <ProviderIcon />, path: '/providers' },
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Button,
|
||||
Typography,
|
||||
Box,
|
||||
Divider,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Alert,
|
||||
useTheme,
|
||||
alpha,
|
||||
Grid,
|
||||
CircularProgress,
|
||||
} from '@mui/material'
|
||||
import {
|
||||
AddCircle as AddIcon,
|
||||
RemoveCircle as RemoveIcon,
|
||||
AttachMoney as CostIcon,
|
||||
AccessTime as TimeIcon,
|
||||
Dns as ServerIcon,
|
||||
} from '@mui/icons-material'
|
||||
import { ScalingPlan } from '../../services/api'
|
||||
|
||||
interface ScalingConfirmationDialogProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
onConfirm: () => void
|
||||
plan: ScalingPlan | null
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
export default function ScalingConfirmationDialog({
|
||||
open,
|
||||
onClose,
|
||||
onConfirm,
|
||||
plan,
|
||||
loading
|
||||
}: ScalingConfirmationDialogProps) {
|
||||
const theme = useTheme()
|
||||
|
||||
if (!plan) return null
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
PaperProps={{
|
||||
sx: {
|
||||
bgcolor: alpha(theme.palette.background.paper, 0.95),
|
||||
backdropFilter: 'blur(10px)',
|
||||
backgroundImage: 'none',
|
||||
border: `1px solid ${alpha(theme.palette.divider, 0.1)}`,
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogTitle sx={{ pb: 1 }}>
|
||||
<Typography variant="h5" sx={{ fontWeight: 'bold' }}>Review Scaling Plan</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Confirm the infrastructure changes before execution.
|
||||
</Typography>
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent>
|
||||
<Box sx={{ mb: 3, p: 2, bgcolor: alpha(theme.palette.primary.main, 0.05), borderRadius: 2, border: `1px solid ${alpha(theme.palette.primary.main, 0.2)}` }}>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={6}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<CostIcon color="primary" fontSize="small" />
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary" uppercase>Monthly Impact</Typography>
|
||||
<Typography variant="h6" sx={{ color: theme.palette.primary.main, fontWeight: 'bold', lineHeight: 1 }}>
|
||||
+€{plan.total_cost_monthly.toFixed(2)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<TimeIcon color="secondary" fontSize="small" />
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary" uppercase>Estimated Time</Typography>
|
||||
<Typography variant="h6" sx={{ fontWeight: 'bold', lineHeight: 1 }}>
|
||||
~{plan.estimated_time_minutes} min
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
|
||||
<Typography variant="subtitle2" gutterBottom sx={{ fontWeight: 'bold' }}>Steps to execute:</Typography>
|
||||
<List dense disablePadding>
|
||||
{plan.scaling_plan.map((step, index) => (
|
||||
<ListItem key={index} sx={{ px: 0, py: 1 }}>
|
||||
<ListItemIcon sx={{ minWidth: 40 }}>
|
||||
{step.action.toLowerCase().includes('add') ? (
|
||||
<AddIcon color="success" />
|
||||
) : (
|
||||
<RemoveIcon color="error" />
|
||||
)}
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={`${step.action}: ${step.count}x ${step.template}`}
|
||||
secondary={`${step.provider} ${step.plan} | €${step.total_cost.toFixed(2)}/mo total`}
|
||||
primaryTypographyProps={{ variant: 'body2', fontWeight: 500 }}
|
||||
secondaryTypographyProps={{ variant: 'caption' }}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
|
||||
<Alert severity="warning" sx={{ mt: 3, bgcolor: alpha(theme.palette.warning.main, 0.1), border: `1px solid ${alpha(theme.palette.warning.main, 0.2)}` }}>
|
||||
New nodes will be immediately provisioned and linked to the gateway. Resource allocation may take up to 2 minutes per node.
|
||||
</Alert>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions sx={{ p: 2.5, pt: 0 }}>
|
||||
<Button onClick={onClose} disabled={loading} sx={{ color: 'text.secondary' }}>
|
||||
Back to Config
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={onConfirm}
|
||||
disabled={loading}
|
||||
sx={{
|
||||
px: 4,
|
||||
py: 1,
|
||||
fontWeight: 'bold',
|
||||
boxShadow: `0 8px 16px ${alpha(theme.palette.primary.main, 0.3)}`
|
||||
}}
|
||||
>
|
||||
{loading ? <CircularProgress size={24} color="inherit" /> : 'Confirm & Execute'}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user