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

View File

@@ -0,0 +1,110 @@
import React from 'react';
import {
Box,
Paper,
Typography,
IconButton,
Button,
Collapse,
Divider,
styled,
} from '@mui/material';
import BugReportIcon from '@mui/icons-material/BugReport';
import CodeIcon from '@mui/icons-material/Code';
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
const DebugPanel = styled(Paper)(({ theme }) => ({
position: 'fixed',
bottom: 0,
right: 20,
width: 400,
maxHeight: '60vh',
borderRadius: theme.spacing(2, 2, 0, 0),
padding: theme.spacing(2),
zIndex: 1000,
backgroundColor: theme.palette.background.paper,
boxShadow: theme.shadows[10],
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
}));
const ConfigDisplay = styled(Box)(({ theme }) => ({
backgroundColor: theme.palette.mode === 'dark' ? '#1e1e1e' : '#f5f5f5',
padding: theme.spacing(1.5),
borderRadius: theme.spacing(1),
fontFamily: 'monospace',
fontSize: '0.875rem',
overflowX: 'auto',
overflowY: 'auto',
maxHeight: '50vh',
'& pre': {
margin: 0,
},
}));
interface DebugConsoleProps {
configData: any | null;
}
const DebugConsole: React.FC<DebugConsoleProps> = ({ configData }) => {
const [showDebugPanel, setShowDebugPanel] = React.useState(false);
const toggleDebugPanel = () => {
setShowDebugPanel(prev => !prev);
};
return (
<>
{/* Debug Panel Toggle Button */}
<Button
variant="outlined"
startIcon={<BugReportIcon />}
onClick={toggleDebugPanel}
sx={{
position: 'fixed',
bottom: showDebugPanel ? 'auto' : 20,
right: 20,
zIndex: 1001,
borderRadius: 2,
}}
>
{showDebugPanel ? 'Hide' : 'Show'} Debug Info
</Button>
{/* Debug Panel */}
<Collapse in={showDebugPanel}>
<DebugPanel>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
<Typography variant="h6" sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<CodeIcon color="primary" />
Last Monitoring Configuration
</Typography>
<IconButton size="small" onClick={toggleDebugPanel}>
<KeyboardArrowDownIcon />
</IconButton>
</Box>
<Divider sx={{ mb: 2 }} />
{configData ? (
<ConfigDisplay>
<pre>{JSON.stringify(configData, null, 2)}</pre>
</ConfigDisplay>
) : (
<Typography variant="body2" color="text.secondary" sx={{ fontStyle: 'italic', textAlign: 'center', py: 2 }}>
No configuration has been sent yet. Start monitoring to see the data structure.
</Typography>
)}
<Box sx={{ mt: 2 }}>
<Typography variant="caption" color="text.secondary">
Tip: You can also view this information in the browser console by pressing F12.
</Typography>
</Box>
</DebugPanel>
</Collapse>
</>
);
};
export default DebugConsole;

View File

@@ -0,0 +1,63 @@
import React, { useState } from 'react';
import { Box, IconButton, useTheme, useMediaQuery } from '@mui/material';
import MenuIcon from '@mui/icons-material/Menu';
import Sidebar from './Sidebar';
const MainLayout = ({ children }: { children: React.ReactNode }) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const [sidebarOpen, setSidebarOpen] = useState(!isMobile);
const toggleSidebar = () => {
setSidebarOpen(!sidebarOpen);
};
return (
<Box sx={{ display: 'flex', minHeight: '100vh' }}>
<Sidebar
open={sidebarOpen}
onToggle={toggleSidebar}
isMobile={isMobile}
/>
<Box
component="main"
sx={{
flexGrow: 1,
bgcolor: 'background.default',
position: 'relative',
overflow: 'auto',
minHeight: '100vh',
}}
>
{isMobile && (
<IconButton
color="inherit"
aria-label="open drawer"
edge="start"
onClick={toggleSidebar}
sx={{
position: 'absolute',
top: 16,
left: 16,
zIndex: theme.zIndex.drawer + 2,
bgcolor: theme.palette.primary.main,
color: '#fff',
'&:hover': {
bgcolor: theme.palette.primary.dark,
},
}}
>
<MenuIcon />
</IconButton>
)}
<Box sx={{ p: { xs: 2, sm: 3 }, mt: { xs: 7, md: 0 } }}>
{children}
</Box>
</Box>
</Box>
);
};
export default MainLayout;

