fix: improve stress testing functionality and stop button behavior

This commit is contained in:
2025-04-09 02:46:24 +03:00
commit c88964275d
35 changed files with 15139 additions and 0 deletions

1
src/pages/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
node_modules/

1305
src/pages/Home.tsx Normal file

File diff suppressed because it is too large Load Diff

247
src/pages/Maintenance.tsx Normal file
View File

@@ -0,0 +1,247 @@
import { useState, useEffect } from 'react';
import { Box, Paper, Typography, Fade, useTheme, AppBar, Toolbar, Chip } from '@mui/material';
import Plot from 'react-plotly.js';
import { Layout, PlotData } from 'plotly.js';
interface DataItem {
now_timestamp: string;
future_timestamp: string;
power: string;
power_future_15min: string;
positive_3p: string;
negative_3p: string;
positive_7p: string;
negative_7p: string;
flag: string;
}
const Maintenance = () => {
const theme = useTheme();
const [chartData, setChartData] = useState<Partial<PlotData>[]>([]);
const [currentFlag, setCurrentFlag] = useState<string>('');
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch('http://141.196.83.136:8003/prom/get_chart_data/maintenance/20');
const result = await response.json();
if (result.data && result.data.length > 0) {
const last20Data = result.data.slice(-20);
setCurrentFlag(last20Data[last20Data.length - 1].flag);
const traces: Partial<PlotData>[] = [
{
x: last20Data.map((item: DataItem) => item.now_timestamp),
y: last20Data.map((item: DataItem) => parseFloat(item.power)),
type: 'scatter' as const,
mode: 'lines+markers' as const,
name: 'Current Power',
line: { color: '#2196f3', width: 2 },
marker: { size: 6, symbol: 'circle' }
},
{
x: last20Data.map((item: DataItem) => item.future_timestamp),
y: last20Data.map((item: DataItem) => parseFloat(item.power_future_15min)),
type: 'scatter' as const,
mode: 'lines+markers' as const,
name: 'Predicted (15min)',
line: { color: '#4caf50', width: 2, dash: 'dot' },
marker: { size: 6, symbol: 'circle' }
},
{
x: last20Data.map((item: DataItem) => item.future_timestamp),
y: last20Data.map((item: DataItem) => parseFloat(item.positive_3p)),
type: 'scatter' as const,
mode: 'lines' as const,
name: '+3% Positive',
line: { color: '#2ca02c', width: 1.5 },
showlegend: true,
},
{
x: last20Data.map((item: DataItem) => item.future_timestamp),
y: last20Data.map((item: DataItem) => parseFloat(item.negative_3p)),
type: 'scatter' as const,
mode: 'lines' as const,
name: '-3% Negative',
line: { color: '#d62728', width: 1.5 },
showlegend: true,
},
{
x: last20Data.map((item: DataItem) => item.future_timestamp),
y: last20Data.map((item: DataItem) => parseFloat(item.positive_7p)),
type: 'scatter' as const,
mode: 'lines' as const,
name: '+7% Positive',
line: { color: '#9467bd', width: 1.5 },
showlegend: true,
},
{
x: last20Data.map((item: DataItem) => item.future_timestamp),
y: last20Data.map((item: DataItem) => parseFloat(item.negative_7p)),
type: 'scatter' as const,
mode: 'lines' as const,
name: '-7% Negative',
line: { color: '#8c564b', width: 1.5 },
showlegend: true,
}
];
setChartData(traces);
}
} catch (error) {
console.error('Error fetching data:', error);
}
};
fetchData();
const interval = setInterval(fetchData, 5000);
return () => clearInterval(interval);
}, []);
const layout: Partial<Layout> = {
xaxis: {
title: {
text: 'Time',
font: { size: 14, color: '#666', family: undefined }
},
type: 'date',
gridcolor: '#eee',
tickfont: { size: 12, family: undefined, color: '#666' },
showgrid: true,
gridwidth: 1,
rangeslider: { visible: true }
},
yaxis: {
title: {
text: 'Power (W)',
font: { size: 14, color: '#666', family: undefined }
},
gridcolor: '#eee',
tickfont: { size: 12, family: undefined, color: '#666' },
showgrid: true,
gridwidth: 1,
rangemode: 'tozero' as const,
fixedrange: false,
range: [0, Math.max(...chartData.flatMap(trace => trace.y as number[]).filter(Boolean)) * 1.1]
},
showlegend: true,
legend: {
orientation: 'h',
y: -0.2,
x: 0.5,
xanchor: 'center',
yanchor: 'top',
font: {
size: 12,
family: theme.typography.fontFamily,
color: theme.palette.text.secondary
},
bgcolor: 'rgba(255, 255, 255, 0)',
bordercolor: 'rgba(255, 255, 255, 0)'
},
margin: { t: 60, b: 100, l: 60, r: 20 },
plot_bgcolor: 'rgba(0,0,0,0)',
paper_bgcolor: 'rgba(0,0,0,0)',
hovermode: 'closest',
modebar: {
bgcolor: 'rgba(255, 255, 255, 0)',
color: theme.palette.text.secondary,
activecolor: theme.palette.primary.main
}
};
return (
<Box sx={{ flexGrow: 1, bgcolor: theme.palette.background.default }}>
<AppBar
position="static"
elevation={0}
sx={{
bgcolor: 'background.paper',
borderBottom: `1px solid ${theme.palette.divider}`,
mb: 3
}}
>
<Toolbar sx={{ px: { xs: 2, sm: 4 } }}>
<Typography
variant="h5"
component="h1"
sx={{
color: 'text.primary',
fontWeight: 500,
flex: 1,
textAlign: 'center',
letterSpacing: '-0.5px'
}}
>
Preventive Maintenance
</Typography>
{currentFlag && (
<Chip
label={currentFlag}
color={currentFlag === 'Correct Estimation for PM energy' ? 'success' : 'warning'}
size="medium"
sx={{
height: 32,
'& .MuiChip-label': {
px: 2,
fontSize: '0.875rem',
fontWeight: 600
},
animation: 'pulse 2s infinite',
'@keyframes pulse': {
'0%': {
boxShadow: '0 0 0 0 rgba(0, 0, 0, 0.2)',
},
'70%': {
boxShadow: '0 0 0 6px rgba(0, 0, 0, 0)',
},
'100%': {
boxShadow: '0 0 0 0 rgba(0, 0, 0, 0)',
},
}
}}
/>
)}
</Toolbar>
</AppBar>
<Box sx={{ p: { xs: 2, sm: 4 } }}>
<Fade in timeout={800}>
<Paper
elevation={0}
sx={{
p: { xs: 2, sm: 3 },
bgcolor: 'background.paper',
borderRadius: 2,
border: `1px solid ${theme.palette.divider}`,
}}
>
<Box sx={{ height: 'calc(100vh - 200px)', minHeight: '500px' }}>
<Plot
data={chartData}
layout={layout}
config={{
responsive: true,
displayModeBar: true,
displaylogo: false,
modeBarButtonsToRemove: ['lasso2d', 'select2d'],
toImageButtonOptions: {
format: 'png',
filename: 'power_consumption_chart',
height: 1000,
width: 1500,
scale: 2
}
}}
style={{ width: '100%', height: '100%' }}
/>
</Box>
</Paper>
</Fade>
</Box>
</Box>
);
};
export default Maintenance;

778
src/pages/Migration.tsx Normal file
View File

