forked from BLC/AyposWeb
fix: improve stress testing functionality and stop button behavior
This commit is contained in:
110
src/components/DebugConsole.tsx
Normal file
110
src/components/DebugConsole.tsx
Normal 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;
|
||||
63
src/components/Layout/MainLayout.tsx
Normal file
63
src/components/Layout/MainLayout.tsx
Normal 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;
|
||||
200
src/components/Layout/Sidebar.tsx
Normal file
200
src/components/Layout/Sidebar.tsx
Normal 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;
|
||||
247
src/components/Migration/MigrationAdviceCard.tsx
Normal file
247
src/components/Migration/MigrationAdviceCard.tsx
Normal 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;
|
||||
228
src/components/Migration/ResourceDistributionChart.tsx
Normal file
228
src/components/Migration/ResourceDistributionChart.tsx
Normal 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;
|
||||
138
src/components/Migration/SummaryStats.tsx
Normal file
138
src/components/Migration/SummaryStats.tsx
Normal 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;
|
||||
163
src/components/Migration/VerifiedMigration.tsx
Normal file
163
src/components/Migration/VerifiedMigration.tsx
Normal 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;
|
||||
161
src/components/Migration/hooks.ts
Normal file
161
src/components/Migration/hooks.ts
Normal 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 };
|
||||
};
|
||||
34
src/components/Migration/types.ts
Normal file
34
src/components/Migration/types.ts
Normal 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;
|
||||
}[];
|
||||
}
|
||||
10
src/components/styled/Logo.tsx
Normal file
10
src/components/styled/Logo.tsx
Normal 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" />
|
||||
Reference in New Issue
Block a user