forked from BLC/AyposWeb
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:
19
package-lock.json
generated
19
package-lock.json
generated
@@ -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"
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
15
src/App.tsx
15
src/App.tsx
@@ -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 />
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 };
|
||||||
};
|
};
|
||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 */}
|
||||||
|
|||||||
Reference in New Issue
Block a user