@@ -0,0 +1,778 @@
import { useState, useEffect } from 'react';
import {
Box,
Paper,
Grid,
Typography,
Button,
useTheme,
Card,
CardContent,
IconButton,
CircularProgress,
Collapse,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
LinearProgress,
} from '@mui/material';
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
import TerminalIcon from '@mui/icons-material/Terminal';
import VisibilityIcon from '@mui/icons-material/Visibility';
import LockIcon from '@mui/icons-material/Lock';
import SummaryStats from '../components/Migration/SummaryStats';
import ResourceDistributionChart from '../components/Migration/ResourceDistributionChart';
import MigrationAdviceCard from '../components/Migration/MigrationAdviceCard';
import VerifiedMigration from '../components/Migration/VerifiedMigration';
import { useMigrationData, useGainAfterData } from '../components/Migration/hooks';
// Constants
const API_BASE_URL = 'http://141.196.83.136:8003';
const REFRESH_INTERVAL = 30000; // 30 seconds
interface VMPlacementData {
data_center: string;
id: number;
physical_machines: Array<{
status: 'blocked' | 'open';
name: string;
power_consumption: number;
vms: {
active: Array<{
status: 'blocked' | 'open';
name: string;
power: number;
confg: {
cpu: number;
ram: number;
disk: number;
};
}>;
inactive: Array<{
status: 'blocked' | 'open';
name: string;
power: number;
confg: {
cpu: number;
ram: number;
disk: number;
};
}>;
};
}>;
}
interface VMCardProps {
vm: {
name: string;
power: number;
status: 'blocked' | 'open';
confg: {
cpu: number;
ram: number;
disk: number;
};
};
vmId: string;
isActive: boolean;
expandedVMs: Record<string, boolean>;
toggleVMDetails: (vmId: string) => void;
theme: any;
}
const VMCard = ({ vm, vmId, isActive, expandedVMs, toggleVMDetails, theme }: VMCardProps) => (
<Box
sx={{
p: 1.5,
bgcolor: theme.palette.grey[50],
borderRadius: 1,
borderWidth: 1,
borderStyle: 'solid',
borderColor: theme.palette.grey[200],
opacity: isActive ? 1 : 0.7,
position: 'relative',
overflow: 'hidden'
}}
>
<Box sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
mb: 1
}}>
<Box sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
}}>
<Box
sx={{
width: 6,
height: 6,
borderRadius: '50%',
bgcolor: isActive ? theme.palette.success.main : theme.palette.error.main,
}}
/>
<Typography variant="subtitle2" sx={{
fontWeight: 'medium',
display: 'flex',
alignItems: 'center',
gap: 1
}}>
{vm.name}
{vm.status === 'blocked' && (
<LockIcon sx={{
fontSize: '0.875rem',
color: theme.palette.warning.main,
opacity: 0.8
}} />
)}
</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="caption" color="text.secondary">
{vm.power.toFixed(2)}W
</Typography>
<IconButton
size="small"
onClick={(e) => {
e.stopPropagation();
toggleVMDetails(vmId);
}}
>
<InfoOutlinedIcon fontSize="small" />
</IconButton>
</Box>
</Box>
<Collapse in={expandedVMs[vmId]}>
<Box sx={{
mt: 1,
p: 1.5,
bgcolor: 'white',
borderRadius: 1,
borderWidth: 1,
borderStyle: 'solid',
borderColor: theme.palette.grey[200],
}}>
<Grid container spacing={2}>
<Grid item xs={6}>
<Typography variant="caption" color="text.secondary" display="block">
Status
</Typography>
<Typography variant="body2" sx={{
display: 'flex',
alignItems: 'center',
gap: 0.5,
}}>
{vm.status === 'blocked' ? (
<>
<LockIcon sx={{
fontSize: '1rem',
color: theme.palette.warning.main
}} />
Blocked
</>
) : (
'Open'
)}
</Typography>
</Grid>
<Grid item xs={6}>
<Typography variant="caption" color="text.secondary" display="block">
CPU Cores
</Typography>
<Typography variant="body2">
{vm.confg.cpu}
</Typography>
</Grid>
<Grid item xs={6}>
<Typography variant="caption" color="text.secondary" display="block">
RAM
</Typography>
<Typography variant="body2">
{vm.confg.ram} GB
</Typography>
</Grid>
<Grid item xs={6}>
<Typography variant="caption" color="text.secondary" display="block">
Disk Size
</Typography>
<Typography variant="body2">
{vm.confg.disk} GB
</Typography>
</Grid>
<Grid item xs={6}>
<Typography variant="caption" color="text.secondary" display="block">
Power
</Typography>
<Typography variant="body2">
{vm.power.toFixed(2)}W
</Typography>
</Grid>
</Grid>
</Box>
</Collapse>
</Box>
);
const getMessageColor = (message: string, theme: any): string => {
if (message.includes('Error') || message.includes('BadRequestException')) {
return theme.palette.error.light;
} else if (message.includes('DEBUG')) {
return theme.palette.info.light;
} else if (message.includes('Attempting')) {
return theme.palette.warning.light;
} else if (message.includes('completed') || message.includes('Migration completed')) {
return theme.palette.success.light;
}
return 'white';
};
const MigrationProgress = ({ open, progress, onClose }: {
open: boolean;
progress: string[];
onClose: () => void;
}) => {
const theme = useTheme();
return (
<Dialog
open={open}
maxWidth="md"
fullWidth
PaperProps={{
sx: {
borderRadius: 2,
maxHeight: '80vh'
}
}}
>
<DialogTitle sx={{
borderBottom: 1,
borderColor: 'divider',
pb: 1,
display: 'flex',
alignItems: 'center',
gap: 2
}}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<TerminalIcon sx={{ color: theme.palette.primary.main }} />
<Typography>Migration Progress</Typography>
</Box>
{progress.length > 0 && (
<LinearProgress
sx={{
width: 100,
borderRadius: 1,
backgroundColor: theme.palette.grey[200]
}}
/>
)}
</DialogTitle>
<DialogContent sx={{
mt: 1,
bgcolor: theme.palette.grey[900],
p: 0
}}>
<Box sx={{
p: 3,
height: '100%',
maxHeight: '60vh',
overflowY: 'auto',
overflowX: 'hidden',
'&::-webkit-scrollbar': {
width: '8px',
},
'&::-webkit-scrollbar-track': {
background: theme.palette.grey[800],
},
'&::-webkit-scrollbar-thumb': {
background: theme.palette.grey[600],
borderRadius: '4px',
},
'&::-webkit-scrollbar-thumb:hover': {
background: theme.palette.grey[500],
},
}}>
{progress.length > 0 ? (
progress.map((message, index) => (
<Typography
key={index}
variant="body2"
sx={{
color: getMessageColor(message, theme),
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
mb: 1,
fontFamily: 'monospace',
fontSize: '0.875rem',
lineHeight: 1.5,
borderLeft: `3px solid ${getMessageColor(message, theme)}`,
pl: 2,
py: 0.5,
bgcolor: 'rgba(255, 255, 255, 0.03)',
borderRadius: '0 4px 4px 0',
transition: 'all 0.2s ease',
'&:hover': {
bgcolor: 'rgba(255, 255, 255, 0.05)',
}
}}
>
{message}
</Typography>
))
) : (
<Box sx={{
display: 'flex',
alignItems: 'center',
gap: 2,
color: 'white',
opacity: 0.7
}}>
<CircularProgress size={20} />
<Typography>Starting migration process...</Typography>
</Box>
)}
</Box>
</DialogContent>
<DialogActions sx={{
borderTop: 1,
borderColor: 'divider',
px: 3,
py: 2,
bgcolor: theme.palette.grey[900]
}}>
<Button
onClick={onClose}
variant="contained"
sx={{
minWidth: 100,
textTransform: 'none'
}}
>
Close
</Button>
</DialogActions>
</Dialog>
);
};
const Migration = () => {
const theme = useTheme();
// Essential states
const [vmPlacementData, setVmPlacementData] = useState<VMPlacementData | null>(null);
const [isLoadingVmPlacement, setIsLoadingVmPlacement] = useState(false);
const [expandedVMs, setExpandedVMs] = useState<Record<string, boolean>>({});
const [showVerifiedSection, setShowVerifiedSection] = useState(false);
const [isCardExpanded, setIsCardExpanded] = useState(false);
const [migrationMode] = useState<'auto' | 'semiauto'>('auto');
const [isProcessing, setIsProcessing] = useState(false);
const [migrationProgress, setMigrationProgress] = useState<string[]>([]);
const [showProgress, setShowProgress] = useState(false);
const [hasProgress, setHasProgress] = useState(false);
// Hooks for migration functionality
const { gainBeforeData, migrationAdviceData, isLoadingGainData, fetchMigrationData } = useMigrationData();
const { gainAfterData, isLoading: isLoadingGainAfter, fetchGainAfterData } = useGainAfterData();
// Essential functions
const toggleVMDetails = (vmId: string) => {
setExpandedVMs(prev => ({
...prev,
[vmId]: !prev[vmId]
}));
};
const fetchVmPlacementData = async () => {
try {
setIsLoadingVmPlacement(true);
const response = await fetch(`${API_BASE_URL}/prom/get_chart_data/vm_placement`);
if (!response.ok) {
throw new Error(`Failed to fetch VM placement data: ${response.status}`);
}
const data = await response.json();
console.log('Raw API response:', data); // Debug log
setVmPlacementData(data); // Use the data directly since it already has the correct structure
} catch (error) {
console.error('Error fetching VM placement data:', error);
} finally {
setIsLoadingVmPlacement(false);
}
};
const handleApproveMigration = async () => {
try {
setIsProcessing(true);
setMigrationProgress([]);
setHasProgress(true);
// First, send the POST request for migration approval
const approvalResponse = await fetch('http://141.196.83.136:8003/prom/migration/decisions4?run_migration=true', {
method: 'POST',
headers: {
'accept': 'application/json'
}
});
if (!approvalResponse.ok) {
throw new Error('Failed to approve migration');
}
const reader = approvalResponse.body?.getReader();
const decoder = new TextDecoder();
if (reader) {
while (true) {
const { value, done } = await reader.read();
if (done) break;
const text = decoder.decode(value);
const lines = text.split('\n').filter(line => line.trim());
setMigrationProgress(prev => [...prev, ...lines]);
}
}
// If approval is successful, show verified section and fetch gain after data
setShowVerifiedSection(true);
await fetchGainAfterData();
} catch (error) {
console.error('Error during migration approval:', error);
setMigrationProgress(prev => [...prev, `Error: ${error instanceof Error ? error.message : 'Unknown error occurred'}`]);
} finally {
setIsProcessing(false);
}
};
const handleDeclineMigration = async () => {
try {
setIsProcessing(true);
const response = await fetch('http://141.196.83.136:8003/prom/migration/decisions4?run_migration=false', {
method: 'POST',
headers: {
'accept': 'application/json'
}
});
if (!response.ok) {
throw new Error('Failed to decline migration');
}
// Hide verified section if it was shown
setShowVerifiedSection(false);
} catch (error) {
console.error('Error declining migration:', error);
} finally {
setIsProcessing(false);
}
};
// Data fetching effect
useEffect(() => {
console.log('Initial data fetch');
fetchVmPlacementData();
const intervalId = setInterval(() => {
console.log('Interval data fetch');
fetchVmPlacementData();
}, REFRESH_INTERVAL);
return () => clearInterval(intervalId);
}, []);
// Add effect to monitor vmPlacementData changes
useEffect(() => {
if (vmPlacementData) {
const blockedPMs = vmPlacementData.physical_machines.filter(pm => pm.status === 'blocked').length;
const blockedVMs = vmPlacementData.physical_machines.reduce((acc, pm) => {
const activeBlocked = pm.vms.active.filter(vm => vm.status === 'blocked').length;
const inactiveBlocked = pm.vms.inactive.filter(vm => vm.status === 'blocked').length;
return acc + activeBlocked + inactiveBlocked;
}, 0);
console.log('VM Placement Data updated:', {
timestamp: new Date().toISOString(),
pmCount: vmPlacementData.physical_machines.length,
blockedPMs,
totalVMs: vmPlacementData.physical_machines.reduce((acc, pm) =>
acc + pm.vms.active.length + pm.vms.inactive.length, 0
),
blockedVMs
});
}
}, [vmPlacementData]);
return (
<Box sx={{ display: 'flex', flexDirection: 'column', maxWidth: '100vw', minHeight: '100vh' }}>
<Box sx={{ p: { xs: 0.5, sm: 1 }, flexGrow: 1 }}>
<Grid container spacing={{ xs: 0.5, sm: 1 }}>
<SummaryStats />
<Grid item xs={12} container spacing={{ xs: 0.5, sm: 1 }}>
<ResourceDistributionChart
vmPlacementData={vmPlacementData}
isLoading={isLoadingVmPlacement}
onRefresh={fetchVmPlacementData}
/>
<MigrationAdviceCard
isCardExpanded={isCardExpanded}
setIsCardExpanded={setIsCardExpanded}
gainBeforeData={gainBeforeData}
migrationAdviceData={migrationAdviceData}
isLoadingGainData={isLoadingGainData}
migrationMode={migrationMode}
onRefresh={(e) => {
e.stopPropagation();
fetchMigrationData();
}}
/>
</Grid>
{/* Migration Action Buttons */}
<Grid item xs={12}>
<Box sx={{
display: 'flex',
justifyContent: 'flex-end',
gap: 2,
mt: 2,
mb: 1,
alignItems: 'center'
}}>
{hasProgress && (
<Button
variant="outlined"
onClick={() => setShowProgress(true)}
startIcon={<VisibilityIcon />}
sx={{
py: 1.5,
borderColor: theme.palette.grey[300],
color: theme.palette.text.secondary,
'&:hover': {
borderColor: theme.palette.grey[400],
bgcolor: 'rgba(0, 0, 0, 0.02)'
}
}}
>
Show Progress
</Button>
)}
<Button
variant="contained"
color="error"
onClick={handleDeclineMigration}
disabled={isProcessing}
sx={{
py: 1.5,
px: 4,
borderRadius: 1,
textTransform: 'none',
fontSize: '1rem',
minWidth: 200
}}
>
{isProcessing ? (
<CircularProgress size={24} color="inherit" />
) : (
'Decline Migration'
)}
</Button>
<Button
variant="contained"
startIcon={!isProcessing && <PowerSettingsNewIcon />}
onClick={handleApproveMigration}
disabled={isProcessing}
sx={{
py: 1.5,
px: 4,
bgcolor: theme.palette.success.main,
'&:hover': { bgcolor: theme.palette.success.dark },
borderRadius: 1,
textTransform: 'none',
fontSize: '1rem',
minWidth: 200
}}
>
{isProcessing ? (
<CircularProgress size={24} color="inherit" />
) : (
'Approve Migration'
)}
</Button>
</Box>
</Grid>
{/* Verified Migration Section */}
{showVerifiedSection && (
<Grid item xs={12}>
<VerifiedMigration
gainAfterData={gainAfterData}
isLoading={isLoadingGainAfter}
/>
</Grid>
)}
{/* PM & VM Monitoring Section */}
<Grid item xs={12}>
<Paper sx={{ p: 2, bgcolor: 'background.paper', boxShadow: 3 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="h6">PMs & VMs Monitoring</Typography>
</Box>
{isLoadingVmPlacement ? (
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
<CircularProgress />
</Box>
) : vmPlacementData?.physical_machines ? (
<Grid container spacing={2}>
{vmPlacementData.physical_machines.map((pm) => (
<Grid item xs={12} sm={6} md={4} key={pm.name}>
<Card
sx={{
borderRadius: 2,
boxShadow: 2,
height: '100%',
minHeight: 250,
display: 'flex',
flexDirection: 'column',
position: 'relative',
overflow: 'hidden',
borderWidth: 1,
borderStyle: 'solid',
borderColor: pm.status === 'blocked' ? theme.palette.warning.light : 'transparent',
'&::before': pm.status === 'blocked' ? {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: '2px',
bgcolor: theme.palette.warning.main
} : undefined
}}
>
<CardContent sx={{
flex: 1,
display: 'flex',
flexDirection: 'column',
}}>
<Box sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
mb: 2,
pb: 1,
borderBottom: 1,
borderColor: 'divider'
}}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography
variant="subtitle1"
sx={{
fontWeight: 'bold',
color: theme.palette.primary.main,
display: 'flex',
alignItems: 'center',
gap: 1
}}
>
{pm.name}
{pm.status === 'blocked' && (
<LockIcon sx={{
fontSize: '1rem',
color: theme.palette.warning.main,
opacity: 0.8
}} />
)}
</Typography>
</Box>
<Typography
variant="caption"
color="text.secondary"
>
{pm.power_consumption.toFixed(2)}W
</Typography>
</Box>
<Box sx={{
flex: 1,
overflowY: 'auto',
display: 'flex',
flexDirection: 'column',
gap: 1
}}>
{/* Active VMs */}
{pm.vms.active.map((vm, index) => (
<VMCard
key={`${pm.name}-${vm.name}-active-${index}`}
vm={vm}
vmId={`${pm.name}-${vm.name}-active-${index}`}
isActive={true}
expandedVMs={expandedVMs}
toggleVMDetails={toggleVMDetails}
theme={theme}
/>
))}
{/* Inactive VMs */}
{pm.vms.inactive.map((vm, index) => (
<VMCard
key={`${pm.name}-${vm.name}-inactive-${index}`}
vm={vm}
vmId={`${pm.name}-${vm.name}-inactive-${index}`}
isActive={false}
expandedVMs={expandedVMs}
toggleVMDetails={toggleVMDetails}
theme={theme}
/>
))}
{pm.vms.active.length === 0 && pm.vms.inactive.length === 0 && (
<Box sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100%',
color: theme.palette.text.secondary,
}}>
<Typography variant="body2">No VMs running</Typography>
</Box>
)}
</Box>
</CardContent>
</Card>
</Grid>
))}
</Grid>
) : (
<Box sx={{
display: 'flex',
justifyContent: 'center',
p: 3,
color: theme.palette.text.secondary
}}>
<Typography>No monitoring data available</Typography>
</Box>
)}
</Paper>
</Grid>
</Grid>
</Box>
<MigrationProgress
open={showProgress}
progress={migrationProgress}
onClose={() => setShowProgress(false)}
/>
</Box>
);
};
export default Migration;

