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",
"@types/plotly.js": "^2.35.2",
"axios": "^1.8.4",
"chart.js": "^4.4.1",
"date-fns": "^3.0.6",
"chart.js": "^4.5.0",
"chartjs-adapter-date-fns": "^3.0.0",
"date-fns": "^3.6.0",
"lucide-react": "^0.525.0",
"plotly.js": "^3.0.1",
"plotly.js-dist": "^3.0.1",
"plotly.js-dist-min": "^3.0.1",
"react": "^18.2.0",
"react-chartjs-2": "^5.2.0",
"react-chartjs-2": "^5.3.0",
"react-dom": "^18.2.0",
"react-google-charts": "^5.2.1",
"react-plotly.js": "^2.6.0",
@@ -3409,7 +3410,6 @@
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz",
"integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==",
"license": "MIT",
"dependencies": {
"@kurkle/color": "^0.3.0"
},
@@ -3417,6 +3417,15 @@
"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": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/clamp/-/clamp-1.0.1.tgz",
@@ -3898,7 +3907,6 @@
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz",
"integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
@@ -6648,7 +6656,6 @@
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.0.tgz",
"integrity": "sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==",
"license": "MIT",
"peerDependencies": {
"chart.js": "^4.1.1",
"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",
"@types/plotly.js": "^2.35.2",
"axios": "^1.8.4",
"chart.js": "^4.4.1",
"date-fns": "^3.0.6",
"chart.js": "^4.5.0",
"chartjs-adapter-date-fns": "^3.0.0",
"date-fns": "^3.6.0",
"lucide-react": "^0.525.0",
"plotly.js": "^3.0.1",
"plotly.js-dist": "^3.0.1",
"plotly.js-dist-min": "^3.0.1",
"react": "^18.2.0",
"react-chartjs-2": "^5.2.0",
"react-chartjs-2": "^5.3.0",
"react-dom": "^18.2.0",
"react-google-charts": "^5.2.1",
"react-plotly.js": "^2.6.0",

View File

@@ -8,8 +8,23 @@ import Maintenance from './pages/Maintenance';
import Migration from './pages/Migration';
import MonitoringSystem from './pages/MonitoringSystem';
import StressTesting from './pages/StressTesting';
import { useEffect } from 'react';
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 (
<ThemeProvider theme={theme}>
<CssBaseline />

View File

@@ -120,7 +120,28 @@ const Sidebar = ({ open, onToggle, isMobile }: SidebarProps) => {
const drawerContent = (
<>
<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
src={bgreenLogo}
alt="B'GREEN Logo"

View File

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

View File

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

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

View File

@@ -70,7 +70,7 @@ const VerifiedMigration: React.FC<VerifiedMigrationProps> = ({
Current Power
</Typography>
<Typography variant="h4" sx={{ fontWeight: 'bold', mb: 0.5 }}>
{gainAfterData.cur_power.toFixed(2)} W
{gainAfterData.cur_power.toFixed(2)}W
</Typography>
</Box>
</Grid>
@@ -91,7 +91,7 @@ const VerifiedMigration: React.FC<VerifiedMigrationProps> = ({
<Typography variant="h4" sx={{
fontWeight: 'bold',
mb: 0.5,
color: isValidated ? '#4caf50' : '#ff9800'
color: isValidated ? '#28c76f' : '#ffb400'
}}>
{(gainAfterData.val_ratio * 100).toFixed(2)}%
</Typography>
@@ -114,7 +114,7 @@ const VerifiedMigration: React.FC<VerifiedMigrationProps> = ({
<Typography variant="h4" sx={{
fontWeight: 'bold',
mb: 0.5,
color: gainAfterData.actual_ratio > 0 ? '#4caf50' : '#ff9800'
color: gainAfterData.actual_ratio > 0 ? '#28c76f' : '#FF1744'
}}>
{(gainAfterData.actual_ratio * 100).toFixed(2)}%
</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 { config } from '../../config/env';
@@ -15,6 +15,76 @@ interface GainAfterData {
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 = () => {
const [gainBeforeData, setGainBeforeData] = useState<GainBeforeData | null>(null);
const [migrationAdviceData, setMigrationAdviceData] = useState<MigrationAdviceData | null>(null);
@@ -125,7 +195,7 @@ export const useMonitoringData = () => {
};
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 toggleVMDetails = (vmId: string) => {
@@ -135,28 +205,21 @@ export const useVMDetails = () => {
}));
};
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);
const fetchVMDetails = async (vmName: string) => {
try {
const response = await fetch(`${API_BASE_URL}/prom/vm_details/${vmName}`);
if (!response.ok) {
throw new Error('Failed to fetch VM details');
}
};
const data = await response.json();
setVmDetails(data);
} catch (error) {
console.error('Error fetching VM details:', error);
setVmDetails(null);
}
};
fetchVMDetails();
const interval = setInterval(fetchVMDetails, REFRESH_INTERVAL);
return () => clearInterval(interval);
}, []);
return { vmDetails, expandedVMs, toggleVMDetails };
return { vmDetails, expandedVMs, toggleVMDetails, fetchVMDetails };
};
export const useGainAfterData = () => {
@@ -167,19 +230,18 @@ export const useGainAfterData = () => {
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');
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);
console.error('Error fetching gain after data:', error);
setGainAfterData(null);
} finally {
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 {
labels: string[];
datasets: {

View File

@@ -5,7 +5,7 @@ const getApiUrl = (): string => {
return '/api';
}
// 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 = {

View File

@@ -232,7 +232,8 @@ const Home = () => {
const [migrationTime, setMigrationTime] = useState<string>('5');
const [migrationModel, setMigrationModel] = useState<string>('mul_reg');
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);
@@ -517,10 +518,10 @@ const Home = () => {
<Typography variant="body2">Time Configuration</Typography>
</IconWrapper>
<FormControl fullWidth sx={{ mb: 2 }}>
<InputLabel>Script Time Unit</InputLabel>
<InputLabel>{"{script time unit (mins)}"}</InputLabel>
<StyledSelect
value={envTimeUnit}
label="Script Time Unit"
label="{script time unit (mins)}"
onChange={handleSelectChange}
name="envTimeUnit"
>
@@ -535,10 +536,10 @@ const Home = () => {
<Typography variant="body2">Steps Configuration</Typography>
</IconWrapper>
<FormControl fullWidth sx={{ mb: 2 }}>
<InputLabel>Number of Steps</InputLabel>
<InputLabel>Estimation Steps</InputLabel>
<StyledSelect
value={envSteps}
label="Number of Steps"
label="Estimation Steps"
onChange={handleSelectChange}
name="envSteps"
>
@@ -581,10 +582,10 @@ const Home = () => {
<Typography variant="body2">Time Configuration</Typography>
</IconWrapper>
<FormControl fullWidth sx={{ mb: 2 }}>
<InputLabel>Script Time Unit</InputLabel>
<InputLabel>{"{script time unit (mins)}"}</InputLabel>
<StyledSelect
value={prevTimeUnit}
label="Script Time Unit"
label="{script time unit (mins)}"
onChange={handleSelectChange}
name="prevTimeUnit"
>
@@ -599,10 +600,10 @@ const Home = () => {
<Typography variant="body2">Steps Configuration</Typography>
</IconWrapper>
<FormControl fullWidth sx={{ mb: 2 }}>
<InputLabel>Number of Steps</InputLabel>
<InputLabel>Estimation Steps</InputLabel>
<StyledSelect
value={prevSteps}
label="Number of Steps"
label="Estimation Steps"
onChange={handleSelectChange}
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" />
Auto
</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" />
Semi-Auto
</ToggleButton>
@@ -681,10 +690,10 @@ const Home = () => {
<Typography variant="body2">Time Configuration</Typography>
</IconWrapper>
<FormControl fullWidth sx={{ mb: 2 }}>
<InputLabel>Script Time Unit </InputLabel>
<InputLabel>{"{script time unit (mins)}"}</InputLabel>
<StyledSelect
value={migrationTime}
label="Script Time Unit "
label="{script time unit (mins)}"
onChange={handleSelectChange}
name="migrationTime"
>

View File

@@ -1,8 +1,32 @@
import { useState, useEffect } from 'react';
import { Box, Paper, Typography, Fade, useTheme, AppBar, Toolbar, Chip } from '@mui/material';
import { LineChart } from '@mui/x-charts/LineChart';
import { Box, Paper, Typography, Fade, useTheme, AppBar, Toolbar, Chip, Slider, FormControlLabel, Switch } from '@mui/material';
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';
// Register Chart.js components
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
TimeScale
);
interface DataItem {
now_timestamp: string;
future_timestamp: string;
@@ -23,6 +47,7 @@ const Maintenance = () => {
const [data, setData] = useState<DataItem[]>([]);
const [currentFlag, setCurrentFlag] = useState<string>('');
const [, setLoading] = useState(true);
const [windowSize, setWindowSize] = useState(20);
useEffect(() => {
const fetchData = async () => {
@@ -49,7 +74,7 @@ const Maintenance = () => {
return () => clearInterval(interval);
}, []);
// Process data for charts
// Process data for charts with sliding window
const prepareChartData = () => {
if (!data || data.length === 0) {
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:', {
hasCurrentPower: processedData.some(d => d.currentPower > 0),
hasPredictedPower: processedData.some(d => d.predictedPower > 0),
currentPowerRange: [Math.min(...processedData.map(d => d.currentPower)), Math.max(...processedData.map(d => d.currentPower))],
predictedPowerRange: [Math.min(...processedData.map(d => d.predictedPower)), Math.max(...processedData.map(d => d.predictedPower))],
rawDataSample: data.slice(0, 2).map(item => ({
hasCurrentPower: slidingData.some(d => d.currentPower > 0),
hasPredictedPower: slidingData.some(d => d.predictedPower > 0),
currentPowerRange: [Math.min(...slidingData.map(d => d.currentPower)), Math.max(...slidingData.map(d => d.currentPower))],
predictedPowerRange: [Math.min(...slidingData.map(d => d.predictedPower)), Math.max(...slidingData.map(d => d.predictedPower))],
rawDataSample: data.slice(-2).map(item => ({
power: item.power,
power_future_min: item.power_future_min,
parsedCurrent: parseFloat(item.power),
parsedPredicted: parseFloat(item.power_future_min)
}))
});
return processedData;
return slidingData;
};
const chartData = prepareChartData();
// Extract data arrays for charts
const currentTimestamps = chartData.map(item => item.currentTimestamp);
const futureTimestamps = chartData.map(item => item.futureTimestamp);
const currentPowerData = chartData.map(item => item.currentPower);
const predictedPowerData = chartData.map(item => item.predictedPower);
const positive3pData = chartData.map(item => item.positive3p);
const negative3pData = chartData.map(item => item.negative3p);
const positive7pData = chartData.map(item => item.positive7p);
const negative7pData = chartData.map(item => item.negative7p);
// Prepare Chart.js data structure
const chartJsData = {
datasets: [
{
label: 'Current Power',
data: chartData.map(item => ({
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 (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
console.log('Chart arrays:', {
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),
});
console.log('Chart.js data structure:', chartJsData);
return (
<Box sx={{ flexGrow: 1, bgcolor: theme.palette.background.default }}>
@@ -156,33 +313,50 @@ const Maintenance = () => {
>
Preventive Maintenance
</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
label={currentFlag}
color={currentFlag === 'Correct Estimation for PM energy' ? 'success' : 'warning'}
size="medium"
label={`Showing last ${windowSize} records`}
variant="outlined"
size="small"
sx={{
height: 32,
height: 24,
'& .MuiChip-label': {
px: 2,
fontSize: '0.875rem',
fontWeight: 600
px: 1.5,
fontSize: '0.75rem',
fontWeight: 500
},
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)',
},
}
borderColor: 'rgba(0, 0, 0, 0.2)',
color: 'rgba(0, 0, 0, 0.7)'
}}
/>
)}
</Box>
</Toolbar>
</AppBar>
@@ -197,71 +371,42 @@ const Maintenance = () => {
border: `1px solid ${theme.palette.divider}`,
}}
>
<Box sx={{ height: 'calc(100vh - 200px)', minHeight: '500px' }}>
<LineChart
height={500}
skipAnimation={false}
series={[
{
data: currentPowerData,
label: 'Current Power',
color: '#028a4a', // B'GREEN brand color
showMark: true,
curve: 'linear'
},
{
data: predictedPowerData,
label: 'Predicted (3min)',
color: '#ff9800',
showMark: true,
curve: 'linear'
},
{
data: positive3pData,
label: '+3% Positive',
color: '#2ca02c',
showMark: false
},
{
data: negative3pData,
label: '-3% Negative',
color: '#d62728',
showMark: false
},
{
data: positive7pData,
label: '+7% Positive',
color: '#9467bd',
showMark: false
},
{
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 sx={{ height: 'calc(100vh - 280px)', minHeight: '500px' }}>
<Line data={chartJsData} options={chartOptions} />
</Box>
{/* Chart Controls */}
<Box sx={{
mt: 2,
p: 2,
bgcolor: 'background.default',
borderRadius: 1,
border: `1px solid ${theme.palette.divider}`
}}>
<Typography variant="body2" sx={{ mb: 1, fontWeight: 500 }}>
Chart Settings
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Typography variant="body2" sx={{ minWidth: 120 }}>
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 }}>
{windowSize}
</Typography>
</Box>
</Box>
</Paper>
</Fade>

View File

@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, useCallback } from 'react';
import {
Box,
Paper,
@@ -16,6 +16,9 @@ import {
DialogContent,
DialogActions,
LinearProgress,
List,
ListItemButton,
ListItemText,
} from '@mui/material';
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
@@ -28,44 +31,15 @@ import SummaryStats from '../components/Migration/SummaryStats';
import ResourceDistributionChart from '../components/Migration/ResourceDistributionChart';
import MigrationAdviceCard from '../components/Migration/MigrationAdviceCard';
import VerifiedMigration from '../components/Migration/VerifiedMigration';
import { useMigrationData, useGainAfterData } from '../components/Migration/hooks';
import { useMigrationData, useGainAfterData, useSmartPolling } from '../components/Migration/hooks';
import { config } from '../config/env';
import { VMPlacementData } from '../components/Migration/types';
// Constants
const API_BASE_URL = config.apiUrl;
const REFRESH_INTERVAL = 30000; // 30 seconds
interface VMPlacementData {
data_center: string;
id: number;
physical_machines: Array<{
status: 'blocked' | 'open';
name: string;
power_consumption: number;
vms: {
active: Array<{
status: 'blocked' | 'open';
name: string;
power: number;
confg: {
cpu: number;
ram: number;
disk: number;
};
}>;
inactive: Array<{
status: 'blocked' | 'open';
name: string;
power: number;
confg: {
cpu: number;
ram: number;
disk: number;
};
}>;
};
}>;
}
interface VMCardProps {
vm: {
@@ -367,8 +341,6 @@ const Migration = () => {
const theme = useTheme();
// Essential states
const [vmPlacementData, setVmPlacementData] = useState<VMPlacementData | null>(null);
const [isLoadingVmPlacement, setIsLoadingVmPlacement] = useState(false);
const [expandedVMs, setExpandedVMs] = useState<Record<string, boolean>>({});
const [showVerifiedSection, setShowVerifiedSection] = useState(false);
const [isCardExpanded, setIsCardExpanded] = useState(false);
@@ -378,36 +350,37 @@ const Migration = () => {
const [showProgress, setShowProgress] = 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
const { gainBeforeData, migrationAdviceData, isLoadingGainData, fetchMigrationData } = useMigrationData();
const { gainAfterData, isLoading: isLoadingGainAfter, fetchGainAfterData } = useGainAfterData();
// Essential functions
const toggleVMDetails = (vmId: string) => {
const toggleVMDetails = useCallback((vmId: string) => {
setExpandedVMs(prev => ({
...prev,
[vmId]: !prev[vmId]
}));
};
}, []);
const fetchVmPlacementData = async () => {
try {
setIsLoadingVmPlacement(true);
const response = await fetch(`${API_BASE_URL}/prom/get_chart_data/vm_placement`);
if (!response.ok) {
throw new Error(`Failed to fetch VM placement data: ${response.status}`);
}
const data = await response.json();
console.log('Raw API response:', data); // Debug log
setVmPlacementData(data); // Use the data directly since it already has the correct structure
} catch (error) {
console.error('Error fetching VM placement data:', error);
} finally {
setIsLoadingVmPlacement(false);
}
};
const handleApproveMigration = async () => {
const handleApproveMigration = useCallback(async () => {
try {
setIsProcessing(true);
setMigrationProgress([]);
@@ -449,9 +422,9 @@ const Migration = () => {
} finally {
setIsProcessing(false);
}
};
}, [fetchGainAfterData]);
const handleDeclineMigration = async () => {
const handleDeclineMigration = useCallback(async () => {
try {
setIsProcessing(true);
@@ -474,40 +447,37 @@ const Migration = () => {
} finally {
setIsProcessing(false);
}
};
// Data fetching effect
useEffect(() => {
console.log('Initial data fetch');
fetchVmPlacementData();
const intervalId = setInterval(() => {
console.log('Interval data fetch');
fetchVmPlacementData();
}, REFRESH_INTERVAL);
return () => clearInterval(intervalId);
}, []);
// Add effect to monitor vmPlacementData changes
useEffect(() => {
if (vmPlacementData) {
const blockedPMs = vmPlacementData.physical_machines.filter(pm => pm.status === 'blocked').length;
const blockedVMs = vmPlacementData.physical_machines.reduce((acc, pm) => {
const activeBlocked = pm.vms.active.filter(vm => vm.status === 'blocked').length;
const inactiveBlocked = pm.vms.inactive.filter(vm => vm.status === 'blocked').length;
return acc + activeBlocked + inactiveBlocked;
}, 0);
let totalPMs = 0;
let totalVMs = 0;
let activeVMs = 0;
let inactiveVMs = 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:', {
timestamp: new Date().toISOString(),
pmCount: vmPlacementData.physical_machines.length,
blockedPMs,
totalVMs: vmPlacementData.physical_machines.reduce((acc, pm) =>
acc + pm.vms.active.length + pm.vms.inactive.length, 0
),
blockedVMs
pmCount: totalPMs,
totalVMs,
activeVMs,
inactiveVMs,
dataCenter: vmPlacementData.data_center,
pollingInterval: `${pollingInterval}ms`
});
}
}, [vmPlacementData]);
}, [vmPlacementData, pollingInterval]);
return (
<Box sx={{ display: 'flex', flexDirection: 'column', maxWidth: '100vw', minHeight: '100vh' }}>
@@ -568,7 +538,7 @@ const Migration = () => {
variant="contained"
color="error"
onClick={handleDeclineMigration}
disabled={isProcessing}
disabled={isProcessing || !migrationAdviceData || Object.keys(migrationAdviceData).length === 0}
sx={{
py: 1.5,
px: 4,
@@ -588,7 +558,7 @@ const Migration = () => {
variant="contained"
startIcon={!isProcessing && <PowerSettingsNewIcon />}
onClick={handleApproveMigration}
disabled={isProcessing}
disabled={isProcessing || !migrationAdviceData || Object.keys(migrationAdviceData).length === 0}
sx={{
py: 1.5,
px: 4,
@@ -619,127 +589,236 @@ const Migration = () => {
</Grid>
)}
{/* PM & VM Monitoring Section */}
{/* PMs & VMs Monitoring Section */}
<Grid item xs={12}>
<Paper sx={{ p: 2, bgcolor: 'background.paper', boxShadow: 3 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="h6">PMs & VMs Monitoring</Typography>
<Typography variant="caption" color="text.secondary">
Polling: {pollingInterval / 1000}s
</Typography>
</Box>
{isLoadingVmPlacement ? (
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
<CircularProgress />
</Box>
) : vmPlacementData?.physical_machines ? (
) : vmPlacementData?.vm_placement ? (
<Grid container spacing={2}>
{vmPlacementData.physical_machines.map((pm) => (
<Grid item xs={12} sm={6} md={4} key={pm.name}>
{Object.values(vmPlacementData.vm_placement).map((pm, index) => (
<Grid item xs={12} sm={6} md={4} key={`pm-${pm.name}-${index}`}>
<Card
sx={{
borderRadius: 2,
boxShadow: 2,
height: '100%',
minHeight: 250,
display: 'flex',
flexDirection: 'column',
position: 'relative',
overflow: 'hidden',
borderWidth: 1,
borderStyle: 'solid',
borderColor: pm.status === 'blocked' ? theme.palette.warning.light : 'transparent',
'&::before': pm.status === 'blocked' ? {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: '2px',
bgcolor: theme.palette.warning.main
} : undefined
borderRadius: 1,
boxShadow: '0 1px 3px 0 rgba(0, 0, 0, 0.1)',
border: '1px solid',
borderColor: theme.palette.divider,
bgcolor: '#fff'
}}
>
<CardContent sx={{
flex: 1,
display: 'flex',
flexDirection: 'column',
}}>
<CardContent sx={{ p: 2 }}>
{/* PM Header */}
<Box sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
mb: 2,
pb: 1,
borderBottom: 1,
borderColor: 'divider'
pb: 1.5,
borderBottom: '1px solid',
borderColor: theme.palette.divider
}}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography
variant="subtitle1"
sx={{
fontWeight: 'bold',
color: theme.palette.primary.main,
display: 'flex',
alignItems: 'center',
gap: 1
fontSize: '1rem',
fontWeight: 600,
color: theme.palette.text.primary
}}
>
{pm.name}
{pm.status === 'blocked' && (
<LockIcon sx={{
fontSize: '1rem',
color: theme.palette.warning.main,
opacity: 0.8
}} />
)}
</Typography>
</Box>
<Typography
variant="caption"
color="text.secondary"
sx={{
fontSize: '0.875rem',
color: theme.palette.text.secondary,
fontWeight: 500
}}
>
{pm.power_consumption.toFixed(2)}W
{pm.power.toFixed(2)}W
</Typography>
</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={{
flex: 1,
overflowY: 'auto',
display: 'flex',
flexDirection: 'column',
gap: 1
flexDirection: 'column'
}}>
{/* Active VMs */}
{pm.vms.active.map((vm, index) => (
<VMCard
key={`${pm.name}-${vm.name}-active-${index}`}
vm={vm}
vmId={`${pm.name}-${vm.name}-active-${index}`}
isActive={true}
expandedVMs={expandedVMs}
toggleVMDetails={toggleVMDetails}
theme={theme}
/>
))}
{/* Inactive VMs */}
{pm.vms.inactive.map((vm, index) => (
<VMCard
key={`${pm.name}-${vm.name}-inactive-${index}`}
vm={vm}
vmId={`${pm.name}-${vm.name}-inactive-${index}`}
isActive={false}
expandedVMs={expandedVMs}
toggleVMDetails={toggleVMDetails}
theme={theme}
/>
))}
{pm.vms.active.length === 0 && pm.vms.inactive.length === 0 && (
{Object.values(pm.vms).map((vm, index) => {
const vmId = `${pm.name}-${vm.vm_name}-${index}`;
const isExpanded = expandedVMs[vmId] || false;
return (
<Box key={vmId}>
<Box
sx={{
display: 'flex',
alignItems: 'center',
p: 1.5,
mb: 1,
bgcolor: vm.state === 'active' ? 'rgba(0, 171, 85, 0.08)' : 'transparent',
borderRadius: 1,
'&:hover': {
bgcolor: vm.state === 'active' ? 'rgba(0, 171, 85, 0.12)' : 'rgba(145, 158, 171, 0.08)'
}
}}
>
{/* Status Indicator */}
<Box sx={{
width: 8,
height: 8,
borderRadius: '50%',
bgcolor: vm.state === 'active' ? '#00AB55' : '#919EAB',
mr: 2,
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={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100%',
py: 1,
color: theme.palette.text.secondary,
}}>
<Typography variant="body2">No VMs running</Typography>

View File

@@ -1,11 +1,37 @@
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 CheckCircleIcon from '@mui/icons-material/CheckCircle';
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';
// Register Chart.js components
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
ChartTooltip,
Legend,
TimeScale
);
// Define the structure of our data
interface ChartData {
power: string;
@@ -26,12 +52,16 @@ const Temperature = () => {
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
const [refreshing, setRefreshing] = useState(false);
const [decisionLoading, setDecisionLoading] = useState(false);
const [windowSize, setWindowSize] = useState(20);
const [alert, setAlert] = useState<{ open: boolean; message: string; severity: 'success' | 'error' }>({
open: false,
message: '',
severity: 'success'
});
// Monitoring status state
const [monitoringStatus, setMonitoringStatus] = useState<MonitoringStatus | null>(null);
// Use refs to keep track of the interval
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
const updateIntervalMs = 5000; // 5 seconds refresh rate
@@ -113,7 +143,20 @@ const Temperature = () => {
};
}, [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 = () => {
if (!data || data.length === 0) {
console.log('No data available, using fallback data');
@@ -130,7 +173,7 @@ const Temperature = () => {
return fallbackData;
}
return data.map(item => ({
const processedData = data.map(item => ({
currentTimestamp: new Date(item.now_timestamp),
futureTimestamp: new Date(item.future_timestamp),
currentPower: parseFloat(item.power),
@@ -138,20 +181,154 @@ const Temperature = () => {
currentTemp: parseFloat(item.env_temp_cur),
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();
// Extract data arrays for charts
const currentTimestamps = chartData.map(item => item.currentTimestamp);
const futureTimestamps = chartData.map(item => item.futureTimestamp);
const currentPowerData = chartData.map(item => item.currentPower);
const predictedPowerData = chartData.map(item => item.predictedPower);
const currentTempData = chartData.map(item => item.currentTemp);
const predictedTempData = chartData.map(item => item.predictedTemp);
// Prepare Chart.js data structures
const powerChartData = {
datasets: [
{
label: 'Current Power',
data: chartData.map(item => ({
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
console.log('Chart data:', chartData);
console.log('Chart.js data structures:', { powerChartData, temperatureChartData });
console.log('Raw data length:', data.length);
// Handle temperature decision
@@ -206,19 +383,35 @@ const Temperature = () => {
}}
>
<Toolbar sx={{ px: { xs: 2, sm: 4 } }}>
<Typography
variant="h5"
component="h1"
sx={{
color: 'text.primary',
fontWeight: 500,
flex: 1,
textAlign: "center",
letterSpacing: '-0.5px'
}}
>
Environmental Temperature & Power Monitoring (Last 20 Records)
</Typography>
<Box sx={{ flex: 1, textAlign: "center" }}>
<Typography
variant="h5"
component="h1"
sx={{
color: 'text.primary',
fontWeight: 500,
letterSpacing: '-0.5px',
mb: 0.5
}}
>
Environmental Temperature & Power Monitoring
</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' }}>
{lastUpdated && (
<Typography
@@ -229,7 +422,7 @@ const Temperature = () => {
Last updated: {lastUpdated.toLocaleTimeString()}
</Typography>
)}
<Tooltip title="Refresh data">
<MuiTooltip title="Refresh data">
<IconButton
onClick={handleRefresh}
color="primary"
@@ -244,59 +437,12 @@ const Temperature = () => {
>
<RefreshIcon />
</IconButton>
</Tooltip>
</MuiTooltip>
</Box>
</Toolbar>
</AppBar>
<Box sx={{ p: { xs: 2, sm: 4 } }}>
{/* Temperature Decision Panel */}
<Paper
elevation={0}
sx={{
p: 2,
mb: 3,
bgcolor: 'background.paper',
borderRadius: 2,
border: `1px solid ${theme.palette.divider}`,
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Typography variant="h6" sx={{ color: 'text.primary' }}>
Temperature Change Decision
</Typography>
<Box sx={{ display: 'flex', gap: 2 }}>
<Button
variant="contained"
color="success"
startIcon={<CheckCircleIcon />}
onClick={() => handleTemperatureDecision(true)}
disabled={decisionLoading}
sx={{
borderRadius: 2,
textTransform: 'none',
minWidth: 120,
}}
>
Approve
</Button>
<Button
variant="contained"
color="error"
startIcon={<CancelIcon />}
onClick={() => handleTemperatureDecision(false)}
disabled={decisionLoading}
sx={{
borderRadius: 2,
textTransform: 'none',
minWidth: 120,
}}
>
Decline
</Button>
</Box>
</Box>
</Paper>
<Fade in timeout={800}>
<Grid container spacing={3}>
@@ -365,44 +511,26 @@ const Temperature = () => {
</Box>
) : (
<Box sx={{ height: 400 }}>
<LineChart
height={400}
skipAnimation={false}
series={[
{
data: predictedPowerData,
label: 'Predicted Power (3min)',
color: '#ff9800',
showMark: false,
curve: 'linear'
},
{
data: currentPowerData,
label: 'Current Power',
color: '#028a4a', // B'GREEN brand color
showMark: false
},
]}
xAxis={[
{
scaleType: 'time',
data: [...currentTimestamps, ...futureTimestamps],
valueFormatter: (value: Date) => value.toLocaleTimeString(),
},
]}
yAxis={[
{
width: 50,
valueFormatter: (value: number) => `${value} W`,
<Line
data={powerChartData}
options={{
...chartOptions,
scales: {
...chartOptions.scales,
y: {
...chartOptions.scales.y,
title: {
display: true,
text: 'Power (W)'
},
ticks: {
callback: function(value: any) {
return `${value} W`;
}
}
}
}
]}
margin={{ right: 24 }}
slotProps={{
legend: {
direction: 'horizontal',
position: { vertical: 'top', horizontal: 'center' },
},
}}
}}
/>
</Box>
)}
@@ -474,43 +602,26 @@ const Temperature = () => {
</Box>
) : (
<Box sx={{ height: 400 }}>
<LineChart
height={400}
skipAnimation={false}
series={[
{
data: predictedTempData,
label: 'Predicted Temperature (3min)',
color: '#ff9800',
showMark: false
},
{
data: currentTempData,
label: 'Current Temperature',
color: '#028a4a', // B'GREEN brand color
showMark: false
},
]}
xAxis={[
{
scaleType: 'time',
data: [...currentTimestamps, ...futureTimestamps],
valueFormatter: (value: Date) => value.toLocaleTimeString(),
},
]}
yAxis={[
{
width: 50,
valueFormatter: (value: number) => `${value} °C`,
<Line
data={temperatureChartData}
options={{
...chartOptions,
scales: {
...chartOptions.scales,
y: {
...chartOptions.scales.y,
title: {
display: true,
text: 'Temperature (°C)'
},
ticks: {
callback: function(value: any) {
return `${value} °C`;
}
}
}
}
]}
margin={{ right: 24 }}
slotProps={{
legend: {
direction: 'horizontal',
position: { vertical: 'top', horizontal: 'center' },
},
}}
}}
/>
</Box>
)}
@@ -518,6 +629,136 @@ const Temperature = () => {
</Grid>
</Grid>
</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>
{/* Snackbar for alerts */}