View File

@@ -0,0 +1,200 @@
import {
Box,
List,
ListItem,
ListItemButton,
ListItemText,
styled,
Typography,
ListItemIcon,
Drawer,
useTheme,
IconButton
} from '@mui/material';
import { useLocation, useNavigate } from 'react-router-dom';
import HomeIcon from '@mui/icons-material/Home';
import ThermostatIcon from '@mui/icons-material/Thermostat';
import BuildIcon from '@mui/icons-material/Build';
import SwapHorizIcon from '@mui/icons-material/SwapHoriz';
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
import SpeedIcon from '@mui/icons-material/Speed';
import bgreenLogo from '../../assets/bgreen-logo.png';
const DRAWER_WIDTH = 240;
const LogoContainer = styled(Box)(() => ({
padding: '20px',
borderBottom: '1px solid rgba(255,255,255,0.1)',
background: 'linear-gradient(180deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0) 100%)',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}));
const StyledListItemButton = styled(ListItemButton)(() => ({
margin: '4px 8px',
borderRadius: 4,
'&.Mui-selected': {
backgroundColor: 'rgba(255,255,255,0.1)',
'&:hover': {
backgroundColor: 'rgba(255,255,255,0.15)',
},
'& .MuiListItemIcon-root': {
color: '#ffffff',
},
'& .MuiListItemText-primary': {
color: '#ffffff',
fontWeight: 500,
},
},
'&:hover': {
backgroundColor: 'rgba(255,255,255,0.05)',
},
}));
const menuItems = [
{ text: 'Home', path: '/', icon: <HomeIcon /> },
{ text: 'Environmental Temperature', path: '/temperature', icon: <ThermostatIcon /> },
{ text: 'Preventive Maintenance', path: '/maintenance', icon: <BuildIcon /> },
{ text: 'Migration Advice', path: '/migration', icon: <SwapHorizIcon /> },
{ text: 'Stress Testing', path: '/stress-testing', icon: <SpeedIcon /> },
];
interface SidebarProps {
open: boolean;
onToggle: () => void;
isMobile: boolean;
}
const Sidebar = ({ open, onToggle, isMobile }: SidebarProps) => {
const theme = useTheme();
const navigate = useNavigate();
const location = useLocation();
const handleNavigation = (path: string) => {
navigate(path);
if (isMobile) {
onToggle();
}
};
const drawerContent = (
<>
<LogoContainer>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<img
src={bgreenLogo}
alt="B'GREEN Logo"
style={{
width: 40,
height: 40,
objectFit: 'contain'
}}
/>
<Box>
<Typography
variant="h6"
sx={{
color: '#ffffff',
fontWeight: 600,
letterSpacing: '0.5px',
}}
>
B'GREEN
</Typography>
<Typography
variant="caption"
sx={{
color: 'rgba(255,255,255,0.7)',
display: 'block',
marginTop: '-2px',
}}
>
Monitor System
</Typography>
</Box>
</Box>
{isMobile && (
<IconButton
onClick={onToggle}
sx={{
color: 'rgba(255,255,255,0.7)',
'&:hover': { color: '#ffffff' }
}}
>
<ChevronLeftIcon />
</IconButton>
)}
</LogoContainer>
<List sx={{ flexGrow: 1, mt: 2 }}>
{menuItems.map((item) => (
<ListItem key={item.text} disablePadding>
<StyledListItemButton
selected={location.pathname === item.path}
onClick={() => handleNavigation(item.path)}
>
<ListItemIcon sx={{ color: 'rgba(255,255,255,0.7)', minWidth: 40 }}>
{item.icon}
</ListItemIcon>
<ListItemText
primary={item.text}
sx={{
'& .MuiListItemText-primary': {
fontSize: '0.9rem',
color: 'rgba(255,255,255,0.7)',
},
}}
/>
</StyledListItemButton>
</ListItem>
))}
</List>
<Box
sx={{
p: 2,
borderTop: '1px solid rgba(255,255,255,0.1)',
textAlign: 'center',
background: 'linear-gradient(0deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0) 100%)',
}}
>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.5)' }}>
2024 B'GREEN
</Typography>
</Box>
</>
);
return (
<Box
component="nav"
sx={{
width: { md: DRAWER_WIDTH },
flexShrink: { md: 0 },
}}
>
<Drawer
variant={isMobile ? "temporary" : "permanent"}
open={isMobile ? open : true}
onClose={isMobile ? onToggle : undefined}
ModalProps={{
keepMounted: true, // Better open performance on mobile
}}
sx={{
display: { xs: 'block' },
'& .MuiDrawer-paper': {
width: DRAWER_WIDTH,
backgroundColor: theme.palette.primary.main,
border: 'none',
height: '100%',
},
}}
>
{drawerContent}
</Drawer>
</Box>
);
};
export default Sidebar;