View File

@@ -0,0 +1,600 @@
import { useState, useEffect, useCallback } from 'react';
import {
Box,
Paper,
Typography,
IconButton,
Switch,
AppBar,
Toolbar,
Button,
CircularProgress,
Tooltip,
Collapse,
Grid,
useTheme,
styled,
Chip,
FormControl,
InputLabel,
Select,
MenuItem,
FormControlLabel,
Alert,
Snackbar,
Tabs,
Tab,
} from '@mui/material';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
import BusinessIcon from '@mui/icons-material/Business';
import LocationOnIcon from '@mui/icons-material/LocationOn';
import DnsIcon from '@mui/icons-material/Dns';
import ComputerIcon from '@mui/icons-material/Computer';
import MemoryIcon from '@mui/icons-material/Memory';
import RefreshIcon from '@mui/icons-material/Refresh';
import SaveIcon from '@mui/icons-material/Save';
import SpeedIcon from '@mui/icons-material/Speed';
import { stressService } from '../services/stressService';
// Define the structure of our tree nodes
interface TreeNode {
id: string;
name: string;
type: 'organization' | 'region' | 'datacenter' | 'pm' | 'vm' | 'compute';
children?: TreeNode[];
ip?: string;
}
interface ComputeNode {
host_ip: string;
hosted_vms: Record<string, string>;
}
interface ApiResponse {
optimization_space: Record<string, ComputeNode>;
}
// Helper function to get all descendant node IDs
const getDescendantIds = (node: TreeNode): string[] => {
let ids: string[] = [node.id];
if (node.children) {
node.children.forEach(child => {
ids = [...ids, ...getDescendantIds(child)];
});
}
return ids;
};
// Helper function to get all ancestor node IDs
const getAncestorIds = (nodeId: string, node: TreeNode): string[] => {
if (!node) return [];
if (node.id === nodeId) return [node.id];
if (node.children) {
for (const child of node.children) {
const path = getAncestorIds(nodeId, child);
if (path.length > 0) {
return [node.id, ...path];
}
}
}
return [];
};
// Helper function to check if all children are selected
const areAllChildrenSelected = (node: TreeNode, selectedNodes: string[]): boolean => {
if (!node.children) return true;
return node.children.every(child => {
if (child.children) {
return areAllChildrenSelected(child, selectedNodes);
}
return selectedNodes.includes(child.id);
});
};
// Add new styled components for stress testing
const StressTestingCard = styled(Paper)(({ theme }) => ({
padding: theme.spacing(3),
borderRadius: theme.spacing(2),
backgroundColor: theme.palette.background.paper,
marginBottom: theme.spacing(3),
}));
const StressLevelChip = styled(Chip)<{ level: 'low' | 'medium' | 'high' }>(({ theme, level }) => ({
borderRadius: theme.spacing(1),
fontWeight: 500,
backgroundColor:
level === 'low' ? theme.palette.success.light :
level === 'medium' ? theme.palette.warning.light :
theme.palette.error.light,
color:
level === 'low' ? theme.palette.success.dark :
level === 'medium' ? theme.palette.warning.dark :
theme.palette.error.dark,
}));
interface MonitoringSystemProps {
onSave?: (unselectedVMs: string[], selectedVMs: string[]) => void;
isDialog?: boolean;
initialBlockList?: string[];
initialSelectedVMs?: string[];
}
const MonitoringSystem: React.FC<MonitoringSystemProps> = ({
onSave,
isDialog = false,
initialBlockList = [],
initialSelectedVMs = [],
}) => {
const [expanded, setExpanded] = useState<string[]>(['org-main', 'region-main', 'dc-old-lab']);
const [selectedNodes, setSelectedNodes] = useState<string[]>([]);
const [loading, setLoading] = useState(false);
const [treeData, setTreeData] = useState<TreeNode[]>([]);
const [isViewMode, setIsViewMode] = useState(false);
const [selectedVMs, setSelectedVMs] = useState<string[]>(initialSelectedVMs);
const [activeTab, setActiveTab] = useState(0);
const [alert, setAlert] = useState<{ open: boolean; message: string; severity: 'success' | 'error' | 'info' }>({
open: false,
message: '',
severity: 'info',
});
// Fetch data and initialize state
const fetchData = async () => {
setLoading(true);
try {
const response = await fetch('http://141.196.83.136:8003/prom/monitoring');
const result: ApiResponse = await response.json();
// Create hierarchical structure
const hierarchicalData: TreeNode[] = [
{
id: 'org-main',
name: 'Main Organization',
type: 'organization',
children: [
{
id: 'region-main',
name: 'Region',
type: 'region',
children: [
{
id: 'dc-ulak',
name: 'Ulak',
type: 'datacenter',
children: [] // Empty for now
},
{
id: 'dc-old-lab',
name: 'Old Lab',
type: 'datacenter',
children: Object.entries(result.optimization_space).map(([computeName, computeData]) => ({
id: computeName,
name: computeName,
type: 'compute',
ip: computeData.host_ip,
children: Object.entries(computeData.hosted_vms).map(([vmName, vmIp]) => ({
id: `${computeName}-${vmName}`,
name: vmName,
type: 'vm',
ip: vmIp
}))
}))
},
{
id: 'dc-new-lab',
name: 'New Lab',
type: 'datacenter',
children: [] // Empty for now
}
]
}
]
}
];
setTreeData(hierarchicalData);
// Initialize selection based on initial values only if they exist
if (initialBlockList.length > 0 || initialSelectedVMs.length > 0) {
const blockList = initialBlockList;
// Select nodes that are not in the block list
const nodesToSelect = new Set<string>();
// Helper function to process compute nodes
const processComputeNodes = (nodes: TreeNode[]) => {
nodes.forEach(node => {
if (node.type === 'compute') {
let hasSelectedVM = false;
// Check compute node
if (node.ip && !blockList.includes(node.ip)) {
nodesToSelect.add(node.id);
}
// Check VM nodes
node.children?.forEach(vm => {
if (vm.ip && !blockList.includes(vm.ip)) {
nodesToSelect.add(vm.id);
hasSelectedVM = true;
}
});
// If any VM is selected, ensure the compute is selected too
if (hasSelectedVM) {
nodesToSelect.add(node.id);
}
}
// Recursively process children
if (node.children) {
processComputeNodes(node.children);
}
});
};
// Process all nodes
processComputeNodes(hierarchicalData);
// Set the selected nodes
setSelectedNodes(Array.from(nodesToSelect));
}
// Expand organization and region nodes by default
setExpanded(['org-main', 'region-main', 'dc-old-lab']);
} catch (error) {
console.error('Error fetching data:', error);
} finally {
setLoading(false);
}
};
// Initialize with previous state
useEffect(() => {
fetchData();
}, [initialBlockList, initialSelectedVMs]); // Re-fetch when initial values change
// Get appropriate icon for each node type
const getNodeIcon = (type: TreeNode['type']) => {
switch (type) {
case 'organization':
return <BusinessIcon color="primary" />;
case 'region':
return <LocationOnIcon color="primary" />;
case 'datacenter':
return <DnsIcon color="primary" />;
case 'pm':
return <ComputerIcon color="primary" />;
case 'vm':
return <MemoryIcon color="primary" />;
default:
return null;
}
};
// Handle node expansion
const handleNodeToggle = (nodeId: string) => {
setExpanded(prev => {
const isExpanded = prev.includes(nodeId);
if (isExpanded) {
return prev.filter(id => id !== nodeId);
} else {
return [...prev, nodeId];
}
});
};
// Updated node selection handler for toggle-like selection with parent-child association
const handleNodeSelect = (nodeId: string) => {
setSelectedNodes(prev => {
const isSelected = prev.includes(nodeId);
let newSelected = [...prev];
// Find the node in the tree
const findNode = (nodes: TreeNode[]): TreeNode | null => {
for (const node of nodes) {
if (node.id === nodeId) return node;
if (node.children) {
const found = findNode(node.children);
if (found) return found;
}
}
return null;
};
// Find the parent compute node for a VM
const findParentCompute = (nodes: TreeNode[], vmId: string): TreeNode | null => {
for (const node of nodes) {
if (node.type === 'compute' && node.children?.some(vm => vm.id === vmId)) {
return node;
}
if (node.children) {
const found = findParentCompute(node.children, vmId);
if (found) return found;
}
}
return null;
};
const targetNode = findNode(treeData);
if (!targetNode) return prev;
if (isSelected) {
// When deselecting a node
if (targetNode.type === 'compute') {
// If deselecting a compute, deselect all its VMs
const computeAndVMs = [targetNode.id, ...(targetNode.children?.map(vm => vm.id) || [])];
newSelected = newSelected.filter(id => !computeAndVMs.includes(id));
} else if (targetNode.type === 'vm') {
// If deselecting a VM, just deselect it
newSelected = newSelected.filter(id => id !== nodeId);
// If this was the last VM, deselect the parent compute too
const parentCompute = findParentCompute(treeData, nodeId);
if (parentCompute) {
const siblingVMs = parentCompute.children?.filter(vm => vm.id !== nodeId) || [];
const hasSelectedSiblings = siblingVMs.some(vm => newSelected.includes(vm.id));
if (!hasSelectedSiblings) {
newSelected = newSelected.filter(id => id !== parentCompute.id);
}
}
}
} else {
// When selecting a node
if (targetNode.type === 'compute') {
// If selecting a compute, select all its VMs
newSelected.push(targetNode.id);
targetNode.children?.forEach(vm => {
newSelected.push(vm.id);
});
} else if (targetNode.type === 'vm') {
// If selecting a VM, select it and its parent compute
newSelected.push(nodeId);
const parentCompute = findParentCompute(treeData, nodeId);
if (parentCompute) {
newSelected.push(parentCompute.id);
}
}
}
// Remove duplicates and return
return Array.from(new Set(newSelected));
});
};
// Updated render function with disabled switches in view mode
const renderTreeNode = (node: TreeNode, level: number = 0) => {
const isExpanded = expanded.includes(node.id);
const hasChildren = node.children && node.children.length > 0;
const isSelected = selectedNodes.includes(node.id);
return (
<Box key={node.id} sx={{ ml: level * 3 }}>
<Box
sx={{
display: 'flex',
alignItems: 'center',
p: 0.5,
'&:hover': { bgcolor: 'action.hover' },
borderRadius: 1,
}}
>
{hasChildren && (
<IconButton
size="small"
onClick={() => handleNodeToggle(node.id)}
sx={{ mr: 1 }}
>
{isExpanded ? <ExpandMoreIcon /> : <ChevronRightIcon />}
</IconButton>
)}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{getNodeIcon(node.type)}
<Typography variant="body2">{node.name}</Typography>
<Switch
checked={isSelected}
onChange={() => handleNodeSelect(node.id)}
disabled={isViewMode}
size="small"
sx={{
ml: 1,
'& .MuiSwitch-switchBase.Mui-checked': {
color: '#4caf50',
},
'& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': {
backgroundColor: '#4caf50',
},
'& .MuiSwitch-track': {
backgroundColor: '#bdbdbd',
},
}}
/>
</Box>
</Box>
{hasChildren && (
<Collapse in={isExpanded}>
<Box>
{node.children!.map(child => renderTreeNode(child, level + 1))}
</Box>
</Collapse>
)}
</Box>
);
};
// Get unselected and selected VMs including compute IPs
const getVMSelectionStatus = () => {
const allIPs: string[] = [];
const selectedIPs: string[] = [];
// Find the Old Lab datacenter node that contains the dynamic data
const oldLabNode = treeData[0]?.children?.[0]?.children?.find(node => node.id === 'dc-old-lab');
if (!oldLabNode) return { selectedVMs: [], unselectedVMs: [] };
// Process only the compute nodes in Old Lab
oldLabNode.children?.forEach(compute => {
// Add compute IP
if (compute.ip) {
allIPs.push(compute.ip);
if (selectedNodes.includes(compute.id)) {
selectedIPs.push(compute.ip);
}
}
// Add VM IPs
compute.children?.forEach(vm => {
if (vm.ip) {
allIPs.push(vm.ip);
if (selectedNodes.includes(vm.id)) {
selectedIPs.push(vm.ip);
}
}
});
});
// Calculate unselected IPs for block list
const unselectedIPs = allIPs.filter(ip => !selectedIPs.includes(ip));
console.log('Block list IPs:', unselectedIPs);
return {
selectedVMs: selectedIPs,
unselectedVMs: unselectedIPs
};
};
// Handle save action
const handleSave = async () => {
try {
setLoading(true);
const { selectedVMs, unselectedVMs } = getVMSelectionStatus();
console.log('Selected VMs and Computes:', selectedVMs);
console.log('Unselected VMs and Computes:', unselectedVMs);
// Store selected VMs in localStorage for stress testing
const oldLabNode = treeData[0]?.children?.[0]?.children?.find(node => node.id === 'dc-old-lab');
if (oldLabNode) {
const selectedVMObjects = oldLabNode.children?.flatMap(compute =>
compute.children?.filter(vm => selectedNodes.includes(vm.id))
.map(vm => ({ id: vm.id, name: vm.name, ip: vm.ip })) || []
) || [];
console.log('Storing VMs in localStorage:', selectedVMObjects);
localStorage.setItem('stressTestVMs', JSON.stringify(selectedVMObjects));
}
if (onSave) {
onSave(unselectedVMs, selectedVMs);
}
} catch (error) {
console.error('Error saving selection:', error);
} finally {
setLoading(false);
}
};
return (
<Box sx={{
height: isDialog ? 'auto' : '100vh',
bgcolor: 'background.default',
p: 3
}}>
{/* Header */}
<AppBar
position="static"
elevation={0}
sx={{
bgcolor: 'background.paper',
borderBottom: 1,
borderColor: 'divider'
}}
>
<Toolbar sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Typography variant="h5" color="textPrimary" sx={{ flex: 1 }}>
Optimization Space Selection
</Typography>
{isDialog && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mr: 2 }}>
<Button
variant={isViewMode ? "contained" : "outlined"}
onClick={() => setIsViewMode(true)}
size="small"
>
View
</Button>
<Button
variant={!isViewMode ? "contained" : "outlined"}
onClick={() => setIsViewMode(false)}
size="small"
>
Edit
</Button>
</Box>
)}
<Tooltip title="Save selected nodes">
<span>
<Button
startIcon={<SaveIcon />}
variant="contained"
onClick={handleSave}
disabled={loading || isViewMode}
sx={{ mr: 1 }}
>
Save
</Button>
</span>
</Tooltip>
<Tooltip title="Refresh tree">
<IconButton onClick={fetchData} disabled={loading}>
{loading ? <CircularProgress size={24} /> : <RefreshIcon />}
</IconButton>
</Tooltip>
</Toolbar>
</AppBar>
{/* Main Content */}
<Paper
elevation={0}
sx={{
p: 2,
height: '100%',
border: 1,
borderColor: 'divider',
borderRadius: 2,
bgcolor: 'background.paper',
mt: 2
}}
>
{loading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%' }}>
<CircularProgress />
</Box>
) : (
<Box sx={{ mt: 1 }}>
{treeData.map(node => renderTreeNode(node))}
</Box>
)}
</Paper>
<Snackbar
open={alert.open}
autoHideDuration={6000}
onClose={() => setAlert({ ...alert, open: false })}
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
>
<Alert
onClose={() => setAlert({ ...alert, open: false })}
severity={alert.severity}
variant="filled"
sx={{ width: '100%' }}
>
{alert.message}
</Alert>
</Snackbar>
</Box>
);
};
export default MonitoringSystem;

