implimented the new Vm-placement data strcuture, fixed 8/9 designs issue from last meeting written in the group blc - in 7/8/2025

This commit is contained in:
2025-08-03 14:51:16 +03:00
parent 277e425332
commit 81fd909637
15 changed files with 1295 additions and 647 deletions

19
package-lock.json generated
View File

@@ -17,14 +17,15 @@
"@mui/x-tree-view": "^7.26.0", "@mui/x-tree-view": "^7.26.0",
"@types/plotly.js": "^2.35.2", "@types/plotly.js": "^2.35.2",
"axios": "^1.8.4", "axios": "^1.8.4",
"chart.js": "^4.4.1", "chart.js": "^4.5.0",
"date-fns": "^3.0.6", "chartjs-adapter-date-fns": "^3.0.0",
"date-fns": "^3.6.0",
"lucide-react": "^0.525.0", "lucide-react": "^0.525.0",
"plotly.js": "^3.0.1", "plotly.js": "^3.0.1",
"plotly.js-dist": "^3.0.1", "plotly.js-dist": "^3.0.1",
"plotly.js-dist-min": "^3.0.1", "plotly.js-dist-min": "^3.0.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-chartjs-2": "^5.2.0", "react-chartjs-2": "^5.3.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-google-charts": "^5.2.1", "react-google-charts": "^5.2.1",
"react-plotly.js": "^2.6.0", "react-plotly.js": "^2.6.0",
@@ -3409,7 +3410,6 @@
"version": "4.5.0", "version": "4.5.0",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz", "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz",
"integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==", "integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"@kurkle/color": "^0.3.0" "@kurkle/color": "^0.3.0"
}, },
@@ -3417,6 +3417,15 @@
"pnpm": ">=8" "pnpm": ">=8"
} }
}, },
"node_modules/chartjs-adapter-date-fns": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chartjs-adapter-date-fns/-/chartjs-adapter-date-fns-3.0.0.tgz",
"integrity": "sha512-Rs3iEB3Q5pJ973J93OBTpnP7qoGwvq3nUnoMdtxO+9aoJof7UFcRbWcIDteXuYd1fgAvct/32T9qaLyLuZVwCg==",
"peerDependencies": {
"chart.js": ">=2.8.0",
"date-fns": ">=2.0.0"
}
},
"node_modules/clamp": { "node_modules/clamp": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/clamp/-/clamp-1.0.1.tgz", "resolved": "https://registry.npmjs.org/clamp/-/clamp-1.0.1.tgz",
@@ -3898,7 +3907,6 @@
"version": "3.6.0", "version": "3.6.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz",
"integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==",
"license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/kossnocorp" "url": "https://github.com/sponsors/kossnocorp"
@@ -6648,7 +6656,6 @@
"version": "5.3.0", "version": "5.3.0",
"resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.0.tgz", "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.0.tgz",
"integrity": "sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==", "integrity": "sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==",
"license": "MIT",
"peerDependencies": { "peerDependencies": {
"chart.js": "^4.1.1", "chart.js": "^4.1.1",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"

View File

@@ -19,14 +19,15 @@
"@mui/x-tree-view": "^7.26.0", "@mui/x-tree-view": "^7.26.0",
"@types/plotly.js": "^2.35.2", "@types/plotly.js": "^2.35.2",
"axios": "^1.8.4", "axios": "^1.8.4",
"chart.js": "^4.4.1", "chart.js": "^4.5.0",
"date-fns": "^3.0.6", "chartjs-adapter-date-fns": "^3.0.0",
"date-fns": "^3.6.0",
"lucide-react": "^0.525.0", "lucide-react": "^0.525.0",
"plotly.js": "^3.0.1", "plotly.js": "^3.0.1",
"plotly.js-dist": "^3.0.1", "plotly.js-dist": "^3.0.1",
"plotly.js-dist-min": "^3.0.1", "plotly.js-dist-min": "^3.0.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-chartjs-2": "^5.2.0", "react-chartjs-2": "^5.3.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-google-charts": "^5.2.1", "react-google-charts": "^5.2.1",
"react-plotly.js": "^2.6.0", "react-plotly.js": "^2.6.0",

View File

@@ -8,8 +8,23 @@ import Maintenance from './pages/Maintenance';
import Migration from './pages/Migration'; import Migration from './pages/Migration';
import MonitoringSystem from './pages/MonitoringSystem'; import MonitoringSystem from './pages/MonitoringSystem';
import StressTesting from './pages/StressTesting'; import StressTesting from './pages/StressTesting';
import { useEffect } from 'react';
function App() { function App() {
useEffect(() => {
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
e.preventDefault();
e.returnValue = 'Are you sure you want to close this page?';
return 'Are you sure you want to close this page?';
};
window.addEventListener('beforeunload', handleBeforeUnload);
return () => {
window.removeEventListener('beforeunload', handleBeforeUnload);
};
}, []);
return ( return (
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<CssBaseline /> <CssBaseline />

View File

@@ -120,7 +120,28 @@ const Sidebar = ({ open, onToggle, isMobile }: SidebarProps) => {
const drawerContent = ( const drawerContent = (
<> <>
<LogoContainer> <LogoContainer>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> <Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
cursor: 'pointer',
'&:hover': {
opacity: 0.8
}
}}
onClick={() => {
// Get the current domain and navigate to the main B'GREEN site
const currentDomain = window.location.hostname;
const protocol = window.location.protocol;
// Remove subdomain if it exists (e.g., aypos.blc-css.com -> blc-css.com)
const baseDomain = currentDomain.replace(/^[^.]+\./, '');
// Navigate to the main B'GREEN site
window.open(`${protocol}//${baseDomain}`, '_blank');
}}
>
<img <img
src={bgreenLogo} src={bgreenLogo}
alt="B'GREEN Logo" alt="B'GREEN Logo"

View File

@@ -45,7 +45,7 @@ const MigrationAdviceCard: React.FC<MigrationAdviceCardProps> = ({
<Paper <Paper
onClick={() => setIsCardExpanded(!isCardExpanded)} onClick={() => setIsCardExpanded(!isCardExpanded)}
sx={{ sx={{
height: isCardExpanded ? 'auto' : '320px', height: isCardExpanded ? 'auto' : '400px',
bgcolor: 'background.paper', bgcolor: 'background.paper',
boxShadow: 3, boxShadow: 3,
display: 'flex', display: 'flex',
@@ -59,7 +59,7 @@ const MigrationAdviceCard: React.FC<MigrationAdviceCardProps> = ({
right: isCardExpanded ? 0 : 'auto', right: isCardExpanded ? 0 : 'auto',
zIndex: isCardExpanded ? 1000 : 1, zIndex: isCardExpanded ? 1000 : 1,
width: isCardExpanded ? '100%' : 'auto', width: isCardExpanded ? '100%' : 'auto',
maxHeight: isCardExpanded ? '80vh' : '320px', maxHeight: isCardExpanded ? '80vh' : '400px',
overflowY: isCardExpanded ? 'auto' : 'hidden' overflowY: isCardExpanded ? 'auto' : 'hidden'
}} }}
> >
@@ -125,7 +125,7 @@ const MigrationAdviceCard: React.FC<MigrationAdviceCardProps> = ({
Current Power Current Power
</Typography> </Typography>
<Typography variant="h6" sx={{ fontWeight: 'bold' }}> <Typography variant="h6" sx={{ fontWeight: 'bold' }}>
{gainBeforeData.cur_power.toFixed(2)} W {gainBeforeData.cur_power.toFixed(2)}W
</Typography> </Typography>
</Grid> </Grid>
<Grid item xs={4}> <Grid item xs={4}>
@@ -133,7 +133,7 @@ const MigrationAdviceCard: React.FC<MigrationAdviceCardProps> = ({
Proposed Power Proposed Power
</Typography> </Typography>
<Typography variant="h6" sx={{ fontWeight: 'bold' }}> <Typography variant="h6" sx={{ fontWeight: 'bold' }}>
{gainBeforeData.prop_power.toFixed(2)} W {gainBeforeData.prop_power.toFixed(2)}W
</Typography> </Typography>
</Grid> </Grid>
<Grid item xs={4}> <Grid item xs={4}>
@@ -144,10 +144,10 @@ const MigrationAdviceCard: React.FC<MigrationAdviceCardProps> = ({
variant="h6" variant="h6"
sx={{ sx={{
fontWeight: 'bold', fontWeight: 'bold',
color: gainBeforeData.prop_gain > 0 ? '#4caf50' : '#f44336' color: gainBeforeData.prop_gain > 0 ? '#28c76f' : '#FF1744'
}} }}
> >
{(gainBeforeData.prop_gain * 100).toFixed(2)}% {Math.abs(gainBeforeData.prop_gain * 100).toFixed(2)}%
</Typography> </Typography>
</Grid> </Grid>
</Grid> </Grid>

View File

@@ -1,52 +1,32 @@
import React from 'react'; import React from 'react';
import { Paper, Typography, IconButton, Box, Grid } from '@mui/material'; import { Grid, Paper, Typography, Box, IconButton, CircularProgress, useTheme } from '@mui/material';
import RefreshIcon from '@mui/icons-material/Refresh';
import { Chart as ChartJSComponent } from 'react-chartjs-2';
import { import {
Chart as ChartJS, Chart as ChartJS,
CategoryScale, CategoryScale,
LinearScale, LinearScale,
BarElement,
BarController,
PointElement, PointElement,
LineElement, LineElement,
LineController, BarElement,
Title, Title,
Tooltip, Tooltip,
Legend, Legend,
ChartOptions,
} from 'chart.js'; } from 'chart.js';
import { Chart } from 'react-chartjs-2'; import { VMPlacementData } from './types';
import RefreshIcon from '@mui/icons-material/Refresh';
ChartJS.register( ChartJS.register(
CategoryScale, CategoryScale,
LinearScale, LinearScale,
BarElement,
BarController,
PointElement, PointElement,
LineElement, LineElement,
LineController, BarElement,
Title, Title,
Tooltip, Tooltip,
Legend 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 { interface ResourceDistributionChartProps {
vmPlacementData: VMPlacementData | null; vmPlacementData: VMPlacementData | null;
isLoading: boolean; isLoading: boolean;
@@ -58,22 +38,34 @@ const ResourceDistributionChart: React.FC<ResourceDistributionChartProps> = ({
isLoading, isLoading,
onRefresh onRefresh
}) => { }) => {
const theme = useTheme();
const chartData = React.useMemo(() => { const chartData = React.useMemo(() => {
if (!vmPlacementData?.physical_machines) { if (!vmPlacementData?.vm_placement) {
return { return {
labels: [], labels: [],
datasets: [] datasets: []
}; };
} }
const physicalMachines = vmPlacementData.physical_machines; // Process data from vm_placement object
const labels = physicalMachines.map(pm => pm.name); const pmData: Record<string, { power: number; activeVMs: number; inactiveVMs: number; vmPower: number }> = {};
const activeVMs = physicalMachines.map(pm => pm.vms.active.length);
const inactiveVMs = physicalMachines.map(pm => pm.vms.inactive.length); Object.values(vmPlacementData.vm_placement).forEach(pm => {
const totalPower = physicalMachines.map(pm => pm.power_consumption); const vms = Object.values(pm.vms);
const vmPower = physicalMachines.map(pm => pmData[pm.name] = {
pm.vms.active.reduce((sum, vm) => sum + vm.power, 0) power: pm.power,
); activeVMs: vms.filter(vm => vm.state === 'active').length,
inactiveVMs: vms.filter(vm => vm.state === 'inactive').length,
vmPower: vms.filter(vm => vm.state === 'active').reduce((sum, vm) => sum + vm.power, 0)
};
});
const labels = Object.keys(pmData);
const activeVMs = labels.map(pmName => pmData[pmName].activeVMs);
const inactiveVMs = labels.map(pmName => pmData[pmName].inactiveVMs);
const totalPower = labels.map(pmName => pmData[pmName].power);
const vmPower = labels.map(pmName => pmData[pmName].vmPower);
return { return {
labels, labels,
@@ -82,23 +74,25 @@ const ResourceDistributionChart: React.FC<ResourceDistributionChartProps> = ({
type: 'bar' as const, type: 'bar' as const,
label: 'Active VMs', label: 'Active VMs',
data: activeVMs, data: activeVMs,
backgroundColor: '#4caf50', backgroundColor: theme.palette.success.main,
yAxisID: 'vmAxis', yAxisID: 'vmAxis',
stack: 'vms', stack: 'vms',
borderRadius: 4,
}, },
{ {
type: 'bar' as const, type: 'bar' as const,
label: 'Inactive VMs', label: 'Inactive VMs',
data: inactiveVMs, data: inactiveVMs,
backgroundColor: '#ff9800', backgroundColor: theme.palette.grey[300],
yAxisID: 'vmAxis', yAxisID: 'vmAxis',
stack: 'vms', stack: 'vms',
borderRadius: 4,
}, },
{ {
type: 'line' as const, type: 'line' as const,
label: 'Total Power (W)', label: 'Total Power (W)',
data: totalPower, data: totalPower,
borderColor: '#f44336', borderColor: theme.palette.primary.main,
backgroundColor: 'transparent', backgroundColor: 'transparent',
yAxisID: 'powerAxis', yAxisID: 'powerAxis',
tension: 0.4, tension: 0.4,
@@ -108,7 +102,7 @@ const ResourceDistributionChart: React.FC<ResourceDistributionChartProps> = ({
type: 'line' as const, type: 'line' as const,
label: 'VM Power (W)', label: 'VM Power (W)',
data: vmPower, data: vmPower,
borderColor: '#2196f3', borderColor: theme.palette.info.main,
backgroundColor: 'transparent', backgroundColor: 'transparent',
yAxisID: 'powerAxis', yAxisID: 'powerAxis',
borderDash: [5, 5], borderDash: [5, 5],
@@ -117,9 +111,9 @@ const ResourceDistributionChart: React.FC<ResourceDistributionChartProps> = ({
} }
] ]
}; };
}, [vmPlacementData]); }, [vmPlacementData, theme.palette]);
const options = { const options: ChartOptions<'bar' | 'line'> = React.useMemo(() => ({
responsive: true, responsive: true,
maintainAspectRatio: false, maintainAspectRatio: false,
interaction: { interaction: {
@@ -131,19 +125,32 @@ const ResourceDistributionChart: React.FC<ResourceDistributionChartProps> = ({
position: 'top' as const, position: 'top' as const,
labels: { labels: {
usePointStyle: true, usePointStyle: true,
color: theme.palette.text.secondary,
font: {
family: theme.typography.fontFamily,
size: 12
},
padding: 20,
}, },
}, },
tooltip: { tooltip: {
callbacks: { enabled: true,
label: (context: any) => { backgroundColor: 'rgba(255, 255, 255, 0.95)',
const label = context.dataset.label || ''; titleColor: theme.palette.text.primary,
const value = context.parsed.y; titleFont: {
if (label.includes('Power')) { family: theme.typography.fontFamily,
return `${label}: ${value.toFixed(2)} W`; size: 13,
} weight: '600'
return `${label}: ${value}`;
},
}, },
bodyColor: theme.palette.text.secondary,
bodyFont: {
family: theme.typography.fontFamily,
size: 12
},
borderColor: theme.palette.divider,
borderWidth: 1,
padding: 12,
boxPadding: 4,
}, },
}, },
scales: { scales: {
@@ -151,6 +158,13 @@ const ResourceDistributionChart: React.FC<ResourceDistributionChartProps> = ({
grid: { grid: {
display: false, display: false,
}, },
ticks: {
color: theme.palette.text.secondary,
font: {
family: theme.typography.fontFamily,
size: 11
}
}
}, },
vmAxis: { vmAxis: {
type: 'linear' as const, type: 'linear' as const,
@@ -158,10 +172,25 @@ const ResourceDistributionChart: React.FC<ResourceDistributionChartProps> = ({
title: { title: {
display: true, display: true,
text: 'Number of VMs', text: 'Number of VMs',
color: theme.palette.text.secondary,
font: {
family: theme.typography.fontFamily,
size: 12,
weight: '500'
}
}, },
beginAtZero: true, beginAtZero: true,
ticks: { ticks: {
stepSize: 1, stepSize: 1,
color: theme.palette.text.secondary,
font: {
family: theme.typography.fontFamily,
size: 11
}
},
grid: {
color: theme.palette.divider,
drawBorder: false,
}, },
stacked: true, stacked: true,
}, },
@@ -171,59 +200,76 @@ const ResourceDistributionChart: React.FC<ResourceDistributionChartProps> = ({
title: { title: {
display: true, display: true,
text: 'Power (W)', text: 'Power (W)',
color: theme.palette.text.secondary,
font: {
family: theme.typography.fontFamily,
size: 12,
weight: '500'
}
}, },
beginAtZero: true, beginAtZero: true,
ticks: {
color: theme.palette.text.secondary,
font: {
family: theme.typography.fontFamily,
size: 11
}
},
grid: { grid: {
drawOnChartArea: false, drawOnChartArea: false,
color: theme.palette.divider,
drawBorder: false,
}, },
}, },
}, },
}; }), [theme]);
return ( return (
<Grid item xs={12} md={8}> <Grid item xs={12} md={8}>
<Paper <Paper
sx={{ sx={{
p: 2, p: 2,
height: '320px',
position: 'relative',
bgcolor: 'background.paper', bgcolor: 'background.paper',
boxShadow: 3,
borderRadius: 1, borderRadius: 1,
height: '400px',
position: 'relative'
}} }}
> >
<Box sx={{ <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
display: 'flex', <Typography variant="h6" sx={{ color: 'text.primary', fontWeight: 500 }}>
justifyContent: 'space-between', Resource Distribution
alignItems: 'center', </Typography>
mb: 2
}}>
<Typography variant="h6">Resource Distribution by Node</Typography>
<IconButton <IconButton
onClick={onRefresh} onClick={onRefresh}
disabled={isLoading} disabled={isLoading}
sx={{ '&:disabled': { opacity: 0.5 } }} size="small"
sx={{
color: 'primary.main',
'&:hover': { bgcolor: 'primary.light' }
}}
> >
<RefreshIcon /> {isLoading ? <CircularProgress size={20} /> : <RefreshIcon />}
</IconButton> </IconButton>
</Box> </Box>
<Box sx={{ height: 'calc(100% - 48px)' }}>
<Chart type="bar" data={chartData} options={options} /> <Box sx={{ height: 'calc(100% - 60px)', position: 'relative' }}>
{chartData.labels.length > 0 ? (
<ChartJSComponent type="bar" data={chartData} options={options} />
) : (
<Box sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100%',
color: 'text.secondary'
}}>
<Typography variant="body2">
{isLoading ? 'Loading data...' : 'No data available'}
</Typography>
</Box>
)}
</Box> </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> </Paper>
</Grid> </Grid>
); );

View File

@@ -1,54 +1,33 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import { Grid, Paper, Typography, Box, Divider } from '@mui/material'; import { Grid, Paper, Typography, Box, Divider } from '@mui/material';
import StorageIcon from '@mui/icons-material/Storage'; import StorageIcon from '@mui/icons-material/Storage';
import ComputerIcon from '@mui/icons-material/Computer'; import ComputerIcon from '@mui/icons-material/Computer';
import { config } from '../../config/env'; import { config } from '../../config/env';
import { VMPlacementData } from './types';
interface VM { import { useSmartPolling } from './hooks';
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 = `${config.apiUrl}/prom/get_chart_data/vm_placement`; const ENDPOINT = `${config.apiUrl}/prom/get_chart_data/vm_placement`;
const REFRESH_INTERVAL = 30000; // 30 seconds
const SummaryStats: React.FC = () => { const SummaryStats: React.FC = () => {
const [data, setData] = useState<VMPlacementData | null>(null); const fetchData = useCallback(async (): Promise<VMPlacementData> => {
const response = await fetch(ENDPOINT);
const fetchData = async () => { if (!response.ok) {
try { throw new Error(`Failed to fetch data: ${response.status}`);
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);
} }
}; const jsonData = await response.json();
console.log('SummaryStats - Raw API response:', jsonData);
useEffect(() => { return jsonData;
fetchData();
const interval = setInterval(fetchData, REFRESH_INTERVAL);
return () => clearInterval(interval);
}, []); }, []);
const { data, pollingInterval } = useSmartPolling<VMPlacementData>(
fetchData,
null,
5000, // min interval: 5 seconds
30000 // max interval: 30 seconds
);
const stats = React.useMemo(() => { const stats = React.useMemo(() => {
if (!data?.physical_machines) { if (!data) {
return { return {
activeComputes: 0, activeComputes: 0,
totalComputes: 0, totalComputes: 0,
@@ -57,30 +36,28 @@ const SummaryStats: React.FC = () => {
}; };
} }
const activeComputes = data.physical_machines.filter( // Count from vm_placement object
pm => pm.power_consumption > 0 || pm.vms.active.length > 0 let totalPMs = Object.keys(data.vm_placement).length;
).length; let activePMs = Object.values(data.vm_placement).filter(pm => pm.power > 0).length;
let totalActiveVMs = 0;
let totalInactiveVMs = 0;
const totalActiveVMs = data.physical_machines.reduce( Object.values(data.vm_placement).forEach(pm => {
(sum, pm) => sum + pm.vms.active.length, const vms = Object.values(pm.vms);
0 totalActiveVMs += vms.filter(vm => vm.state === 'active').length;
); totalInactiveVMs += vms.filter(vm => vm.state === 'inactive').length;
});
const totalInactiveVMs = data.physical_machines.reduce(
(sum, pm) => sum + pm.vms.inactive.length,
0
);
return { return {
activeComputes, activeComputes: activePMs,
totalComputes: data.physical_machines.length, totalComputes: totalPMs,
activeVMs: totalActiveVMs, activeVMs: totalActiveVMs,
inactiveVMs: totalInactiveVMs, inactiveVMs: totalInactiveVMs,
}; };
}, [data]); }, [data]);
return ( return (
<Grid item xs={12}> <Grid item xs={12} md={8}>
<Paper <Paper
sx={{ sx={{
p: 2, p: 2,
@@ -112,23 +89,9 @@ const SummaryStats: React.FC = () => {
<Typography variant="body2" color="textSecondary" sx={{ mb: 0.5 }}> <Typography variant="body2" color="textSecondary" sx={{ mb: 0.5 }}>
Virtual Machines Virtual Machines
</Typography> </Typography>
<Box sx={{ display: 'flex', alignItems: 'baseline', gap: 0.5 }}> <Typography variant="h6" sx={{ lineHeight: 1, fontWeight: 500 }}>
<Typography variant="h6" sx={{ lineHeight: 1, fontWeight: 500 }}> {stats.activeVMs}/{stats.activeVMs + stats.inactiveVMs}
{stats.activeVMs} </Typography>
</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>
</Box> </Box>
</Paper> </Paper>

View File

@@ -70,7 +70,7 @@ const VerifiedMigration: React.FC<VerifiedMigrationProps> = ({
Current Power Current Power
</Typography> </Typography>
<Typography variant="h4" sx={{ fontWeight: 'bold', mb: 0.5 }}> <Typography variant="h4" sx={{ fontWeight: 'bold', mb: 0.5 }}>
{gainAfterData.cur_power.toFixed(2)} W {gainAfterData.cur_power.toFixed(2)}W
</Typography> </Typography>
</Box> </Box>
</Grid> </Grid>
@@ -91,7 +91,7 @@ const VerifiedMigration: React.FC<VerifiedMigrationProps> = ({
<Typography variant="h4" sx={{ <Typography variant="h4" sx={{
fontWeight: 'bold', fontWeight: 'bold',
mb: 0.5, mb: 0.5,
color: isValidated ? '#4caf50' : '#ff9800' color: isValidated ? '#28c76f' : '#ffb400'
}}> }}>
{(gainAfterData.val_ratio * 100).toFixed(2)}% {(gainAfterData.val_ratio * 100).toFixed(2)}%
</Typography> </Typography>
@@ -114,7 +114,7 @@ const VerifiedMigration: React.FC<VerifiedMigrationProps> = ({
<Typography variant="h4" sx={{ <Typography variant="h4" sx={{
fontWeight: 'bold', fontWeight: 'bold',
mb: 0.5, mb: 0.5,
color: gainAfterData.actual_ratio > 0 ? '#4caf50' : '#ff9800' color: gainAfterData.actual_ratio > 0 ? '#28c76f' : '#FF1744'
}}> }}>
{(gainAfterData.actual_ratio * 100).toFixed(2)}% {(gainAfterData.actual_ratio * 100).toFixed(2)}%
</Typography> </Typography>

View File

@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react'; import { useState, useEffect, useCallback, useRef } from 'react';
import { VMDetails, GainBeforeData, MigrationAdviceData } from './types'; import { VMDetails, GainBeforeData, MigrationAdviceData } from './types';
import { config } from '../../config/env'; import { config } from '../../config/env';
@@ -15,6 +15,76 @@ interface GainAfterData {
val_difference: number; val_difference: number;
} }
// Smart polling hook
export const useSmartPolling = <T>(
fetchFunction: () => Promise<T>,
initialState: T | null,
minInterval: number = 5000,
maxInterval: number = 30000
) => {
const [data, setData] = useState<T | null>(initialState);
const [pollingInterval, setPollingInterval] = useState(minInterval);
const [isLoading, setIsLoading] = useState(false);
const [isInitialLoad, setIsInitialLoad] = useState(true);
const lastDataRef = useRef<string>('');
const intervalRef = useRef<NodeJS.Timeout | null>(null);
const poll = useCallback(async () => {
try {
if (isInitialLoad) {
setIsLoading(true);
}
const newData = await fetchFunction();
const newDataString = JSON.stringify(newData);
// Only update if data actually changed
if (lastDataRef.current !== newDataString) {
console.log('Data changed, resetting polling interval to', minInterval, 'ms');
setData(newData);
setPollingInterval(minInterval);
lastDataRef.current = newDataString;
} else {
// Gradually increase interval when data is stable
setPollingInterval(prevInterval => {
const newInterval = Math.min(prevInterval * 1.5, maxInterval);
if (newInterval !== prevInterval) {
console.log('Data stable, increasing polling interval to', newInterval, 'ms');
}
return newInterval;
});
}
} catch (error) {
console.error('Error in smart polling:', error);
// On error, keep current interval
} finally {
setIsLoading(false);
setIsInitialLoad(false);
}
}, [fetchFunction, minInterval, maxInterval, isInitialLoad]);
useEffect(() => {
// Initial fetch
poll();
// Clear any existing interval
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
// Set new interval
intervalRef.current = setInterval(poll, pollingInterval);
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
}, [poll, pollingInterval]);
return { data, isLoading, pollingInterval };
};
export const useMigrationData = () => { export const useMigrationData = () => {
const [gainBeforeData, setGainBeforeData] = useState<GainBeforeData | null>(null); const [gainBeforeData, setGainBeforeData] = useState<GainBeforeData | null>(null);
const [migrationAdviceData, setMigrationAdviceData] = useState<MigrationAdviceData | null>(null); const [migrationAdviceData, setMigrationAdviceData] = useState<MigrationAdviceData | null>(null);
@@ -125,7 +195,7 @@ export const useMonitoringData = () => {
}; };
export const useVMDetails = () => { export const useVMDetails = () => {
const [vmDetails, setVmDetails] = useState<Record<string, VMDetails>>({}); const [vmDetails, setVmDetails] = useState<VMDetails | null>(null);
const [expandedVMs, setExpandedVMs] = useState<Record<string, boolean>>({}); const [expandedVMs, setExpandedVMs] = useState<Record<string, boolean>>({});
const toggleVMDetails = (vmId: string) => { const toggleVMDetails = (vmId: string) => {
@@ -135,28 +205,21 @@ export const useVMDetails = () => {
})); }));
}; };
useEffect(() => { const fetchVMDetails = async (vmName: string) => {
const fetchVMDetails = async () => { try {
try { const response = await fetch(`${API_BASE_URL}/prom/vm_details/${vmName}`);
const response = await fetch(`${API_BASE_URL}/prom/vm_mac_details`); if (!response.ok) {
if (!response.ok) { throw new Error('Failed to fetch VM details');
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);
} }
}; const data = await response.json();
setVmDetails(data);
} catch (error) {
console.error('Error fetching VM details:', error);
setVmDetails(null);
}
};
fetchVMDetails(); return { vmDetails, expandedVMs, toggleVMDetails, fetchVMDetails };
const interval = setInterval(fetchVMDetails, REFRESH_INTERVAL);
return () => clearInterval(interval);
}, []);
return { vmDetails, expandedVMs, toggleVMDetails };
}; };
export const useGainAfterData = () => { export const useGainAfterData = () => {
@@ -167,19 +230,18 @@ export const useGainAfterData = () => {
try { try {
setIsLoading(true); setIsLoading(true);
const response = await fetch(`${API_BASE_URL}/prom/get_chart_data/gain_after`); const response = await fetch(`${API_BASE_URL}/prom/get_chart_data/gain_after`);
if (!response.ok) { if (!response.ok) {
throw new Error('Failed to fetch gain-after data'); throw new Error('Failed to fetch gain after data');
} }
const data = await response.json(); const data = await response.json();
setGainAfterData(data); setGainAfterData(data);
} catch (error) { } catch (error) {
console.error('Error fetching gain-after data:', error); console.error('Error fetching gain after data:', error);
setGainAfterData(null);
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
}; };
return { gainAfterData, isLoading, fetchGainAfterData }; return { gainAfterData, isLoading: isLoading, fetchGainAfterData };
}; };

View File

@@ -24,6 +24,65 @@ export interface MigrationAdviceData {
}; };
} }
// VM interface for the physical_machines structure
export interface PhysicalMachineVM {
status: 'open' | 'closed';
name: string;
power: number;
confg: {
cpu: number;
ram: number;
disk: number;
};
calcOn: string;
}
// Physical Machine interface for the physical_machines structure
export interface PhysicalMachine {
status: 'open' | 'closed';
name: string;
power_consumption: number;
vms: {
active: PhysicalMachineVM[];
inactive: PhysicalMachineVM[];
};
}
// VM interface for the vm_placement structure
export interface VMPlacementVM {
vm_name: string;
power: number;
state: 'active' | 'inactive';
hosting_pm: string;
host: string;
flavor_name: string;
emissionsource: {
dizel: number;
coal: number;
'solar energy': number;
};
tag: string | null;
confg: [string, number, number, number, string]; // [name, vcpus, ram, disk, host]
project: string;
}
// PM interface for the vm_placement structure
export interface VMPlacementPM {
tag: string | null;
name: string;
power: number;
confg: [string, number, number, number, number]; // [name, vcpus, ram, disk, total]
vms: Record<string, VMPlacementVM>;
}
// Updated VMPlacementData interface to match the actual API response
export interface VMPlacementData {
data_center: string;
id: number;
physical_machines: PhysicalMachine[];
vm_placement: Record<string, VMPlacementPM>;
}
export interface ChartData { export interface ChartData {
labels: string[]; labels: string[];
datasets: { datasets: {

View File

@@ -5,7 +5,7 @@ const getApiUrl = (): string => {
return '/api'; return '/api';
} }
// In development, use the direct URL // In development, use the direct URL
return import.meta.env.VITE_API_URL || 'http://141.196.166.241:8003'; return import.meta.env.VITE_API_URL || 'http://aypos-api.blc-css.com';
}; };
export const config = { export const config = {

View File

@@ -232,7 +232,8 @@ const Home = () => {
const [migrationTime, setMigrationTime] = useState<string>('5'); const [migrationTime, setMigrationTime] = useState<string>('5');
const [migrationModel, setMigrationModel] = useState<string>('mul_reg'); const [migrationModel, setMigrationModel] = useState<string>('mul_reg');
const [migrationMethod, setMigrationMethod] = useState<string>('mathematical'); const [migrationMethod, setMigrationMethod] = useState<string>('mathematical');
const [migrationMode, setMigrationMode] = useState<'auto' | 'semiauto'>('auto'); // Default to semiauto since auto mode is not available yet
const [migrationMode, setMigrationMode] = useState<'auto' | 'semiauto'>('semiauto');
const [isMonitoring, setIsMonitoring] = useState(false); const [isMonitoring, setIsMonitoring] = useState(false);
@@ -517,10 +518,10 @@ const Home = () => {
<Typography variant="body2">Time Configuration</Typography> <Typography variant="body2">Time Configuration</Typography>
</IconWrapper> </IconWrapper>
<FormControl fullWidth sx={{ mb: 2 }}> <FormControl fullWidth sx={{ mb: 2 }}>
<InputLabel>Script Time Unit</InputLabel> <InputLabel>{"{script time unit (mins)}"}</InputLabel>
<StyledSelect <StyledSelect
value={envTimeUnit} value={envTimeUnit}
label="Script Time Unit" label="{script time unit (mins)}"
onChange={handleSelectChange} onChange={handleSelectChange}
name="envTimeUnit" name="envTimeUnit"
> >
@@ -535,10 +536,10 @@ const Home = () => {
<Typography variant="body2">Steps Configuration</Typography> <Typography variant="body2">Steps Configuration</Typography>
</IconWrapper> </IconWrapper>
<FormControl fullWidth sx={{ mb: 2 }}> <FormControl fullWidth sx={{ mb: 2 }}>
<InputLabel>Number of Steps</InputLabel> <InputLabel>Estimation Steps</InputLabel>
<StyledSelect <StyledSelect
value={envSteps} value={envSteps}
label="Number of Steps" label="Estimation Steps"
onChange={handleSelectChange} onChange={handleSelectChange}
name="envSteps" name="envSteps"
> >
@@ -581,10 +582,10 @@ const Home = () => {
<Typography variant="body2">Time Configuration</Typography> <Typography variant="body2">Time Configuration</Typography>
</IconWrapper> </IconWrapper>
<FormControl fullWidth sx={{ mb: 2 }}> <FormControl fullWidth sx={{ mb: 2 }}>
<InputLabel>Script Time Unit</InputLabel> <InputLabel>{"{script time unit (mins)}"}</InputLabel>
<StyledSelect <StyledSelect
value={prevTimeUnit} value={prevTimeUnit}
label="Script Time Unit" label="{script time unit (mins)}"
onChange={handleSelectChange} onChange={handleSelectChange}
name="prevTimeUnit" name="prevTimeUnit"
> >
@@ -599,10 +600,10 @@ const Home = () => {
<Typography variant="body2">Steps Configuration</Typography> <Typography variant="body2">Steps Configuration</Typography>
</IconWrapper> </IconWrapper>
<FormControl fullWidth sx={{ mb: 2 }}> <FormControl fullWidth sx={{ mb: 2 }}>
<InputLabel>Number of Steps</InputLabel> <InputLabel>Estimation Steps</InputLabel>
<StyledSelect <StyledSelect
value={prevSteps} value={prevSteps}
label="Number of Steps" label="Estimation Steps"
onChange={handleSelectChange} onChange={handleSelectChange}
name="prevSteps" name="prevSteps"
> >
@@ -665,11 +666,19 @@ const Home = () => {
} }
}} }}
> >
<ToggleButton value="auto" aria-label="auto mode"> <ToggleButton
value="auto"
aria-label="auto mode"
disabled={true}
sx={{
opacity: 0.5,
cursor: 'not-allowed'
}}
>
<AutoFixHighIcon sx={{ mr: 0.5 }} fontSize="small" /> <AutoFixHighIcon sx={{ mr: 0.5 }} fontSize="small" />
Auto Auto
</ToggleButton> </ToggleButton>
<ToggleButton value="semiauto" aria-label="semi-auto mode" disabled> <ToggleButton value="semiauto" aria-label="semi-auto mode">
<HandymanIcon sx={{ mr: 0.5 }} fontSize="small" /> <HandymanIcon sx={{ mr: 0.5 }} fontSize="small" />
Semi-Auto Semi-Auto
</ToggleButton> </ToggleButton>
@@ -681,10 +690,10 @@ const Home = () => {
<Typography variant="body2">Time Configuration</Typography> <Typography variant="body2">Time Configuration</Typography>
</IconWrapper> </IconWrapper>
<FormControl fullWidth sx={{ mb: 2 }}> <FormControl fullWidth sx={{ mb: 2 }}>
<InputLabel>Script Time Unit </InputLabel> <InputLabel>{"{script time unit (mins)}"}</InputLabel>
<StyledSelect <StyledSelect
value={migrationTime} value={migrationTime}
label="Script Time Unit " label="{script time unit (mins)}"
onChange={handleSelectChange} onChange={handleSelectChange}
name="migrationTime" name="migrationTime"
> >

View File

@@ -1,8 +1,32 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { Box, Paper, Typography, Fade, useTheme, AppBar, Toolbar, Chip } from '@mui/material'; import { Box, Paper, Typography, Fade, useTheme, AppBar, Toolbar, Chip, Slider, FormControlLabel, Switch } from '@mui/material';
import { LineChart } from '@mui/x-charts/LineChart'; import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
TimeScale
} from 'chart.js';
import 'chartjs-adapter-date-fns';
import { Line } from 'react-chartjs-2';
import { config } from '../config/env'; import { config } from '../config/env';
// Register Chart.js components
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
TimeScale
);
interface DataItem { interface DataItem {
now_timestamp: string; now_timestamp: string;
future_timestamp: string; future_timestamp: string;
@@ -23,6 +47,7 @@ const Maintenance = () => {
const [data, setData] = useState<DataItem[]>([]); const [data, setData] = useState<DataItem[]>([]);
const [currentFlag, setCurrentFlag] = useState<string>(''); const [currentFlag, setCurrentFlag] = useState<string>('');
const [, setLoading] = useState(true); const [, setLoading] = useState(true);
const [windowSize, setWindowSize] = useState(20);
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchData = async () => {
@@ -49,7 +74,7 @@ const Maintenance = () => {
return () => clearInterval(interval); return () => clearInterval(interval);
}, []); }, []);
// Process data for charts // Process data for charts with sliding window
const prepareChartData = () => { const prepareChartData = () => {
if (!data || data.length === 0) { if (!data || data.length === 0) {
console.log('No data available, using fallback data'); console.log('No data available, using fallback data');
@@ -84,52 +109,184 @@ const Maintenance = () => {
}; };
}); });
console.log('Processed chart data:', processedData); // Apply sliding window - show only last N records
const slidingData = processedData.slice(-windowSize);
console.log('Processed chart data:', {
totalRecords: processedData.length,
showingRecords: slidingData.length,
timeRange: {
start: slidingData[0]?.currentTimestamp,
end: slidingData[slidingData.length - 1]?.currentTimestamp
}
});
console.log('Data validation:', { console.log('Data validation:', {
hasCurrentPower: processedData.some(d => d.currentPower > 0), hasCurrentPower: slidingData.some(d => d.currentPower > 0),
hasPredictedPower: processedData.some(d => d.predictedPower > 0), hasPredictedPower: slidingData.some(d => d.predictedPower > 0),
currentPowerRange: [Math.min(...processedData.map(d => d.currentPower)), Math.max(...processedData.map(d => d.currentPower))], currentPowerRange: [Math.min(...slidingData.map(d => d.currentPower)), Math.max(...slidingData.map(d => d.currentPower))],
predictedPowerRange: [Math.min(...processedData.map(d => d.predictedPower)), Math.max(...processedData.map(d => d.predictedPower))], predictedPowerRange: [Math.min(...slidingData.map(d => d.predictedPower)), Math.max(...slidingData.map(d => d.predictedPower))],
rawDataSample: data.slice(0, 2).map(item => ({ rawDataSample: data.slice(-2).map(item => ({
power: item.power, power: item.power,
power_future_min: item.power_future_min, power_future_min: item.power_future_min,
parsedCurrent: parseFloat(item.power), parsedCurrent: parseFloat(item.power),
parsedPredicted: parseFloat(item.power_future_min) parsedPredicted: parseFloat(item.power_future_min)
})) }))
}); });
return processedData;
return slidingData;
}; };
const chartData = prepareChartData(); const chartData = prepareChartData();
// Extract data arrays for charts // Prepare Chart.js data structure
const currentTimestamps = chartData.map(item => item.currentTimestamp); const chartJsData = {
const futureTimestamps = chartData.map(item => item.futureTimestamp); datasets: [
{
const currentPowerData = chartData.map(item => item.currentPower); label: 'Current Power',
const predictedPowerData = chartData.map(item => item.predictedPower); data: chartData.map(item => ({
const positive3pData = chartData.map(item => item.positive3p); x: item.currentTimestamp,
const negative3pData = chartData.map(item => item.negative3p); y: item.currentPower
const positive7pData = chartData.map(item => item.positive7p); })),
const negative7pData = chartData.map(item => item.negative7p); borderColor: '#028a4a',
backgroundColor: '#028a4a',
pointBackgroundColor: '#028a4a',
pointBorderColor: '#028a4a',
pointRadius: 0,
pointHoverRadius: 6,
tension: 0.4,
fill: false
},
{
label: 'Predicted (Dynamic)',
data: chartData.map(item => ({
x: item.futureTimestamp,
y: item.predictedPower
})),
borderColor: '#ff9800',
backgroundColor: '#ff9800',
pointBackgroundColor: '#ff9800',
pointBorderColor: '#ff9800',
pointRadius: 0,
pointHoverRadius: 6,
tension: 0.4,
fill: false
},
{
label: '+3% Positive',
data: chartData.map(item => ({
x: item.futureTimestamp,
y: item.positive3p
})),
borderColor: '#ffb400',
backgroundColor: 'rgba(255, 180, 0, 0.1)',
pointRadius: 0,
tension: 0.4,
fill: false
},
{
label: '-3% Negative',
data: chartData.map(item => ({
x: item.futureTimestamp,
y: item.negative3p
})),
borderColor: '#ffb400',
backgroundColor: 'rgba(255, 180, 0, 0.1)',
pointRadius: 0,
tension: 0.4,
fill: false
},
{
label: '+7% Positive',
data: chartData.map(item => ({
x: item.futureTimestamp,
y: item.positive7p
})),
borderColor: '#FF1744',
backgroundColor: 'rgba(255, 23, 68, 0.1)',
pointRadius: 0,
tension: 0.4,
fill: false
},
{
label: '-7% Negative',
data: chartData.map(item => ({
x: item.futureTimestamp,
y: item.negative7p
})),
borderColor: '#FF1744',
backgroundColor: 'rgba(255, 23, 68, 0.1)',
pointRadius: 0,
tension: 0.4,
fill: false
}
]
};
// Chart.js options
const chartOptions = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'top' as const,
labels: {
usePointStyle: true,
padding: 20,
font: {
size: 12,
weight: 'normal' as const
}
}
},
tooltip: {
mode: 'index' as const,
intersect: false,
callbacks: {
title: function(context: any) {
const date = new Date(context[0].parsed.x);
return date.toLocaleTimeString();
},
label: function(context: any) {
return `${context.dataset.label}: ${context.parsed.y.toFixed(2)} W`;
}
}
}
},
scales: {
x: {
type: 'time' as const,
time: {
displayFormats: {
minute: 'HH:mm:ss'
}
},
title: {
display: true,
text: 'Time'
}
},
y: {
title: {
display: true,
text: 'Power (W)'
},
ticks: {
callback: function(value: any) {
return `${value} W`;
}
}
}
},
interaction: {
mode: 'nearest' as const,
axis: 'x' as const,
intersect: false
}
};
// Debug logging // Debug logging
console.log('Chart arrays:', { console.log('Chart.js data structure:', chartJsData);
currentTimestamps: currentTimestamps.length,
futureTimestamps: futureTimestamps.length,
currentPower: currentPowerData.length,
predictedPower: predictedPowerData.length,
positive3p: positive3pData.length,
negative3p: negative3pData.length,
positive7p: positive7pData.length,
negative7p: negative7pData.length,
});
console.log('Sample timestamps:', {
current: currentTimestamps.slice(0, 3),
future: futureTimestamps.slice(0, 3),
currentPower: currentPowerData.slice(0, 3),
predictedPower: predictedPowerData.slice(0, 3),
});
return ( return (
<Box sx={{ flexGrow: 1, bgcolor: theme.palette.background.default }}> <Box sx={{ flexGrow: 1, bgcolor: theme.palette.background.default }}>
@@ -156,33 +313,50 @@ const Maintenance = () => {
> >
Preventive Maintenance Preventive Maintenance
</Typography> </Typography>
{currentFlag && ( <Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
{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)',
},
}
}}
/>
)}
<Chip <Chip
label={currentFlag} label={`Showing last ${windowSize} records`}
color={currentFlag === 'Correct Estimation for PM energy' ? 'success' : 'warning'} variant="outlined"
size="medium" size="small"
sx={{ sx={{
height: 32, height: 24,
'& .MuiChip-label': { '& .MuiChip-label': {
px: 2, px: 1.5,
fontSize: '0.875rem', fontSize: '0.75rem',
fontWeight: 600 fontWeight: 500
}, },
animation: 'pulse 2s infinite', borderColor: 'rgba(0, 0, 0, 0.2)',
'@keyframes pulse': { color: 'rgba(0, 0, 0, 0.7)'
'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)',
},
}
}} }}
/> />
)} </Box>
</Toolbar> </Toolbar>
</AppBar> </AppBar>
@@ -197,71 +371,42 @@ const Maintenance = () => {
border: `1px solid ${theme.palette.divider}`, border: `1px solid ${theme.palette.divider}`,
}} }}
> >
<Box sx={{ height: 'calc(100vh - 200px)', minHeight: '500px' }}> <Box sx={{ height: 'calc(100vh - 280px)', minHeight: '500px' }}>
<LineChart <Line data={chartJsData} options={chartOptions} />
height={500} </Box>
skipAnimation={false}
series={[ {/* Chart Controls */}
{ <Box sx={{
data: currentPowerData, mt: 2,
label: 'Current Power', p: 2,
color: '#028a4a', // B'GREEN brand color bgcolor: 'background.default',
showMark: true, borderRadius: 1,
curve: 'linear' border: `1px solid ${theme.palette.divider}`
}, }}>
{ <Typography variant="body2" sx={{ mb: 1, fontWeight: 500 }}>
data: predictedPowerData, Chart Settings
label: 'Predicted (3min)', </Typography>
color: '#ff9800', <Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
showMark: true, <Typography variant="body2" sx={{ minWidth: 120 }}>
curve: 'linear' Records to show:
}, </Typography>
{ <Slider
data: positive3pData, value={windowSize}
label: '+3% Positive', onChange={(_, value) => setWindowSize(value as number)}
color: '#2ca02c', min={5}
showMark: false max={50}
}, step={5}
{ marks={[
data: negative3pData, { value: 5, label: '5' },
label: '-3% Negative', { value: 20, label: '20' },
color: '#d62728', { value: 50, label: '50' }
showMark: false ]}
}, sx={{ flex: 1, maxWidth: 200 }}
{ />
data: positive7pData, <Typography variant="body2" sx={{ minWidth: 40 }}>
label: '+7% Positive', {windowSize}
color: '#9467bd', </Typography>
showMark: false </Box>
},
{
data: negative7pData,
label: '-7% Negative',
color: '#8c564b',
showMark: false
},
]}
xAxis={[
{
scaleType: 'time',
data: [...currentTimestamps, ...futureTimestamps],
valueFormatter: (value: Date) => value.toLocaleTimeString(),
},
]}
yAxis={[
{
width: 50,
valueFormatter: (value: number) => `${value} W`,
}
]}
margin={{ right: 24 }}
slotProps={{
legend: {
direction: 'horizontal',
position: { vertical: 'top', horizontal: 'center' },
},
}}
/>
</Box> </Box>
</Paper> </Paper>
</Fade> </Fade>

View File

@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { import {
Box, Box,
Paper, Paper,
@@ -16,6 +16,9 @@ import {
DialogContent, DialogContent,
DialogActions, DialogActions,
LinearProgress, LinearProgress,
List,
ListItemButton,
ListItemText,
} from '@mui/material'; } from '@mui/material';
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew'; import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
@@ -28,44 +31,15 @@ import SummaryStats from '../components/Migration/SummaryStats';
import ResourceDistributionChart from '../components/Migration/ResourceDistributionChart'; import ResourceDistributionChart from '../components/Migration/ResourceDistributionChart';
import MigrationAdviceCard from '../components/Migration/MigrationAdviceCard'; import MigrationAdviceCard from '../components/Migration/MigrationAdviceCard';
import VerifiedMigration from '../components/Migration/VerifiedMigration'; import VerifiedMigration from '../components/Migration/VerifiedMigration';
import { useMigrationData, useGainAfterData } from '../components/Migration/hooks'; import { useMigrationData, useGainAfterData, useSmartPolling } from '../components/Migration/hooks';
import { config } from '../config/env'; import { config } from '../config/env';
import { VMPlacementData } from '../components/Migration/types';
// Constants // Constants
const API_BASE_URL = config.apiUrl; const API_BASE_URL = config.apiUrl;
const REFRESH_INTERVAL = 30000; // 30 seconds 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 { interface VMCardProps {
vm: { vm: {
@@ -367,8 +341,6 @@ const Migration = () => {
const theme = useTheme(); const theme = useTheme();
// Essential states // Essential states
const [vmPlacementData, setVmPlacementData] = useState<VMPlacementData | null>(null);
const [isLoadingVmPlacement, setIsLoadingVmPlacement] = useState(false);
const [expandedVMs, setExpandedVMs] = useState<Record<string, boolean>>({}); const [expandedVMs, setExpandedVMs] = useState<Record<string, boolean>>({});
const [showVerifiedSection, setShowVerifiedSection] = useState(false); const [showVerifiedSection, setShowVerifiedSection] = useState(false);
const [isCardExpanded, setIsCardExpanded] = useState(false); const [isCardExpanded, setIsCardExpanded] = useState(false);
@@ -378,36 +350,37 @@ const Migration = () => {
const [showProgress, setShowProgress] = useState(false); const [showProgress, setShowProgress] = useState(false);
const [hasProgress, setHasProgress] = useState(false); const [hasProgress, setHasProgress] = useState(false);
// Smart polling for VM placement data - memoized to prevent re-renders
const fetchVmPlacementData = useCallback(async (): Promise<VMPlacementData> => {
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} ${response.statusText}`);
}
const data = await response.json();
console.log('Raw API response:', data);
return data;
}, []);
const { data: vmPlacementData, isLoading: isLoadingVmPlacement, pollingInterval } = useSmartPolling<VMPlacementData>(
fetchVmPlacementData,
null,
5000, // min interval: 5 seconds
30000 // max interval: 30 seconds
);
// Hooks for migration functionality // Hooks for migration functionality
const { gainBeforeData, migrationAdviceData, isLoadingGainData, fetchMigrationData } = useMigrationData(); const { gainBeforeData, migrationAdviceData, isLoadingGainData, fetchMigrationData } = useMigrationData();
const { gainAfterData, isLoading: isLoadingGainAfter, fetchGainAfterData } = useGainAfterData(); const { gainAfterData, isLoading: isLoadingGainAfter, fetchGainAfterData } = useGainAfterData();
// Essential functions // Essential functions
const toggleVMDetails = (vmId: string) => { const toggleVMDetails = useCallback((vmId: string) => {
setExpandedVMs(prev => ({ setExpandedVMs(prev => ({
...prev, ...prev,
[vmId]: !prev[vmId] [vmId]: !prev[vmId]
})); }));
}; }, []);
const fetchVmPlacementData = async () => { const handleApproveMigration = useCallback(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 { try {
setIsProcessing(true); setIsProcessing(true);
setMigrationProgress([]); setMigrationProgress([]);
@@ -449,9 +422,9 @@ const Migration = () => {
} finally { } finally {
setIsProcessing(false); setIsProcessing(false);
} }
}; }, [fetchGainAfterData]);
const handleDeclineMigration = async () => { const handleDeclineMigration = useCallback(async () => {
try { try {
setIsProcessing(true); setIsProcessing(true);
@@ -474,40 +447,37 @@ const Migration = () => {
} finally { } finally {
setIsProcessing(false); 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 // Add effect to monitor vmPlacementData changes
useEffect(() => { useEffect(() => {
if (vmPlacementData) { if (vmPlacementData) {
const blockedPMs = vmPlacementData.physical_machines.filter(pm => pm.status === 'blocked').length; let totalPMs = 0;
const blockedVMs = vmPlacementData.physical_machines.reduce((acc, pm) => { let totalVMs = 0;
const activeBlocked = pm.vms.active.filter(vm => vm.status === 'blocked').length; let activeVMs = 0;
const inactiveBlocked = pm.vms.inactive.filter(vm => vm.status === 'blocked').length; let inactiveVMs = 0;
return acc + activeBlocked + inactiveBlocked;
}, 0); // Count from vm_placement object
totalPMs = Object.keys(vmPlacementData.vm_placement).length;
Object.values(vmPlacementData.vm_placement).forEach(pm => {
const vms = Object.values(pm.vms);
totalVMs += vms.length;
activeVMs += vms.filter(vm => vm.state === 'active').length;
inactiveVMs += vms.filter(vm => vm.state === 'inactive').length;
});
console.log('VM Placement Data updated:', { console.log('VM Placement Data updated:', {
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
pmCount: vmPlacementData.physical_machines.length, pmCount: totalPMs,
blockedPMs, totalVMs,
totalVMs: vmPlacementData.physical_machines.reduce((acc, pm) => activeVMs,
acc + pm.vms.active.length + pm.vms.inactive.length, 0 inactiveVMs,
), dataCenter: vmPlacementData.data_center,
blockedVMs pollingInterval: `${pollingInterval}ms`
}); });
} }
}, [vmPlacementData]); }, [vmPlacementData, pollingInterval]);
return ( return (
<Box sx={{ display: 'flex', flexDirection: 'column', maxWidth: '100vw', minHeight: '100vh' }}> <Box sx={{ display: 'flex', flexDirection: 'column', maxWidth: '100vw', minHeight: '100vh' }}>
@@ -568,7 +538,7 @@ const Migration = () => {
variant="contained" variant="contained"
color="error" color="error"
onClick={handleDeclineMigration} onClick={handleDeclineMigration}
disabled={isProcessing} disabled={isProcessing || !migrationAdviceData || Object.keys(migrationAdviceData).length === 0}
sx={{ sx={{
py: 1.5, py: 1.5,
px: 4, px: 4,
@@ -588,7 +558,7 @@ const Migration = () => {
variant="contained" variant="contained"
startIcon={!isProcessing && <PowerSettingsNewIcon />} startIcon={!isProcessing && <PowerSettingsNewIcon />}
onClick={handleApproveMigration} onClick={handleApproveMigration}
disabled={isProcessing} disabled={isProcessing || !migrationAdviceData || Object.keys(migrationAdviceData).length === 0}
sx={{ sx={{
py: 1.5, py: 1.5,
px: 4, px: 4,
@@ -619,127 +589,236 @@ const Migration = () => {
</Grid> </Grid>
)} )}
{/* PM & VM Monitoring Section */} {/* PMs & VMs Monitoring Section */}
<Grid item xs={12}> <Grid item xs={12}>
<Paper sx={{ p: 2, bgcolor: 'background.paper', boxShadow: 3 }}> <Paper sx={{ p: 2, bgcolor: 'background.paper', boxShadow: 3 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}> <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="h6">PMs & VMs Monitoring</Typography> <Typography variant="h6">PMs & VMs Monitoring</Typography>
<Typography variant="caption" color="text.secondary">
Polling: {pollingInterval / 1000}s
</Typography>
</Box> </Box>
{isLoadingVmPlacement ? ( {isLoadingVmPlacement ? (
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}> <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
<CircularProgress /> <CircularProgress />
</Box> </Box>
) : vmPlacementData?.physical_machines ? ( ) : vmPlacementData?.vm_placement ? (
<Grid container spacing={2}> <Grid container spacing={2}>
{vmPlacementData.physical_machines.map((pm) => ( {Object.values(vmPlacementData.vm_placement).map((pm, index) => (
<Grid item xs={12} sm={6} md={4} key={pm.name}> <Grid item xs={12} sm={6} md={4} key={`pm-${pm.name}-${index}`}>
<Card <Card
sx={{ sx={{
borderRadius: 2, borderRadius: 1,
boxShadow: 2, boxShadow: '0 1px 3px 0 rgba(0, 0, 0, 0.1)',
height: '100%', border: '1px solid',
minHeight: 250, borderColor: theme.palette.divider,
display: 'flex', bgcolor: '#fff'
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={{ <CardContent sx={{ p: 2 }}>
flex: 1, {/* PM Header */}
display: 'flex',
flexDirection: 'column',
}}>
<Box sx={{ <Box sx={{
display: 'flex', display: 'flex',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
mb: 2, mb: 2,
pb: 1, pb: 1.5,
borderBottom: 1, borderBottom: '1px solid',
borderColor: 'divider' borderColor: theme.palette.divider
}}> }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography <Typography
variant="subtitle1"
sx={{ sx={{
fontWeight: 'bold', fontSize: '1rem',
color: theme.palette.primary.main, fontWeight: 600,
display: 'flex', color: theme.palette.text.primary
alignItems: 'center',
gap: 1
}} }}
> >
{pm.name} {pm.name}
{pm.status === 'blocked' && (
<LockIcon sx={{
fontSize: '1rem',
color: theme.palette.warning.main,
opacity: 0.8
}} />
)}
</Typography> </Typography>
</Box>
<Typography <Typography
variant="caption" sx={{
color="text.secondary" fontSize: '0.875rem',
color: theme.palette.text.secondary,
fontWeight: 500
}}
> >
{pm.power_consumption.toFixed(2)}W {pm.power.toFixed(2)}W
</Typography> </Typography>
</Box> </Box>
{/* PM Info */}
<Box sx={{ mb: 2 }}>
<Typography sx={{
fontSize: '0.75rem',
color: theme.palette.text.secondary,
mb: 0.5
}}>
Aggregate: {pm.tag ?? '-'}
</Typography>
<Typography sx={{
fontSize: '0.75rem',
color: theme.palette.text.secondary
}}>
Config: CPU {pm.confg[1]}, RAM {pm.confg[2]}, Disk {pm.confg[3]}
</Typography>
</Box>
{/* VMs Container */}
<Box sx={{ <Box sx={{
flex: 1,
overflowY: 'auto',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column'
gap: 1
}}> }}>
{/* Active VMs */} {Object.values(pm.vms).map((vm, index) => {
{pm.vms.active.map((vm, index) => ( const vmId = `${pm.name}-${vm.vm_name}-${index}`;
<VMCard const isExpanded = expandedVMs[vmId] || false;
key={`${pm.name}-${vm.name}-active-${index}`}
vm={vm} return (
vmId={`${pm.name}-${vm.name}-active-${index}`} <Box key={vmId}>
isActive={true} <Box
expandedVMs={expandedVMs} sx={{
toggleVMDetails={toggleVMDetails} display: 'flex',
theme={theme} alignItems: 'center',
/> p: 1.5,
))} mb: 1,
bgcolor: vm.state === 'active' ? 'rgba(0, 171, 85, 0.08)' : 'transparent',
{/* Inactive VMs */} borderRadius: 1,
{pm.vms.inactive.map((vm, index) => ( '&:hover': {
<VMCard bgcolor: vm.state === 'active' ? 'rgba(0, 171, 85, 0.12)' : 'rgba(145, 158, 171, 0.08)'
key={`${pm.name}-${vm.name}-inactive-${index}`} }
vm={vm} }}
vmId={`${pm.name}-${vm.name}-inactive-${index}`} >
isActive={false} {/* Status Indicator */}
expandedVMs={expandedVMs} <Box sx={{
toggleVMDetails={toggleVMDetails} width: 8,
theme={theme} height: 8,
/> borderRadius: '50%',
))} bgcolor: vm.state === 'active' ? '#00AB55' : '#919EAB',
mr: 2,
{pm.vms.active.length === 0 && pm.vms.inactive.length === 0 && ( flexShrink: 0
}} />
{/* VM Name */}
<Typography sx={{
flex: 1,
color: theme.palette.text.primary,
fontSize: '0.875rem'
}}>
{vm.vm_name}
</Typography>
{/* Power Consumption */}
<Typography sx={{
color: theme.palette.text.secondary,
fontSize: '0.875rem',
mr: 2
}}>
{vm.power.toFixed(2)}W
</Typography>
{/* Info Icon */}
<IconButton
size="small"
onClick={() => toggleVMDetails(vmId)}
sx={{
color: isExpanded ? theme.palette.primary.main : theme.palette.text.secondary,
p: 0.5,
'&:hover': {
color: theme.palette.primary.main
}
}}
>
<InfoOutlinedIcon sx={{ fontSize: 18 }} />
</IconButton>
</Box>
{/* Expandable Details Section */}
<Collapse in={isExpanded}>
<Box sx={{
p: 1.5,
borderTop: `1px solid ${theme.palette.divider}`,
}}>
<List sx={{ p: 0 }}>
{/* Power Consumption */}
<ListItemButton>
<ListItemText
primary="Power Consumption"
secondary={`${vm.power.toFixed(2)}W`}
primaryTypographyProps={{
sx: theme.typography.body2
}}
secondaryTypographyProps={{
sx: {
...theme.typography.body1,
color: theme.palette.primary.main,
fontWeight: 600
}
}}
/>
</ListItemButton>
{/* Configuration */}
<ListItemButton>
<ListItemText
primary="Configuration"
secondary={
<Box sx={{ display: 'flex', gap: 2 }}>
<Typography sx={theme.typography.body1}>
CPU: {vm.confg[1]}
</Typography>
<Typography sx={theme.typography.body1}>
RAM: {vm.confg[2]}
</Typography>
<Typography sx={theme.typography.body1}>
Disk: {vm.confg[3]}
</Typography>
</Box>
}
primaryTypographyProps={{
sx: theme.typography.body2
}}
/>
</ListItemButton>
{/* Project ID */}
<ListItemButton>
<ListItemText
primary="Project ID"
secondary={vm.project ?? '-'}
primaryTypographyProps={{
sx: theme.typography.body2
}}
secondaryTypographyProps={{
sx: theme.typography.body1
}}
/>
</ListItemButton>
{/* Aggregate */}
<ListItemButton>
<ListItemText
primary="Aggregate"
secondary={vm.tag ?? '-'}
primaryTypographyProps={{
sx: theme.typography.body2
}}
secondaryTypographyProps={{
sx: theme.typography.body1
}}
/>
</ListItemButton>
</List>
</Box>
</Collapse>
</Box>
);
})}
{Object.values(pm.vms).length === 0 && (
<Box sx={{ <Box sx={{
display: 'flex', display: 'flex',
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
height: '100%', py: 1,
color: theme.palette.text.secondary, color: theme.palette.text.secondary,
}}> }}>
<Typography variant="body2">No VMs running</Typography> <Typography variant="body2">No VMs running</Typography>

View File

@@ -1,11 +1,37 @@
import { useEffect, useState, useCallback, useRef } from 'react'; 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 { Box, Paper, Typography, Fade, useTheme, Grid, AppBar, Toolbar, CircularProgress, IconButton, Tooltip as MuiTooltip, Chip, Button, Snackbar, Alert, Slider } from '@mui/material';
import RefreshIcon from '@mui/icons-material/Refresh'; import RefreshIcon from '@mui/icons-material/Refresh';
import CheckCircleIcon from '@mui/icons-material/CheckCircle'; import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import CancelIcon from '@mui/icons-material/Cancel'; import CancelIcon from '@mui/icons-material/Cancel';
import { LineChart } from '@mui/x-charts/LineChart'; import { monitoringService } from '../services/monitoringService';
import { MonitoringStatus } from '../types/monitoring';
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip as ChartTooltip,
Legend,
TimeScale
} from 'chart.js';
import 'chartjs-adapter-date-fns';
import { Line } from 'react-chartjs-2';
import { config } from '../config/env'; import { config } from '../config/env';
// Register Chart.js components
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
ChartTooltip,
Legend,
TimeScale
);
// Define the structure of our data // Define the structure of our data
interface ChartData { interface ChartData {
power: string; power: string;
@@ -26,12 +52,16 @@ const Temperature = () => {
const [lastUpdated, setLastUpdated] = useState<Date | null>(null); const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
const [refreshing, setRefreshing] = useState(false); const [refreshing, setRefreshing] = useState(false);
const [decisionLoading, setDecisionLoading] = useState(false); const [decisionLoading, setDecisionLoading] = useState(false);
const [windowSize, setWindowSize] = useState(20);
const [alert, setAlert] = useState<{ open: boolean; message: string; severity: 'success' | 'error' }>({ const [alert, setAlert] = useState<{ open: boolean; message: string; severity: 'success' | 'error' }>({
open: false, open: false,
message: '', message: '',
severity: 'success' severity: 'success'
}); });
// Monitoring status state
const [monitoringStatus, setMonitoringStatus] = useState<MonitoringStatus | null>(null);
// Use refs to keep track of the interval // Use refs to keep track of the interval
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null); const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
const updateIntervalMs = 5000; // 5 seconds refresh rate const updateIntervalMs = 5000; // 5 seconds refresh rate
@@ -113,7 +143,20 @@ const Temperature = () => {
}; };
}, [fetchData]); }, [fetchData]);
// Process data for charts // Set up monitoring status polling
useEffect(() => {
// Start polling for monitoring status
monitoringService.startStatusPolling((status) => {
setMonitoringStatus(status);
}, 5000); // Poll every 5 seconds
// Cleanup function to stop polling when component unmounts
return () => {
monitoringService.stopStatusPolling();
};
}, []);
// Process data for charts with sliding window
const prepareChartData = () => { const prepareChartData = () => {
if (!data || data.length === 0) { if (!data || data.length === 0) {
console.log('No data available, using fallback data'); console.log('No data available, using fallback data');
@@ -130,7 +173,7 @@ const Temperature = () => {
return fallbackData; return fallbackData;
} }
return data.map(item => ({ const processedData = data.map(item => ({
currentTimestamp: new Date(item.now_timestamp), currentTimestamp: new Date(item.now_timestamp),
futureTimestamp: new Date(item.future_timestamp), futureTimestamp: new Date(item.future_timestamp),
currentPower: parseFloat(item.power), currentPower: parseFloat(item.power),
@@ -138,20 +181,154 @@ const Temperature = () => {
currentTemp: parseFloat(item.env_temp_cur), currentTemp: parseFloat(item.env_temp_cur),
predictedTemp: parseFloat(item.env_temp_min), predictedTemp: parseFloat(item.env_temp_min),
})); }));
// Apply sliding window - show only last N records
const slidingData = processedData.slice(-windowSize);
console.log('Processed chart data:', {
totalRecords: processedData.length,
showingRecords: slidingData.length,
timeRange: {
start: slidingData[0]?.currentTimestamp,
end: slidingData[slidingData.length - 1]?.currentTimestamp
}
});
return slidingData;
}; };
const chartData = prepareChartData(); const chartData = prepareChartData();
// Extract data arrays for charts // Prepare Chart.js data structures
const currentTimestamps = chartData.map(item => item.currentTimestamp); const powerChartData = {
const futureTimestamps = chartData.map(item => item.futureTimestamp); datasets: [
const currentPowerData = chartData.map(item => item.currentPower); {
const predictedPowerData = chartData.map(item => item.predictedPower); label: 'Current Power',
const currentTempData = chartData.map(item => item.currentTemp); data: chartData.map(item => ({
const predictedTempData = chartData.map(item => item.predictedTemp); x: item.currentTimestamp,
y: item.currentPower
})),
borderColor: '#028a4a',
backgroundColor: '#028a4a',
pointBackgroundColor: '#028a4a',
pointBorderColor: '#028a4a',
pointRadius: 0,
pointHoverRadius: 6,
tension: 0.4,
fill: false
},
{
label: 'Predicted Power (Dynamic)',
data: chartData.map(item => ({
x: item.futureTimestamp,
y: item.predictedPower
})),
borderColor: '#ff9800',
backgroundColor: '#ff9800',
pointBackgroundColor: '#ff9800',
pointBorderColor: '#ff9800',
pointRadius: 0,
pointHoverRadius: 6,
tension: 0.4,
fill: false
}
]
};
const temperatureChartData = {
datasets: [
{
label: 'Current Temperature',
data: chartData.map(item => ({
x: item.currentTimestamp,
y: item.currentTemp
})),
borderColor: '#028a4a',
backgroundColor: '#028a4a',
pointBackgroundColor: '#028a4a',
pointBorderColor: '#028a4a',
pointRadius: 0,
pointHoverRadius: 6,
tension: 0.4,
fill: false
},
{
label: 'Predicted Temperature (Dynamic)',
data: chartData.map(item => ({
x: item.futureTimestamp,
y: item.predictedTemp
})),
borderColor: '#ff9800',
backgroundColor: '#ff9800',
pointBackgroundColor: '#ff9800',
pointBorderColor: '#ff9800',
pointRadius: 0,
pointHoverRadius: 6,
tension: 0.4,
fill: false
}
]
};
// Chart.js options
const chartOptions = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'top' as const,
labels: {
usePointStyle: true,
padding: 20,
font: {
size: 12,
weight: 'normal' as const
}
}
},
tooltip: {
mode: 'index' as const,
intersect: false,
callbacks: {
title: function(context: any) {
const date = new Date(context[0].parsed.x);
return date.toLocaleTimeString();
},
label: function(context: any) {
return `${context.dataset.label}: ${context.parsed.y.toFixed(2)}`;
}
}
}
},
scales: {
x: {
type: 'time' as const,
time: {
displayFormats: {
minute: 'HH:mm:ss'
}
},
title: {
display: true,
text: 'Time'
}
},
y: {
title: {
display: true,
text: 'Value'
}
}
},
interaction: {
mode: 'nearest' as const,
axis: 'x' as const,
intersect: false
}
};
// Debug logging // Debug logging
console.log('Chart data:', chartData); console.log('Chart.js data structures:', { powerChartData, temperatureChartData });
console.log('Raw data length:', data.length); console.log('Raw data length:', data.length);
// Handle temperature decision // Handle temperature decision
@@ -206,19 +383,35 @@ const Temperature = () => {
}} }}
> >
<Toolbar sx={{ px: { xs: 2, sm: 4 } }}> <Toolbar sx={{ px: { xs: 2, sm: 4 } }}>
<Typography <Box sx={{ flex: 1, textAlign: "center" }}>
variant="h5" <Typography
component="h1" variant="h5"
sx={{ component="h1"
color: 'text.primary', sx={{
fontWeight: 500, color: 'text.primary',
flex: 1, fontWeight: 500,
textAlign: "center", letterSpacing: '-0.5px',
letterSpacing: '-0.5px' mb: 0.5
}} }}
> >
Environmental Temperature & Power Monitoring (Last 20 Records) Environmental Temperature & Power Monitoring
</Typography> </Typography>
<Chip
label={`Showing last ${windowSize} records`}
variant="outlined"
size="small"
sx={{
height: 24,
'& .MuiChip-label': {
px: 1.5,
fontSize: '0.75rem',
fontWeight: 500
},
borderColor: 'rgba(0, 0, 0, 0.2)',
color: 'rgba(0, 0, 0, 0.7)'
}}
/>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center' }}> <Box sx={{ display: 'flex', alignItems: 'center' }}>
{lastUpdated && ( {lastUpdated && (
<Typography <Typography
@@ -229,7 +422,7 @@ const Temperature = () => {
Last updated: {lastUpdated.toLocaleTimeString()} Last updated: {lastUpdated.toLocaleTimeString()}
</Typography> </Typography>
)} )}
<Tooltip title="Refresh data"> <MuiTooltip title="Refresh data">
<IconButton <IconButton
onClick={handleRefresh} onClick={handleRefresh}
color="primary" color="primary"
@@ -244,59 +437,12 @@ const Temperature = () => {
> >
<RefreshIcon /> <RefreshIcon />
</IconButton> </IconButton>
</Tooltip> </MuiTooltip>
</Box> </Box>
</Toolbar> </Toolbar>
</AppBar> </AppBar>
<Box sx={{ p: { xs: 2, sm: 4 } }}> <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}> <Fade in timeout={800}>
<Grid container spacing={3}> <Grid container spacing={3}>
@@ -365,44 +511,26 @@ const Temperature = () => {
</Box> </Box>
) : ( ) : (
<Box sx={{ height: 400 }}> <Box sx={{ height: 400 }}>
<LineChart <Line
height={400} data={powerChartData}
skipAnimation={false} options={{
series={[ ...chartOptions,
{ scales: {
data: predictedPowerData, ...chartOptions.scales,
label: 'Predicted Power (3min)', y: {
color: '#ff9800', ...chartOptions.scales.y,
showMark: false, title: {
curve: 'linear' display: true,
}, text: 'Power (W)'
{ },
data: currentPowerData, ticks: {
label: 'Current Power', callback: function(value: any) {
color: '#028a4a', // B'GREEN brand color return `${value} W`;
showMark: false }
}, }
]} }
xAxis={[
{
scaleType: 'time',
data: [...currentTimestamps, ...futureTimestamps],
valueFormatter: (value: Date) => value.toLocaleTimeString(),
},
]}
yAxis={[
{
width: 50,
valueFormatter: (value: number) => `${value} W`,
} }
]} }}
margin={{ right: 24 }}
slotProps={{
legend: {
direction: 'horizontal',
position: { vertical: 'top', horizontal: 'center' },
},
}}
/> />
</Box> </Box>
)} )}
@@ -474,43 +602,26 @@ const Temperature = () => {
</Box> </Box>
) : ( ) : (
<Box sx={{ height: 400 }}> <Box sx={{ height: 400 }}>
<LineChart <Line
height={400} data={temperatureChartData}
skipAnimation={false} options={{
series={[ ...chartOptions,
{ scales: {
data: predictedTempData, ...chartOptions.scales,
label: 'Predicted Temperature (3min)', y: {
color: '#ff9800', ...chartOptions.scales.y,
showMark: false title: {
}, display: true,
{ text: 'Temperature (°C)'
data: currentTempData, },
label: 'Current Temperature', ticks: {
color: '#028a4a', // B'GREEN brand color callback: function(value: any) {
showMark: false return `${value} °C`;
}, }
]} }
xAxis={[ }
{
scaleType: 'time',
data: [...currentTimestamps, ...futureTimestamps],
valueFormatter: (value: Date) => value.toLocaleTimeString(),
},
]}
yAxis={[
{
width: 50,
valueFormatter: (value: number) => `${value} °C`,
} }
]} }}
margin={{ right: 24 }}
slotProps={{
legend: {
direction: 'horizontal',
position: { vertical: 'top', horizontal: 'center' },
},
}}
/> />
</Box> </Box>
)} )}
@@ -518,6 +629,136 @@ const Temperature = () => {
</Grid> </Grid>
</Grid> </Grid>
</Fade> </Fade>
{/* Chart Controls */}
<Paper
elevation={0}
sx={{
p: 3,
mt: 3,
bgcolor: 'background.paper',
borderRadius: 2,
border: `1px solid ${theme.palette.divider}`,
}}
>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 3 }}>
<Box>
<Typography variant="h6" sx={{ color: 'text.primary', mb: 1, fontWeight: 500 }}>
Chart Settings
</Typography>
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
Configure chart display and temperature change proposals
</Typography>
</Box>
{/* Temperature Change Proposal Section */}
<Box sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-end',
gap: 1,
minWidth: 200
}}>
<Typography variant="subtitle2" sx={{ color: 'text.primary', fontWeight: 600 }}>
Temperature Change Proposal
</Typography>
{(!monitoringStatus?.statuses?.environmental?.is_running || !monitoringStatus?.statuses?.preventive?.is_running) ? (
<Box sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
p: 1,
borderRadius: 1,
bgcolor: 'warning.light',
color: 'warning.contrastText'
}}>
<CircularProgress size={16} thickness={4} />
<Typography variant="caption" sx={{ fontSize: '0.75rem' }}>
Waiting for services...
</Typography>
</Box>
) : data.length > 0 && chartData.some(item => !isNaN(item.currentTemp) && item.currentTemp > 0) ? (
<Box sx={{ display: 'flex', gap: 1 }}>
<Button
variant="contained"
color="success"
size="small"
startIcon={<CheckCircleIcon />}
onClick={() => handleTemperatureDecision(true)}
disabled={decisionLoading}
sx={{
borderRadius: 1.5,
textTransform: 'none',
fontSize: '0.75rem',
px: 2,
py: 0.5,
minWidth: 80,
boxShadow: 1,
'&:hover': {
boxShadow: 2,
transform: 'translateY(-1px)'
},
transition: 'all 0.2s ease-in-out'
}}
>
Approve
</Button>
<Button
variant="contained"
color="error"
size="small"
startIcon={<CancelIcon />}
onClick={() => handleTemperatureDecision(false)}
disabled={decisionLoading}
sx={{
borderRadius: 1.5,
textTransform: 'none',
fontSize: '0.75rem',
px: 2,
py: 0.5,
minWidth: 80,
boxShadow: 1,
'&:hover': {
boxShadow: 2,
transform: 'translateY(-1px)'
},
transition: 'all 0.2s ease-in-out'
}}
>
Decline
</Button>
</Box>
) : (
<Typography variant="caption" sx={{ color: 'text.secondary', fontSize: '0.75rem' }}>
No temperature data available
</Typography>
)}
</Box>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Typography variant="body2" sx={{ minWidth: 120, fontWeight: 500 }}>
Records to show:
</Typography>
<Slider
value={windowSize}
onChange={(_, value) => setWindowSize(value as number)}
min={5}
max={50}
step={5}
marks={[
{ value: 5, label: '5' },
{ value: 20, label: '20' },
{ value: 50, label: '50' }
]}
sx={{ flex: 1, maxWidth: 200 }}
/>
<Typography variant="body2" sx={{ minWidth: 40, fontWeight: 500 }}>
{windowSize}
</Typography>
</Box>
</Paper>
</Box> </Box>
{/* Snackbar for alerts */} {/* Snackbar for alerts */}