View File

@@ -0,0 +1,247 @@
import React from 'react';
import {
Paper,
Typography,
IconButton,
Box,
Grid,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
Chip,
CircularProgress,
useTheme,
} from '@mui/material';
import RefreshIcon from '@mui/icons-material/Refresh';
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh';
import { GainBeforeData, MigrationAdviceData } from './types';
interface MigrationAdviceCardProps {
isCardExpanded: boolean;
setIsCardExpanded: (expanded: boolean) => void;
gainBeforeData: GainBeforeData | null;
migrationAdviceData: MigrationAdviceData | null;
isLoadingGainData: boolean;
onRefresh: (e: React.MouseEvent) => void;
migrationMode: 'auto' | 'semiauto';
}
const MigrationAdviceCard: React.FC<MigrationAdviceCardProps> = ({
isCardExpanded,
setIsCardExpanded,
gainBeforeData,
migrationAdviceData,
isLoadingGainData,
onRefresh,
migrationMode,
}) => {
const theme = useTheme();
return (
<Grid item xs={12} md={4}>
<Paper
onClick={() => setIsCardExpanded(!isCardExpanded)}
sx={{
height: isCardExpanded ? 'auto' : '320px',
bgcolor: 'background.paper',
boxShadow: 3,
display: 'flex',
flexDirection: 'column',
cursor: 'pointer',
transition: 'all 0.3s ease-in-out',
'&:hover': {
boxShadow: 6,
},
position: isCardExpanded ? 'absolute' : 'relative',
right: isCardExpanded ? 0 : 'auto',
zIndex: isCardExpanded ? 1000 : 1,
width: isCardExpanded ? '100%' : 'auto',
maxHeight: isCardExpanded ? '80vh' : '320px',
overflowY: isCardExpanded ? 'auto' : 'hidden'
}}
>
<Box sx={{ p: 1.5, display: 'flex', flexDirection: 'column', height: '100%' }}>
<Box sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
mb: 1,
position: 'sticky',
top: 0,
bgcolor: 'background.paper',
zIndex: 2,
py: 1
}}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="h6">Migration Advice</Typography>
<Chip
icon={<AutoFixHighIcon fontSize="small" />}
label={`${migrationMode === 'auto' ? 'Auto' : 'Semi-Auto'} Mode`}
size="small"
sx={{
bgcolor: migrationMode === 'auto' ? theme.palette.success.light : theme.palette.warning.light,
color: migrationMode === 'auto' ? theme.palette.success.dark : theme.palette.warning.dark,
fontWeight: 500,
fontSize: '0.7rem',
height: '24px',
}}
/>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{isLoadingGainData && <CircularProgress size={20} />}
<KeyboardArrowDownIcon
fontSize="small"
sx={{
transform: isCardExpanded ? 'rotate(180deg)' : 'none',
transition: 'transform 0.2s ease-in-out'
}}
/>
</Box>
</Box>
<Box sx={{
flex: 1,
display: 'flex',
flexDirection: 'column',
gap: 2
}}>
{/* Power Gain Information */}
{gainBeforeData && (
<Box sx={{
p: 1.5,
bgcolor: theme.palette.primary.light,
borderRadius: 1,
color: 'white',
position: 'sticky',
top: 48,
zIndex: 1
}}>
<Grid container spacing={2}>
<Grid item xs={4}>
<Typography variant="caption" sx={{ opacity: 0.9 }}>
Current Power
</Typography>
<Typography variant="h6" sx={{ fontWeight: 'bold' }}>
{gainBeforeData.cur_power.toFixed(2)} W
</Typography>
</Grid>
<Grid item xs={4}>
<Typography variant="caption" sx={{ opacity: 0.9 }}>
Proposed Power
</Typography>
<Typography variant="h6" sx={{ fontWeight: 'bold' }}>
{gainBeforeData.prop_power.toFixed(2)} W
</Typography>
</Grid>
<Grid item xs={4}>
<Typography variant="caption" sx={{ opacity: 0.9 }}>
Expected Gain
</Typography>
<Typography
variant="h6"
sx={{
fontWeight: 'bold',
color: gainBeforeData.prop_gain > 0 ? '#4caf50' : '#f44336'
}}
>
{(gainBeforeData.prop_gain * 100).toFixed(2)}%
</Typography>
</Grid>
</Grid>
</Box>
)}
{/* Migration Advice Table */}
{migrationAdviceData && (
<Box sx={{ flex: 1, overflow: 'auto' }}>
<Table size="small">
<TableHead>
<TableRow sx={{
'& th': {
fontWeight: 'bold',
bgcolor: 'primary.main',
color: 'white',
padding: '4px 8px'
}
}}>
<TableCell>Virtual Machine</TableCell>
<TableCell>Current PM</TableCell>
<TableCell sx={{ width: 30, p: 0 }}></TableCell>
<TableCell>Proposed PM</TableCell>
</TableRow>
</TableHead>
<TableBody>
{Object.entries(migrationAdviceData).map(([vm, data]) => (
<TableRow key={vm}>
<TableCell sx={{ padding: '4px 8px' }}>
<Chip
label={vm}
size="small"
sx={{ bgcolor: 'info.light', color: 'white', height: '24px' }}
/>
</TableCell>
<TableCell sx={{ padding: '4px 8px' }}>
<Chip
label={data.current_pm}
size="small"
sx={{ bgcolor: 'warning.light', height: '24px' }}
/>
</TableCell>
<TableCell sx={{
p: 0,
textAlign: 'center',
fontSize: '20px',
color: theme.palette.success.main,
fontWeight: 'bold'
}}>
</TableCell>
<TableCell sx={{ padding: '4px 8px' }}>
<Chip
label={data.proposed_pm}
size="small"
sx={{ bgcolor: 'success.light', color: 'white', height: '24px' }}
/>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Box>
)}
{!gainBeforeData && !migrationAdviceData && !isLoadingGainData && (
<Typography variant="body2" sx={{ textAlign: 'center', color: 'text.secondary' }}>
No migration advice available at this time
</Typography>
)}
</Box>
</Box>
<Box sx={{
borderTop: 1,
borderColor: theme.palette.grey[200],
p: 1,
bgcolor: theme.palette.grey[50],
mt: 'auto',
position: 'sticky',
bottom: 0,
zIndex: 2
}}>
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
<IconButton
onClick={onRefresh}
size="small"
>
<RefreshIcon />
</IconButton>
</Box>
</Box>
</Paper>
</Grid>
);
};
export default MigrationAdviceCard;

View File

@@ -0,0 +1,228 @@
import React from 'react';
import { Paper, Typography, IconButton, Box, Grid } from '@mui/material';
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
BarElement,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
} from 'chart.js';
import { Chart } from 'react-chartjs-2';
import RefreshIcon from '@mui/icons-material/Refresh';
ChartJS.register(
CategoryScale,
LinearScale,
BarElement,
PointElement,
LineElement,
Title,
Tooltip,
Legend
);
interface VM {
name: string;
power: number;
}
interface VMPlacementData {
data_center: string;
id: number;
physical_machines: Array<{
name: string;
power_consumption: number;
vms: {
active: VM[];
inactive: VM[];
};
}>;
}
interface ResourceDistributionChartProps {
vmPlacementData: VMPlacementData | null;
isLoading: boolean;
onRefresh: () => Promise<void>;
}
const ResourceDistributionChart: React.FC<ResourceDistributionChartProps> = ({
vmPlacementData,
isLoading,
onRefresh
}) => {
const chartData = React.useMemo(() => {
if (!vmPlacementData?.physical_machines) {
return {
labels: [],
datasets: []
};
}
const physicalMachines = vmPlacementData.physical_machines;
const labels = physicalMachines.map(pm => pm.name);
const activeVMs = physicalMachines.map(pm => pm.vms.active.length);
const inactiveVMs = physicalMachines.map(pm => pm.vms.inactive.length);
const totalPower = physicalMachines.map(pm => pm.power_consumption);
const vmPower = physicalMachines.map(pm =>
pm.vms.active.reduce((sum, vm) => sum + vm.power, 0)
);
return {
labels,
datasets: [
{
type: 'bar' as const,
label: 'Active VMs',
data: activeVMs,
backgroundColor: '#4caf50',
yAxisID: 'vmAxis',
stack: 'vms',
},
{
type: 'bar' as const,
label: 'Inactive VMs',
data: inactiveVMs,
backgroundColor: '#ff9800',
yAxisID: 'vmAxis',
stack: 'vms',
},
{
type: 'line' as const,
label: 'Total Power (W)',
data: totalPower,
borderColor: '#f44336',
backgroundColor: 'transparent',
yAxisID: 'powerAxis',
tension: 0.4,
borderWidth: 2,
},
{
type: 'line' as const,
label: 'VM Power (W)',
data: vmPower,
borderColor: '#2196f3',
backgroundColor: 'transparent',
yAxisID: 'powerAxis',
borderDash: [5, 5],
tension: 0.4,
borderWidth: 2,
}
]
};
}, [vmPlacementData]);
const options = {
responsive: true,
maintainAspectRatio: false,
interaction: {
mode: 'index' as const,
intersect: false,
},
plugins: {
legend: {
position: 'top' as const,
labels: {
usePointStyle: true,
},
},
tooltip: {
callbacks: {
label: (context: any) => {
const label = context.dataset.label || '';
const value = context.parsed.y;
if (label.includes('Power')) {
return `${label}: ${value.toFixed(2)} W`;
}
return `${label}: ${value}`;
},
},
},
},
scales: {
x: {
grid: {
display: false,
},
},
vmAxis: {
type: 'linear' as const,
position: 'left' as const,
title: {
display: true,
text: 'Number of VMs',
},
beginAtZero: true,
ticks: {
stepSize: 1,
},
stacked: true,
},
powerAxis: {
type: 'linear' as const,
position: 'right' as const,
title: {
display: true,
text: 'Power (W)',
},
beginAtZero: true,
grid: {
drawOnChartArea: false,
},
},
},
};
return (
<Grid item xs={12} md={8}>
<Paper
sx={{
p: 2,
height: '320px',
position: 'relative',
bgcolor: 'background.paper',
borderRadius: 1,
}}
>
<Box sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
mb: 2
}}>
<Typography variant="h6">Resource Distribution by Node</Typography>
<IconButton
onClick={onRefresh}
disabled={isLoading}
sx={{ '&:disabled': { opacity: 0.5 } }}
>
<RefreshIcon />
</IconButton>
</Box>
<Box sx={{ height: 'calc(100% - 48px)' }}>
<Chart type="bar" data={chartData} options={options} />
</Box>
{isLoading && (
<Box sx={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
bgcolor: 'rgba(255, 255, 255, 0.7)',
}}>
<Typography variant="body2">Loading...</Typography>
</Box>
)}
</Paper>
</Grid>
);
};
export default ResourceDistributionChart;