478
src/pages/StressTesting.tsx Normal file
View File

@@ -0,0 +1,478 @@
import { useState, useEffect } from 'react';
import {
Box,
Paper,
Typography,
Button,
CircularProgress,
Grid,
FormControl,
InputLabel,
Select,
MenuItem,
FormControlLabel,
Switch,
Alert,
Snackbar,
Breadcrumbs,
Link,
Stepper,
Step,
StepLabel,
StepContent,
} from '@mui/material';
import SpeedIcon from '@mui/icons-material/Speed';
import HomeIcon from '@mui/icons-material/Home';
import { stressService } from '../services/stressService';
// Define the structure of our VM nodes
interface VMNode {
id: string;
name: string;
ip: string;
}
// Remove the props interface since we'll get VMs from localStorage
// interface StressTestingProps {
// selectedVMs: VMNode[];
// }
// Update component to not require props
const StressTesting: React.FC = () => {
// Initialize with empty array, will be populated from localStorage
const [selectedVMs, setSelectedVMs] = useState<VMNode[]>([]);
const [activeStep, setActiveStep] = useState(0);
const [stressLevel, setStressLevel] = useState<'low' | 'medium' | 'high'>('low');
const [stressTestVMs, setStressTestVMs] = useState<string[]>([]);
const [isStressTesting, setIsStressTesting] = useState(false);
const [isLoadingStress, setIsLoadingStress] = useState(false);
const [stressedVMs, setStressedVMs] = useState<string[]>([]);
const [alert, setAlert] = useState<{ open: boolean; message: string; severity: 'success' | 'error' | 'info' }>({
open: false,
message: '',
severity: 'info',
});
// Load selected VMs from localStorage
useEffect(() => {
const loadVMsFromStorage = () => {
const storedVMs = localStorage.getItem('stressTestVMs');
console.log('Loading VMs from storage:', storedVMs);
if (storedVMs) {
try {
const parsedVMs = JSON.parse(storedVMs);
console.log('Parsed VMs:', parsedVMs);
setSelectedVMs(parsedVMs);
} catch (error) {
console.error('Error parsing stored VMs:', error);
setAlert({
open: true,
message: 'Error loading VMs from storage. Please select VMs in the Monitoring page first.',
severity: 'error',
});
}
} else {
console.log('No VMs found in storage');
setAlert({
open: true,
message: 'No VMs found. Please select VMs in the Monitoring page first.',
severity: 'info',
});
}
};
// Load VMs initially
loadVMsFromStorage();
// Set up event listener for storage changes
const handleStorageChange = (e: StorageEvent) => {
if (e.key === 'stressTestVMs') {
loadVMsFromStorage();
}
};
// Add event listener
window.addEventListener('storage', handleStorageChange);
return () => {
window.removeEventListener('storage', handleStorageChange);
};
}, []); // Empty dependency array since we only want this to run once on mount
// Add status polling for stress test
useEffect(() => {
let interval: NodeJS.Timeout;
const pollStressStatus = async () => {
try {
// Get the currently selected VMs from the tree view
const selectedVMList = selectedVMs.filter(vm => stressTestVMs.includes(vm.id));
const vmIPs = selectedVMList.map(vm => vm.ip);
console.log('Polling stress status for VMs:', vmIPs);
if (vmIPs.length > 0) {
const status = await stressService.getStressStatus(vmIPs);
console.log('Stress status response:', status);
setStressedVMs(status);
// Only update isStressTesting if we're not already in a stress testing state
if (!isStressTesting) {
setIsStressTesting(status.length > 0);
}
} else {
setStressedVMs([]);
setIsStressTesting(false);
}
} catch (error) {
console.error('Error polling stress status:', error);
setStressedVMs([]);
// Don't automatically set isStressTesting to false on error
// This allows the stop button to remain enabled even if there's a temporary polling error
}
};
// Start polling immediately and then every 5 seconds
pollStressStatus();
interval = setInterval(pollStressStatus, 5000);
return () => {
if (interval) {
clearInterval(interval);
}
};
}, [selectedVMs, stressTestVMs, isStressTesting]); // Added isStressTesting to dependencies
// Handle VM selection for stress testing
const handleStressTestVMSelection = (vmId: string) => {
setStressTestVMs(prev => {
if (prev.includes(vmId)) {
return prev.filter(id => id !== vmId);
} else {
return [...prev, vmId];
}
});
};
// Handle next step
const handleNext = () => {
setActiveStep((prevActiveStep) => prevActiveStep + 1);
};
// Handle back step
const handleBack = () => {
setActiveStep((prevActiveStep) => prevActiveStep - 1);
};
// Handle reset
const handleReset = () => {
setActiveStep(0);
setStressTestVMs([]);
};
// Start stress test
const handleStartStressTest = async () => {
try {
setIsLoadingStress(true);
// Get the VMs selected for stress testing
const selectedVMList = selectedVMs.filter(vm => stressTestVMs.includes(vm.id));
const vmIPs = selectedVMList.map(vm => vm.ip);
// Log the selected VMs for debugging
console.log('Selected VMs for stress test:', vmIPs);
if (vmIPs.length === 0) {
setAlert({
open: true,
message: 'Please select at least one VM to stress test',
severity: 'error',
});
return;
}
await stressService.startStressTest({
vms: vmIPs,
level: stressLevel,
force: true,
});
setIsStressTesting(true);
setAlert({
open: true,
message: 'Stress test started successfully',
severity: 'success',
});
} catch (error) {
console.error('Error in handleStartStressTest:', error);
setAlert({
open: true,
message: error instanceof Error ? error.message : 'Failed to start stress test',
severity: 'error',
});
} finally {
setIsLoadingStress(false);
}
};
// Stop stress test
const handleStopStressTest = async () => {
try {
setIsLoadingStress(true);
// Get the VMs selected for stress testing
const selectedVMList = selectedVMs.filter(vm => stressTestVMs.includes(vm.id));
const vmIPs = selectedVMList.map(vm => vm.ip);
console.log('Stopping stress test for VMs:', vmIPs);
await stressService.stopStressTest(vmIPs);
setIsStressTesting(false);
setStressedVMs([]);
setAlert({
open: true,
message: 'Stress test stopped successfully',
severity: 'success',
});
} catch (error) {
console.error('Error in handleStopStressTest:', error);
setAlert({
open: true,
message: error instanceof Error ? error.message : 'Failed to stop stress test',
severity: 'error',
});
} finally {
setIsLoadingStress(false);
}
};
// Steps for the stepper
const steps = [
{
label: 'Select VMs',
description: 'Select the VMs you want to include in the stress test.',
content: (
<Box sx={{ mt: 2 }}>
<Paper
variant="outlined"
sx={{
p: 2,
maxHeight: '300px',
overflow: 'auto',
bgcolor: 'background.default'
}}
>
{selectedVMs.length === 0 ? (
<Typography variant="body2" color="text.secondary" align="center">
No VMs found. Please select VMs in the Monitoring page first.
</Typography>
) : (
<Grid container spacing={1}>
{selectedVMs.map((vm) => (
<Grid item xs={12} sm={6} md={4} key={vm.id}>
<FormControlLabel
control={
<Switch
checked={stressTestVMs.includes(vm.id)}
onChange={() => handleStressTestVMSelection(vm.id)}
disabled={isStressTesting || isLoadingStress}
size="small"
/>
}
label={
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
<Typography variant="body2">{vm.name}</Typography>
<Typography variant="caption" color="text.secondary">
{vm.ip}
</Typography>
</Box>
}
/>
</Grid>
))}
</Grid>
)}
</Paper>
</Box>
),
},
{
label: 'Configure Stress Level',
description: 'Select the stress level for the test.',
content: (
<Box sx={{ mt: 2 }}>
<FormControl fullWidth>
<InputLabel>Stress Level</InputLabel>
<Select
value={stressLevel}
label="Stress Level"
onChange={(e) => setStressLevel(e.target.value as 'low' | 'medium' | 'high')}
disabled={isStressTesting || isLoadingStress}
>
<MenuItem value="low">Low</MenuItem>
<MenuItem value="medium">Medium</MenuItem>
<MenuItem value="high">High</MenuItem>
</Select>
</FormControl>
<Box sx={{ mt: 2 }}>
<Typography variant="body2" color="text.secondary">
<strong>Low:</strong> Minimal stress, suitable for testing basic functionality.
</Typography>
<Typography variant="body2" color="text.secondary">
<strong>Medium:</strong> Moderate stress, tests system under typical load conditions.
</Typography>
<Typography variant="body2" color="text.secondary">
<strong>High:</strong> Maximum stress, tests system under extreme conditions.
</Typography>
</Box>
</Box>
),
},
{
label: 'Run Stress Test',
description: 'Start or stop the stress test.',
content: (
<Box sx={{ mt: 2 }}>
<Box sx={{ display: 'flex', gap: 2, mb: 3 }}>
<Button
variant="contained"
color="primary"
onClick={handleStartStressTest}
disabled={isStressTesting || isLoadingStress || stressTestVMs.length === 0}
fullWidth
>
{isLoadingStress ? <CircularProgress size={24} /> : 'Start Stress Test'}
</Button>
<Button
variant="contained"
color="error"
onClick={handleStopStressTest}
disabled={(!isStressTesting && stressedVMs.length === 0) || isLoadingStress}
fullWidth
>
{isLoadingStress ? <CircularProgress size={24} /> : 'Stop Stress Test'}
</Button>
</Box>
{stressedVMs.length > 0 && (
<Box sx={{ mt: 3 }}>
<Typography variant="subtitle2" sx={{ mb: 1 }}>Currently Stressed VMs:</Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
{stressedVMs.map((vm) => (
<Paper
key={vm}
sx={{
p: 1,
borderRadius: 1,
bgcolor:
stressLevel === 'low' ? 'success.light' :
stressLevel === 'medium' ? 'warning.light' :
'error.light',
color:
stressLevel === 'low' ? 'success.dark' :
stressLevel === 'medium' ? 'warning.dark' :
'error.dark',
}}
>
<Typography variant="body2">{vm}</Typography>
</Paper>
))}
</Box>
</Box>
)}
</Box>
),
},
];
return (
<Box sx={{ p: 3 }}>
{/* Breadcrumbs */}
<Breadcrumbs sx={{ mb: 3 }}>
<Link href="/" color="inherit" sx={{ display: 'flex', alignItems: 'center' }}>
<HomeIcon sx={{ mr: 0.5 }} fontSize="inherit" />
Home
</Link>
<Link href="/monitoring" color="inherit">
Monitoring
</Link>
<Typography color="text.primary" sx={{ display: 'flex', alignItems: 'center' }}>
<SpeedIcon sx={{ mr: 0.5 }} fontSize="inherit" />
Stress Testing
</Typography>
</Breadcrumbs>
{/* Header */}
<Box sx={{ display: 'flex', alignItems: 'center', mb: 4 }}>
<SpeedIcon color="primary" sx={{ mr: 1, fontSize: '2rem' }} />
<Typography variant="h5">Stress Testing</Typography>
<Button
sx={{ ml: 'auto' }}
onClick={() => {
const storedVMs = localStorage.getItem('stressTestVMs');
console.log('Current localStorage contents:', storedVMs);
setAlert({
open: true,
message: storedVMs ? `Found VMs in storage: ${storedVMs}` : 'No VMs in storage',
severity: 'info'
});
}}
>
Debug Storage
</Button>
</Box>
{/* Stepper */}
<Paper sx={{ p: 3, mb: 3 }}>
<Stepper activeStep={activeStep} orientation="vertical">
{steps.map((step, index) => (
<Step key={step.label}>
<StepLabel>{step.label}</StepLabel>
<StepContent>
<Typography>{step.description}</Typography>
{step.content}
<Box sx={{ mb: 2, mt: 2 }}>
<div>
<Button
variant="contained"
onClick={index === steps.length - 1 ? handleReset : handleNext}
sx={{ mt: 1, mr: 1 }}
disabled={index === 0 && stressTestVMs.length === 0}
>
{index === steps.length - 1 ? 'Reset' : 'Continue'}
</Button>
<Button
disabled={index === 0}
onClick={handleBack}
sx={{ mt: 1, mr: 1 }}
>
Back
</Button>
</div>
</Box>
</StepContent>
</Step>
))}
</Stepper>
</Paper>
{/* Alert Snackbar */}
<Snackbar
open={alert.open}
autoHideDuration={6000}
onClose={() => setAlert({ ...alert, open: false })}
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
>
<Alert
onClose={() => setAlert({ ...alert, open: false })}
severity={alert.severity}
variant="filled"
sx={{ width: '100%' }}
>
{alert.message}
</Alert>
</Snackbar>
</Box>
);
};
export default StressTesting;