View File

@@ -0,0 +1,138 @@
import React, { useState, useEffect } from 'react';
import { Grid, Paper, Typography, Box, Divider } from '@mui/material';
import StorageIcon from '@mui/icons-material/Storage';
import ComputerIcon from '@mui/icons-material/Computer';
interface VM {
name: string;
power: number;
}
interface VMPlacementData {
data_center: string;
id: number;
physical_machines: Array<{
name: string;
power_consumption: number;
vms: {
active: VM[];
inactive: VM[];
};
}>;
}
const ENDPOINT = 'http://141.196.83.136:8003/prom/get_chart_data/vm_placement';
const REFRESH_INTERVAL = 30000; // 30 seconds
const SummaryStats: React.FC = () => {
const [data, setData] = useState<VMPlacementData | null>(null);
const fetchData = async () => {
try {
const response = await fetch(ENDPOINT);
if (!response.ok) {
throw new Error(`Failed to fetch data: ${response.status}`);
}
const jsonData = await response.json();
setData(jsonData);
} catch (err) {
console.error('Error fetching VM placement data:', err);
}
};
useEffect(() => {
fetchData();
const interval = setInterval(fetchData, REFRESH_INTERVAL);
return () => clearInterval(interval);
}, []);
const stats = React.useMemo(() => {
if (!data?.physical_machines) {
return {
activeComputes: 0,
totalComputes: 0,
activeVMs: 0,
inactiveVMs: 0,
};
}
const activeComputes = data.physical_machines.filter(
pm => pm.power_consumption > 0 || pm.vms.active.length > 0
).length;
const totalActiveVMs = data.physical_machines.reduce(
(sum, pm) => sum + pm.vms.active.length,
0
);
const totalInactiveVMs = data.physical_machines.reduce(
(sum, pm) => sum + pm.vms.inactive.length,
0
);
return {
activeComputes,
totalComputes: data.physical_machines.length,
activeVMs: totalActiveVMs,
inactiveVMs: totalInactiveVMs,
};
}, [data]);
return (
<Grid item xs={12}>
<Paper
sx={{
p: 2,
bgcolor: 'background.paper',
boxShadow: 3,
borderRadius: 1,
display: 'flex',
alignItems: 'center',
gap: 3
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
<StorageIcon sx={{ color: 'primary.main', fontSize: 28 }} />
<Box>
<Typography variant="body2" color="textSecondary" sx={{ mb: 0.5 }}>
Compute Nodes
</Typography>
<Typography variant="h6" sx={{ lineHeight: 1, fontWeight: 500 }}>
{stats.activeComputes}/{stats.totalComputes}
</Typography>
</Box>
</Box>
<Divider orientation="vertical" flexItem />
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
<ComputerIcon sx={{ color: 'info.main', fontSize: 28 }} />
<Box>
<Typography variant="body2" color="textSecondary" sx={{ mb: 0.5 }}>
Virtual Machines
</Typography>
<Box sx={{ display: 'flex', alignItems: 'baseline', gap: 0.5 }}>
<Typography variant="h6" sx={{ lineHeight: 1, fontWeight: 500 }}>
{stats.activeVMs}
</Typography>
<Typography variant="body2" color="success.main" sx={{ fontWeight: 500 }}>
active
</Typography>
<Typography variant="body2" color="text.secondary">
/
</Typography>
<Typography variant="h6" sx={{ lineHeight: 1, fontWeight: 500 }}>
{stats.activeVMs + stats.inactiveVMs}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ fontWeight: 500 }}>
total
</Typography>
</Box>
</Box>
</Box>
</Paper>
</Grid>
);
};
export default SummaryStats;

View File

@@ -0,0 +1,163 @@
import React from 'react';
import {
Paper,
Typography,
Box,
Grid,
Chip,
CircularProgress,
useTheme,
} from '@mui/material';
interface VerifiedMigrationProps {
gainAfterData: {
past_power: number;
cur_power: number;
prop_power: number;
actual_ratio: number;
val_ratio: number;
} | null;
isLoading: boolean;
}
const VerifiedMigration: React.FC<VerifiedMigrationProps> = ({
gainAfterData,
isLoading,
}) => {
const theme = useTheme();
if (isLoading) {
return (
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
<CircularProgress />
</Box>
);
}
if (!gainAfterData) {
return (
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
<Typography color="text.secondary">No verification data available</Typography>
</Box>
);
}
const isValidated = gainAfterData.val_ratio >= 0.95;
return (
<Paper
sx={{
p: 1.5,
mb: 2,
bgcolor: 'background.paper',
boxShadow: 3,
}}
>
<Typography variant="h6" sx={{ mb: 1.5 }}>Migration Verification Results</Typography>
{/* Power Optimization Results */}
<Box sx={{
p: 2,
bgcolor: theme.palette.primary.light,
color: 'white',
borderRadius: 2,
boxShadow: 1
}}>
<Grid container spacing={3}>
<Grid item xs={12} md={4}>
<Box sx={{ textAlign: 'center' }}>
<Typography variant="subtitle2" sx={{ mb: 0.5, opacity: 0.9, fontSize: '0.875rem' }}>
Current Power
</Typography>
<Typography variant="h4" sx={{ fontWeight: 'bold', mb: 0.5 }}>
{gainAfterData.cur_power.toFixed(2)} W
</Typography>
</Box>
</Grid>
<Grid item xs={12} md={4}>
<Box sx={{
textAlign: 'center',
p: 1.5,
bgcolor: 'rgba(255, 255, 255, 0.1)',
borderRadius: 1,
height: '100%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center'
}}>
<Typography variant="subtitle2" sx={{ mb: 0.5, fontSize: '0.875rem' }}>
Accuracy of migration proposal
</Typography>
<Typography variant="h4" sx={{
fontWeight: 'bold',
mb: 0.5,
color: isValidated ? '#4caf50' : '#ff9800'
}}>
{(gainAfterData.val_ratio * 100).toFixed(2)}%
</Typography>
</Box>
</Grid>
<Grid item xs={12} md={4}>
<Box sx={{
textAlign: 'center',
p: 1.5,
bgcolor: 'rgba(255, 255, 255, 0.1)',
borderRadius: 1,
height: '100%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center'
}}>
<Typography variant="subtitle2" sx={{ mb: 0.5, fontSize: '0.875rem' }}>
Actual Power Change
</Typography>
<Typography variant="h4" sx={{
fontWeight: 'bold',
mb: 0.5,
color: gainAfterData.actual_ratio > 0 ? '#4caf50' : '#ff9800'
}}>
{(gainAfterData.actual_ratio * 100).toFixed(2)}%
</Typography>
</Box>
</Grid>
</Grid>
</Box>
{/* Summary Footer */}
<Box sx={{
mt: 1.5,
p: 1.5,
bgcolor: theme.palette.grey[50],
borderRadius: 2,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
border: 1,
borderColor: theme.palette.grey[200]
}}>
<Box>
<Typography variant="subtitle2" color="text.secondary">
Previous Power Consumption
</Typography>
<Typography variant="h6" sx={{ color: theme.palette.text.primary, fontWeight: 'bold' }}>
{gainAfterData.past_power.toFixed(2)} W
</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Chip
label={isValidated ? "Optimization Verified" : "Verification Needed"}
size="small"
sx={{
bgcolor: isValidated ? theme.palette.success.light : theme.palette.warning.light,
color: isValidated ? theme.palette.success.dark : theme.palette.warning.dark,
fontWeight: 'bold',
height: '24px'
}}
/>
</Box>
</Box>
</Paper>
);
};
export default VerifiedMigration;