667
src/pages/Temperature.tsx Normal file
View File

@@ -0,0 +1,667 @@
import { useEffect, useState, useCallback, useRef } from 'react';
import { Box, Paper, Typography, Fade, useTheme, Grid, AppBar, Toolbar, CircularProgress, IconButton, Tooltip, Chip, Button, Snackbar, Alert } from '@mui/material';
import RefreshIcon from '@mui/icons-material/Refresh';
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import CancelIcon from '@mui/icons-material/Cancel';
import Plot from 'react-plotly.js';
import { Layout, PlotData, Config } from 'plotly.js';
// Extend the Window interface to include Google Charts
declare global {
interface Window {
google?: {
charts?: any;
};
}
}
// Define the structure of our data
interface ChartData {
power: string;
flag: string;
env_temp_cur: string;
now_timestamp: string;
future_timestamp: string;
env_temp_min: string;
power_future_min: string;
}
const Temperature = () => {
const theme = useTheme();
const [data, setData] = useState<ChartData[]>([]);
const [loading, setLoading] = useState(true);
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
const [refreshing, setRefreshing] = useState(false);
const [decisionLoading, setDecisionLoading] = useState(false);
const [powerZoom, setPowerZoom] = useState<{xRange?: [number, number]; yRange?: [number, number]}>({});
const [tempZoom, setTempZoom] = useState<{xRange?: [number, number]; yRange?: [number, number]}>({});
const [alert, setAlert] = useState<{ open: boolean; message: string; severity: 'success' | 'error' }>({
open: false,
message: '',
severity: 'success'
});
// Use refs to keep track of the interval
const intervalRef = useRef<NodeJS.Timeout | null>(null);
const updateIntervalMs = 5000; // 5 seconds refresh rate
// Create a memoized fetchData function with useCallback
const fetchData = useCallback(async (showLoadingIndicator = false) => {
try {
if (showLoadingIndicator) {
setRefreshing(true);
}
const response = await fetch('http://141.196.83.136:8003/prom/get_chart_data/temperature/20');
const result = await response.json();
if (result.data && result.data.length > 0) {
// Sort by timestamp first to ensure we get the latest data
const sortedData = [...result.data].sort((a, b) =>
new Date(b.now_timestamp).getTime() - new Date(a.now_timestamp).getTime()
);
// Log the most recent flag
console.log('Most recent flag:', sortedData[0].flag);
// Filter valid data points
const validData = sortedData.filter((item: any) =>
item.now_timestamp &&
item.future_timestamp &&
item.power &&
item.power_future_min &&
item.env_temp_cur &&
item.env_temp_min
);
// Limit to last 20 records but maintain chronological order
const last20Data = validData.slice(-20).sort((a, b) =>
new Date(a.now_timestamp).getTime() - new Date(b.now_timestamp).getTime()
);
console.log(`Data updated at ${new Date().toLocaleTimeString()}:`, {
totalRecords: result.data.length,
validRecords: validData.length,
displayedRecords: last20Data.length,
latestFlag: last20Data[last20Data.length - 1]?.flag
});
setData(last20Data);
setLastUpdated(new Date());
}
} catch (error) {
console.error('Error fetching data:', error);
} finally {
setLoading(false);
setRefreshing(false);
}
}, []);
// Manual refresh handler
const handleRefresh = () => {
fetchData(true);
};
// Set up the interval for real-time updates
useEffect(() => {
// Initial fetch
fetchData(true);
// Set up interval for auto-refresh
intervalRef.current = setInterval(() => {
console.log(`Auto-refreshing data at ${new Date().toLocaleTimeString()}`);
fetchData(false);
}, updateIntervalMs);
// Cleanup function to clear the interval when component unmounts
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
}, [fetchData]);
// Process data for Plotly charts
const preparePlotlyData = () => {
if (!data || data.length === 0) return { powerData: [], tempData: [] };
const currentTimestamps = data.map(item => new Date(item.now_timestamp));
const futureTimestamps = data.map(item => new Date(item.future_timestamp));
const currentPower = data.map(item => parseFloat(item.power));
const predictedPower = data.map(item => parseFloat(item.power_future_min));
const currentTemp = data.map(item => parseFloat(item.env_temp_cur));
const predictedTemp = data.map(item => parseFloat(item.env_temp_min));
// Calculate min and max values for range sliders
const powerMin = Math.min(...currentPower, ...predictedPower);
const powerMax = Math.max(...currentPower, ...predictedPower);
const tempMin = Math.min(...currentTemp, ...predictedTemp);
const tempMax = Math.max(...currentTemp, ...predictedTemp);
const powerData: Partial<PlotData>[] = [
{
x: currentTimestamps,
y: currentPower,
type: 'scatter',
mode: 'lines+markers',
name: 'Current Power',
line: { color: theme.palette.primary.main, width: 2 },
marker: { size: 6 }
},
{
x: futureTimestamps,
y: predictedPower,
type: 'scatter',
mode: 'lines+markers',
name: 'Predicted Power',
line: { color: theme.palette.success.main, width: 2, dash: 'dash' },
marker: { size: 6 }
},
// Range slider trace
{
x: [...currentTimestamps, ...futureTimestamps],
y: Array(currentTimestamps.length + futureTimestamps.length).fill(0.5),
type: 'scatter',
mode: 'lines',
name: 'Y Range',
yaxis: 'y2',
line: { color: 'transparent' },
showlegend: false,
hoverinfo: 'skip' as const
}
];
const tempData: Partial<PlotData>[] = [
{
x: currentTimestamps,
y: currentTemp,
type: 'scatter',
mode: 'lines+markers',
name: 'Current Temperature',
line: { color: theme.palette.primary.main, width: 2 },
marker: { size: 6 }
},
{
x: futureTimestamps,
y: predictedTemp,
type: 'scatter',
mode: 'lines+markers',
name: 'Predicted Temperature',
line: { color: theme.palette.success.main, width: 2, dash: 'dash' },
marker: { size: 6 }
},
// Range slider trace
{
x: [...currentTimestamps, ...futureTimestamps],
y: Array(currentTimestamps.length + futureTimestamps.length).fill(0.5),
type: 'scatter',
mode: 'lines',
name: 'Y Range',
yaxis: 'y2',
line: { color: 'transparent' },
showlegend: false,
hoverinfo: 'skip' as const
}
];
return {
powerData,
tempData,
ranges: {
power: { min: powerMin, max: powerMax },
temp: { min: tempMin, max: tempMax }
}
};
};
const { powerData, tempData, ranges } = preparePlotlyData();
// Common layout settings for both charts
const commonLayoutSettings: Partial<Layout> = {
showlegend: true,
legend: {
orientation: 'h',
y: -0.2,
x: 0.5,
xanchor: 'center',
yanchor: 'top',
font: {
size: 12,
family: theme.typography.fontFamily,
color: theme.palette.text.secondary
},
bgcolor: 'rgba(255, 255, 255, 0)',
bordercolor: 'rgba(255, 255, 255, 0)'
},
margin: { t: 60, b: 100, l: 60, r: 60 }, // Increased right margin for Y-axis range slider
plot_bgcolor: 'rgba(0,0,0,0)',
paper_bgcolor: 'rgba(0,0,0,0)',
hovermode: 'closest',
xaxis: {
type: 'date',
gridcolor: theme.palette.divider,
tickfont: { size: 12, color: theme.palette.text.secondary },
showgrid: true,
rangeslider: { visible: true }
},
yaxis2: {
overlaying: 'y',
side: 'right',
showgrid: false,
zeroline: false,
showticklabels: false,
range: [0, 1],
rangeslider: {
visible: true,
thickness: 0.1,
bgcolor: 'rgba(0,0,0,0)',
bordercolor: theme.palette.divider
}
}
};
// Add handlers for zoom events
const handlePowerZoom = (event: any) => {
if (event['xaxis.range[0]']) {
setPowerZoom({
xRange: [new Date(event['xaxis.range[0]']).getTime(), new Date(event['xaxis.range[1]']).getTime()],
yRange: [event['yaxis.range[0]'], event['yaxis.range[1]']]
});
}
};
const handleTempZoom = (event: any) => {
if (event['xaxis.range[0]']) {
setTempZoom({
xRange: [new Date(event['xaxis.range[0]']).getTime(), new Date(event['xaxis.range[1]']).getTime()],
yRange: [event['yaxis.range[0]'], event['yaxis.range[1]']]
});
}
};
// Modify the power layout to use preserved zoom
const powerLayout: Partial<Layout> = {
...commonLayoutSettings,
yaxis: {
title: 'Power (W)',
gridcolor: theme.palette.divider,
tickfont: { size: 12, color: theme.palette.text.secondary },
titlefont: { size: 14, color: theme.palette.text.primary },
showgrid: true,
rangemode: 'tozero',
fixedrange: false,
range: powerZoom.yRange || (ranges ? [ranges.power.min * 0.9, ranges.power.max * 1.1] : undefined)
},
xaxis: {
...commonLayoutSettings.xaxis,
range: powerZoom.xRange ? [new Date(powerZoom.xRange[0]), new Date(powerZoom.xRange[1])] : undefined
}
};
// Modify the temperature layout to use preserved zoom
const tempLayout: Partial<Layout> = {
...commonLayoutSettings,
yaxis: {
title: 'Temperature (°C)',
gridcolor: theme.palette.divider,
tickfont: { size: 12, color: theme.palette.text.secondary },
titlefont: { size: 14, color: theme.palette.text.primary },
showgrid: true,
rangemode: 'tozero',
fixedrange: false,
range: tempZoom.yRange || (ranges ? [ranges.temp.min * 0.9, ranges.temp.max * 1.1] : undefined)
},
xaxis: {
...commonLayoutSettings.xaxis,
range: tempZoom.xRange ? [new Date(tempZoom.xRange[0]), new Date(tempZoom.xRange[1])] : undefined
}
};
// Common Plotly config with additional modebar buttons
const plotConfig: Partial<Config> = {
responsive: true,
displayModeBar: true,
displaylogo: false,
modeBarButtonsToRemove: ['lasso2d', 'select2d'] as ('lasso2d' | 'select2d')[],
toImageButtonOptions: {
format: 'png' as const,
filename: 'temperature_monitoring',
height: 1000,
width: 1500,
scale: 2
}
};
// Handle temperature decision
const handleTemperatureDecision = async (approval: boolean) => {
try {
setDecisionLoading(true);
const response = await fetch('http://141.196.83.136:8003/prom/temperature/decisions?approval=' + approval, {
method: 'POST',
headers: {
'accept': 'application/json',
}
});
if (!response.ok) {
throw new Error(`Failed to send temperature decision: ${response.statusText}`);
}
const result = await response.json();
setAlert({
open: true,
message: result.message || `Temperature change ${approval ? 'approved' : 'declined'} successfully`,
severity: 'success'
});
// Refresh data after decision
await fetchData(true);
} catch (error) {
console.error('Error sending temperature decision:', error);
setAlert({
open: true,
message: error instanceof Error ? error.message : 'Failed to send temperature decision',
severity: 'error'
});
} finally {
setDecisionLoading(false);
}
};
const handleCloseAlert = () => {
setAlert(prev => ({ ...prev, open: false }));
};
return (
<Box sx={{ flexGrow: 1, bgcolor: theme.palette.background.default }}>
<AppBar
position="static"
elevation={0}
sx={{
bgcolor: 'background.paper',
borderBottom: `1px solid ${theme.palette.divider}`,
mb: 3
}}
>
<Toolbar sx={{ px: { xs: 2, sm: 4 } }}>
<Typography
variant="h5"
component="h1"
sx={{
color: 'text.primary',
fontWeight: 500,
flex: 1,
textAlign: "center",
letterSpacing: '-0.5px'
}}
>
Environmental Temperature & Power Monitoring (Last 20 Records)
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
{lastUpdated && (
<Typography
variant="body2"
color="text.secondary"
sx={{ mr: 2, display: { xs: 'none', sm: 'block' } }}
>
Last updated: {lastUpdated.toLocaleTimeString()}
</Typography>
)}
<Tooltip title="Refresh data">
<IconButton
onClick={handleRefresh}
color="primary"
disabled={loading || refreshing}
sx={{
animation: refreshing ? 'spin 1s linear infinite' : 'none',
'@keyframes spin': {
'0%': { transform: 'rotate(0deg)' },
'100%': { transform: 'rotate(360deg)' }
}
}}
>
<RefreshIcon />
</IconButton>
</Tooltip>
</Box>
</Toolbar>
</AppBar>
<Box sx={{ p: { xs: 2, sm: 4 } }}>
{/* Temperature Decision Panel */}
<Paper
elevation={0}
sx={{
p: 2,
mb: 3,
bgcolor: 'background.paper',
borderRadius: 2,
border: `1px solid ${theme.palette.divider}`,
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Typography variant="h6" sx={{ color: 'text.primary' }}>
Temperature Change Decision
</Typography>
<Box sx={{ display: 'flex', gap: 2 }}>
<Button
variant="contained"
color="success"
startIcon={<CheckCircleIcon />}
onClick={() => handleTemperatureDecision(true)}
disabled={decisionLoading}
sx={{
borderRadius: 2,
textTransform: 'none',
minWidth: 120,
}}
>
Approve
</Button>
<Button
variant="contained"
color="error"
startIcon={<CancelIcon />}
onClick={() => handleTemperatureDecision(false)}
disabled={decisionLoading}
sx={{
borderRadius: 2,
textTransform: 'none',
minWidth: 120,
}}
>
Decline
</Button>
</Box>
</Box>
</Paper>
<Fade in timeout={800}>
<Grid container spacing={3}>
{/* Power Chart */}
<Grid item xs={12} md={6}>
<Paper
elevation={0}
sx={{
p: { xs: 2, sm: 3 },
bgcolor: 'background.paper',
borderRadius: 2,
border: `1px solid ${theme.palette.divider}`,
height: '100%',
position: 'relative'
}}
>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography
variant="h6"
sx={{
color: theme.palette.text.primary,
fontWeight: 500
}}
>
Power Consumption
</Typography>
{data.length > 0 && (
<Chip
label={data[data.length - 1]?.flag || 'N/A'}
color={data[data.length - 1]?.flag === '25' ? 'success' : 'warning'}
size="medium"
sx={{
height: 32,
'& .MuiChip-label': {
px: 2,
fontSize: '0.875rem',
fontWeight: 600
}
}}
/>
)}
</Box>
{refreshing && (
<Box
sx={{
position: 'absolute',
top: 10,
right: 10,
zIndex: 10,
bgcolor: 'rgba(255,255,255,0.8)',
borderRadius: '50%',
p: 0.5
}}
>
<CircularProgress size={24} thickness={5} />
</Box>
)}
{loading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '400px' }}>
<CircularProgress size={40} thickness={4} />
<Typography variant="h6" sx={{ ml: 2 }} color="text.secondary">
Loading power data...
</Typography>
</Box>
) : data.length === 0 ? (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '400px' }}>
<Typography variant="h6" color="text.secondary">No power data available</Typography>
</Box>
) : (
<Box sx={{ height: 400 }}>
<Plot
data={powerData}
layout={powerLayout}
config={plotConfig}
style={{ width: '100%', height: '100%' }}
onRelayout={handlePowerZoom}
/>
</Box>
)}
</Paper>
</Grid>
{/* Temperature Chart */}
<Grid item xs={12} md={6}>
<Paper
elevation={0}
sx={{
p: { xs: 2, sm: 3 },
bgcolor: 'background.paper',
borderRadius: 2,
border: `1px solid ${theme.palette.divider}`,
height: '100%',
position: 'relative'
}}
>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography
variant="h6"
sx={{
color: theme.palette.text.primary,
fontWeight: 500
}}
>
Environmental Temperature
</Typography>
{data.length > 0 && (
<Chip
label={data[data.length - 1]?.flag || 'N/A'}
color={data[data.length - 1]?.flag === '25' ? 'success' : 'warning'}
size="medium"
sx={{
height: 32,
'& .MuiChip-label': {
px: 2,
fontSize: '0.875rem',
fontWeight: 600
}
}}
/>
)}
</Box>
{refreshing && (
<Box
sx={{
position: 'absolute',
top: 10,
right: 10,
zIndex: 10,
bgcolor: 'rgba(255,255,255,0.8)',
borderRadius: '50%',
p: 0.5
}}
>
<CircularProgress size={24} thickness={5} />
</Box>
)}
{loading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '400px' }}>
<CircularProgress size={40} thickness={4} />
<Typography variant="h6" sx={{ ml: 2 }} color="text.secondary">
Loading temperature data...
</Typography>
</Box>
) : data.length === 0 ? (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '400px' }}>
<Typography variant="h6" color="text.secondary">No temperature data available</Typography>
</Box>
) : (
<Box sx={{ height: 400 }}>
<Plot
data={tempData}
layout={tempLayout}
config={plotConfig}
style={{ width: '100%', height: '100%' }}
onRelayout={handleTempZoom}
/>
</Box>
)}
</Paper>
</Grid>
</Grid>
</Fade>
</Box>
{/* Snackbar for alerts */}
<Snackbar
open={alert.open}
autoHideDuration={6000}
onClose={handleCloseAlert}
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
>
<Alert
onClose={handleCloseAlert}
severity={alert.severity}
variant="filled"
sx={{ width: '100%' }}
>
{alert.message}
</Alert>
</Snackbar>
</Box>
);
};
export default Temperature;