View File

@@ -0,0 +1,161 @@
import { useState, useEffect } from 'react';
import { VMDetails, GainBeforeData, MigrationAdviceData } from './types';
const API_BASE_URL = 'http://141.196.83.136:8003';
const REFRESH_INTERVAL = 30000; // 30 seconds
interface GainAfterData {
past_power: number;
cur_power: number;
prop_power: number;
prop_ratio: number;
actual_ratio: number;
val_ratio: number;
val_difference: number;
}
export const useMigrationData = () => {
const [gainBeforeData, setGainBeforeData] = useState<GainBeforeData | null>(null);
const [migrationAdviceData, setMigrationAdviceData] = useState<MigrationAdviceData | null>(null);
const [isLoadingGainData, setIsLoadingGainData] = useState(false);
const fetchMigrationData = async () => {
try {
setIsLoadingGainData(true);
const [gainResponse, migrationResponse] = await Promise.all([
fetch(`${API_BASE_URL}/prom/get_chart_data/gain_before`),
fetch(`${API_BASE_URL}/prom/get_chart_data/migration`)
]);
if (!gainResponse.ok || !migrationResponse.ok) {
throw new Error('Failed to fetch migration data');
}
const [gainData, migrationData] = await Promise.all([
gainResponse.json(),
migrationResponse.json()
]);
setGainBeforeData(gainData);
setMigrationAdviceData(migrationData);
} catch (error) {
console.error('Error fetching migration data:', error);
} finally {
setIsLoadingGainData(false);
}
};
useEffect(() => {
fetchMigrationData();
const interval = setInterval(fetchMigrationData, REFRESH_INTERVAL);
return () => clearInterval(interval);
}, []);
return { gainBeforeData, migrationAdviceData, isLoadingGainData, fetchMigrationData };
};
export const useMonitoringData = () => {
const [monitoringData, setMonitoringData] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [stableHosts, setStableHosts] = useState<string[]>([]);
const [computeCount, setComputeCount] = useState<number>(0);
const [vmCount, setVmCount] = useState<number>(0);
const fetchMonitoringData = async () => {
try {
setLoading(true);
const response = await fetch(`${API_BASE_URL}/prom/monitoring`);
if (!response.ok) {
throw new Error('Network response was not ok');
}
const result = await response.json();
if (result?.data) {
setMonitoringData(result.data);
const filteredData = result.data.filter((pm: any) => pm.virtual_machines?.length > 0);
setComputeCount(filteredData.length);
setVmCount(filteredData.reduce((acc: number, pm: any) => acc + pm.virtual_machines.length, 0));
const newHosts = result.data.map((pm: any) => pm.host);
setStableHosts(prevHosts => {
const allHosts = Array.from(new Set([...prevHosts, ...newHosts]));
return allHosts.filter(host => newHosts.includes(host));
});
}
} catch (error) {
console.error('Error fetching monitoring data:', error);
setMonitoringData([]);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchMonitoringData();
const interval = setInterval(fetchMonitoringData, REFRESH_INTERVAL);
return () => clearInterval(interval);
}, []);
return { monitoringData, loading, stableHosts, computeCount, vmCount };
};
export const useVMDetails = () => {
const [vmDetails, setVmDetails] = useState<Record<string, VMDetails>>({});
const [expandedVMs, setExpandedVMs] = useState<Record<string, boolean>>({});
const toggleVMDetails = (vmId: string) => {
setExpandedVMs(prev => ({
...prev,
[vmId]: !prev[vmId]
}));
};
useEffect(() => {
const fetchVMDetails = async () => {
try {
const response = await fetch(`${API_BASE_URL}/prom/vm_mac_details`);
if (!response.ok) {
throw new Error('Failed to fetch VM details');
}
const data = await response.json();
if (data?.res) {
setVmDetails(data.res);
}
} catch (error) {
console.error('Error fetching VM details:', error);
}
};
fetchVMDetails();
const interval = setInterval(fetchVMDetails, REFRESH_INTERVAL);
return () => clearInterval(interval);
}, []);
return { vmDetails, expandedVMs, toggleVMDetails };
};
export const useGainAfterData = () => {
const [gainAfterData, setGainAfterData] = useState<GainAfterData | null>(null);
const [isLoading, setIsLoading] = useState(false);
const fetchGainAfterData = async () => {
try {
setIsLoading(true);
const response = await fetch(`${API_BASE_URL}/prom/get_chart_data/gain_after`);
if (!response.ok) {
throw new Error('Failed to fetch gain-after data');
}
const data = await response.json();
setGainAfterData(data);
} catch (error) {
console.error('Error fetching gain-after data:', error);
} finally {
setIsLoading(false);
}
};
return { gainAfterData, isLoading, fetchGainAfterData };
};

View File

@@ -0,0 +1,34 @@
export interface VMDetails {
disk: number;
ephemeral: number;
extra_specs: Record<string, any>;
host: string;
ip: string;
name: string;
original_name: string;
ram: number;
swap: number;
vcpus: number;
}
export interface GainBeforeData {
prop_gain: number;
prop_power: number;
cur_power: number;
}
export interface MigrationAdviceData {
[key: string]: {
current_pm: string;
proposed_pm: string;
};
}
export interface ChartData {
labels: string[];
datasets: {
label: string;
data: number[];
backgroundColor: string;
}[];
}

View File

@@ -0,0 +1,10 @@
import styled from '@mui/material/styles/styled';
export const Logo = styled('img')({
height: '40px',
width: 'auto',
objectFit: 'contain'
});
// Then use it like:
// <Logo src={bgreenLogo} alt="B'GREEN Logo" />