From 81fd9096374a3aeec18fc54f6113545989acb1a9 Mon Sep 17 00:00:00 2001 From: k3n9achi Date: Sun, 3 Aug 2025 14:51:16 +0300 Subject: [PATCH] implimented the new Vm-placement data strcuture, fixed 8/9 designs issue from last meeting written in the group blc - in 7/8/2025 --- package-lock.json | 19 +- package.json | 7 +- src/App.tsx | 15 + src/components/Layout/Sidebar.tsx | 23 +- .../Migration/MigrationAdviceCard.tsx | 12 +- .../Migration/ResourceDistributionChart.tsx | 210 ++++--- src/components/Migration/SummaryStats.tsx | 105 ++-- .../Migration/VerifiedMigration.tsx | 6 +- src/components/Migration/hooks.ts | 116 +++- src/components/Migration/types.ts | 59 ++ src/config/env.ts | 2 +- src/pages/Home.tsx | 35 +- src/pages/Maintenance.tsx | 389 +++++++++---- src/pages/Migration.tsx | 409 +++++++------ src/pages/Temperature.tsx | 535 +++++++++++++----- 15 files changed, 1295 insertions(+), 647 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1e4f250..32b2c6a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,14 +17,15 @@ "@mui/x-tree-view": "^7.26.0", "@types/plotly.js": "^2.35.2", "axios": "^1.8.4", - "chart.js": "^4.4.1", - "date-fns": "^3.0.6", + "chart.js": "^4.5.0", + "chartjs-adapter-date-fns": "^3.0.0", + "date-fns": "^3.6.0", "lucide-react": "^0.525.0", "plotly.js": "^3.0.1", "plotly.js-dist": "^3.0.1", "plotly.js-dist-min": "^3.0.1", "react": "^18.2.0", - "react-chartjs-2": "^5.2.0", + "react-chartjs-2": "^5.3.0", "react-dom": "^18.2.0", "react-google-charts": "^5.2.1", "react-plotly.js": "^2.6.0", @@ -3409,7 +3410,6 @@ "version": "4.5.0", "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz", "integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==", - "license": "MIT", "dependencies": { "@kurkle/color": "^0.3.0" }, @@ -3417,6 +3417,15 @@ "pnpm": ">=8" } }, + "node_modules/chartjs-adapter-date-fns": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chartjs-adapter-date-fns/-/chartjs-adapter-date-fns-3.0.0.tgz", + "integrity": "sha512-Rs3iEB3Q5pJ973J93OBTpnP7qoGwvq3nUnoMdtxO+9aoJof7UFcRbWcIDteXuYd1fgAvct/32T9qaLyLuZVwCg==", + "peerDependencies": { + "chart.js": ">=2.8.0", + "date-fns": ">=2.0.0" + } + }, "node_modules/clamp": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/clamp/-/clamp-1.0.1.tgz", @@ -3898,7 +3907,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", - "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/kossnocorp" @@ -6648,7 +6656,6 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.0.tgz", "integrity": "sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==", - "license": "MIT", "peerDependencies": { "chart.js": "^4.1.1", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" diff --git a/package.json b/package.json index d80c70a..a51727f 100644 --- a/package.json +++ b/package.json @@ -19,14 +19,15 @@ "@mui/x-tree-view": "^7.26.0", "@types/plotly.js": "^2.35.2", "axios": "^1.8.4", - "chart.js": "^4.4.1", - "date-fns": "^3.0.6", + "chart.js": "^4.5.0", + "chartjs-adapter-date-fns": "^3.0.0", + "date-fns": "^3.6.0", "lucide-react": "^0.525.0", "plotly.js": "^3.0.1", "plotly.js-dist": "^3.0.1", "plotly.js-dist-min": "^3.0.1", "react": "^18.2.0", - "react-chartjs-2": "^5.2.0", + "react-chartjs-2": "^5.3.0", "react-dom": "^18.2.0", "react-google-charts": "^5.2.1", "react-plotly.js": "^2.6.0", diff --git a/src/App.tsx b/src/App.tsx index 70b6b12..4d24660 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,8 +8,23 @@ import Maintenance from './pages/Maintenance'; import Migration from './pages/Migration'; import MonitoringSystem from './pages/MonitoringSystem'; import StressTesting from './pages/StressTesting'; +import { useEffect } from 'react'; function App() { + useEffect(() => { + const handleBeforeUnload = (e: BeforeUnloadEvent) => { + e.preventDefault(); + e.returnValue = 'Are you sure you want to close this page?'; + return 'Are you sure you want to close this page?'; + }; + + window.addEventListener('beforeunload', handleBeforeUnload); + + return () => { + window.removeEventListener('beforeunload', handleBeforeUnload); + }; + }, []); + return ( diff --git a/src/components/Layout/Sidebar.tsx b/src/components/Layout/Sidebar.tsx index 2a1073c..21d3f8e 100644 --- a/src/components/Layout/Sidebar.tsx +++ b/src/components/Layout/Sidebar.tsx @@ -120,7 +120,28 @@ const Sidebar = ({ open, onToggle, isMobile }: SidebarProps) => { const drawerContent = ( <> - + { + // 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'); + }} + > B'GREEN Logo = ({ setIsCardExpanded(!isCardExpanded)} sx={{ - height: isCardExpanded ? 'auto' : '320px', + height: isCardExpanded ? 'auto' : '400px', bgcolor: 'background.paper', boxShadow: 3, display: 'flex', @@ -59,7 +59,7 @@ const MigrationAdviceCard: React.FC = ({ right: isCardExpanded ? 0 : 'auto', zIndex: isCardExpanded ? 1000 : 1, width: isCardExpanded ? '100%' : 'auto', - maxHeight: isCardExpanded ? '80vh' : '320px', + maxHeight: isCardExpanded ? '80vh' : '400px', overflowY: isCardExpanded ? 'auto' : 'hidden' }} > @@ -125,7 +125,7 @@ const MigrationAdviceCard: React.FC = ({ Current Power - {gainBeforeData.cur_power.toFixed(2)} W + {gainBeforeData.cur_power.toFixed(2)}W @@ -133,7 +133,7 @@ const MigrationAdviceCard: React.FC = ({ Proposed Power - {gainBeforeData.prop_power.toFixed(2)} W + {gainBeforeData.prop_power.toFixed(2)}W @@ -144,10 +144,10 @@ const MigrationAdviceCard: React.FC = ({ variant="h6" sx={{ fontWeight: 'bold', - color: gainBeforeData.prop_gain > 0 ? '#4caf50' : '#f44336' + color: gainBeforeData.prop_gain > 0 ? '#28c76f' : '#FF1744' }} > - {(gainBeforeData.prop_gain * 100).toFixed(2)}% + {Math.abs(gainBeforeData.prop_gain * 100).toFixed(2)}% diff --git a/src/components/Migration/ResourceDistributionChart.tsx b/src/components/Migration/ResourceDistributionChart.tsx index 5523f2d..c62b4c3 100644 --- a/src/components/Migration/ResourceDistributionChart.tsx +++ b/src/components/Migration/ResourceDistributionChart.tsx @@ -1,52 +1,32 @@ import React from 'react'; -import { Paper, Typography, IconButton, Box, Grid } from '@mui/material'; +import { Grid, Paper, Typography, Box, IconButton, CircularProgress, useTheme } from '@mui/material'; +import RefreshIcon from '@mui/icons-material/Refresh'; +import { Chart as ChartJSComponent } from 'react-chartjs-2'; import { Chart as ChartJS, CategoryScale, LinearScale, - BarElement, - BarController, PointElement, LineElement, - LineController, + BarElement, Title, Tooltip, Legend, + ChartOptions, } from 'chart.js'; -import { Chart } from 'react-chartjs-2'; -import RefreshIcon from '@mui/icons-material/Refresh'; +import { VMPlacementData } from './types'; ChartJS.register( CategoryScale, LinearScale, - BarElement, - BarController, PointElement, LineElement, - LineController, + BarElement, Title, Tooltip, Legend ); -interface VM { - name: string; - power: number; -} - -interface VMPlacementData { - data_center: string; - id: number; - physical_machines: Array<{ - name: string; - power_consumption: number; - vms: { - active: VM[]; - inactive: VM[]; - }; - }>; -} - interface ResourceDistributionChartProps { vmPlacementData: VMPlacementData | null; isLoading: boolean; @@ -58,22 +38,34 @@ const ResourceDistributionChart: React.FC = ({ isLoading, onRefresh }) => { + const theme = useTheme(); + const chartData = React.useMemo(() => { - if (!vmPlacementData?.physical_machines) { + if (!vmPlacementData?.vm_placement) { return { labels: [], datasets: [] }; } - const physicalMachines = vmPlacementData.physical_machines; - const labels = physicalMachines.map(pm => pm.name); - const activeVMs = physicalMachines.map(pm => pm.vms.active.length); - const inactiveVMs = physicalMachines.map(pm => pm.vms.inactive.length); - const totalPower = physicalMachines.map(pm => pm.power_consumption); - const vmPower = physicalMachines.map(pm => - pm.vms.active.reduce((sum, vm) => sum + vm.power, 0) - ); + // Process data from vm_placement object + const pmData: Record = {}; + + Object.values(vmPlacementData.vm_placement).forEach(pm => { + const vms = Object.values(pm.vms); + pmData[pm.name] = { + power: pm.power, + activeVMs: vms.filter(vm => vm.state === 'active').length, + inactiveVMs: vms.filter(vm => vm.state === 'inactive').length, + vmPower: vms.filter(vm => vm.state === 'active').reduce((sum, vm) => sum + vm.power, 0) + }; + }); + + const labels = Object.keys(pmData); + const activeVMs = labels.map(pmName => pmData[pmName].activeVMs); + const inactiveVMs = labels.map(pmName => pmData[pmName].inactiveVMs); + const totalPower = labels.map(pmName => pmData[pmName].power); + const vmPower = labels.map(pmName => pmData[pmName].vmPower); return { labels, @@ -82,23 +74,25 @@ const ResourceDistributionChart: React.FC = ({ type: 'bar' as const, label: 'Active VMs', data: activeVMs, - backgroundColor: '#4caf50', + backgroundColor: theme.palette.success.main, yAxisID: 'vmAxis', stack: 'vms', + borderRadius: 4, }, { type: 'bar' as const, label: 'Inactive VMs', data: inactiveVMs, - backgroundColor: '#ff9800', + backgroundColor: theme.palette.grey[300], yAxisID: 'vmAxis', stack: 'vms', + borderRadius: 4, }, { type: 'line' as const, label: 'Total Power (W)', data: totalPower, - borderColor: '#f44336', + borderColor: theme.palette.primary.main, backgroundColor: 'transparent', yAxisID: 'powerAxis', tension: 0.4, @@ -108,7 +102,7 @@ const ResourceDistributionChart: React.FC = ({ type: 'line' as const, label: 'VM Power (W)', data: vmPower, - borderColor: '#2196f3', + borderColor: theme.palette.info.main, backgroundColor: 'transparent', yAxisID: 'powerAxis', borderDash: [5, 5], @@ -117,9 +111,9 @@ const ResourceDistributionChart: React.FC = ({ } ] }; - }, [vmPlacementData]); + }, [vmPlacementData, theme.palette]); - const options = { + const options: ChartOptions<'bar' | 'line'> = React.useMemo(() => ({ responsive: true, maintainAspectRatio: false, interaction: { @@ -131,19 +125,32 @@ const ResourceDistributionChart: React.FC = ({ position: 'top' as const, labels: { usePointStyle: true, + color: theme.palette.text.secondary, + font: { + family: theme.typography.fontFamily, + size: 12 + }, + padding: 20, }, }, tooltip: { - callbacks: { - label: (context: any) => { - const label = context.dataset.label || ''; - const value = context.parsed.y; - if (label.includes('Power')) { - return `${label}: ${value.toFixed(2)} W`; - } - return `${label}: ${value}`; - }, + enabled: true, + backgroundColor: 'rgba(255, 255, 255, 0.95)', + titleColor: theme.palette.text.primary, + titleFont: { + family: theme.typography.fontFamily, + size: 13, + weight: '600' }, + bodyColor: theme.palette.text.secondary, + bodyFont: { + family: theme.typography.fontFamily, + size: 12 + }, + borderColor: theme.palette.divider, + borderWidth: 1, + padding: 12, + boxPadding: 4, }, }, scales: { @@ -151,6 +158,13 @@ const ResourceDistributionChart: React.FC = ({ grid: { display: false, }, + ticks: { + color: theme.palette.text.secondary, + font: { + family: theme.typography.fontFamily, + size: 11 + } + } }, vmAxis: { type: 'linear' as const, @@ -158,10 +172,25 @@ const ResourceDistributionChart: React.FC = ({ title: { display: true, text: 'Number of VMs', + color: theme.palette.text.secondary, + font: { + family: theme.typography.fontFamily, + size: 12, + weight: '500' + } }, beginAtZero: true, ticks: { stepSize: 1, + color: theme.palette.text.secondary, + font: { + family: theme.typography.fontFamily, + size: 11 + } + }, + grid: { + color: theme.palette.divider, + drawBorder: false, }, stacked: true, }, @@ -171,59 +200,76 @@ const ResourceDistributionChart: React.FC = ({ title: { display: true, text: 'Power (W)', + color: theme.palette.text.secondary, + font: { + family: theme.typography.fontFamily, + size: 12, + weight: '500' + } }, beginAtZero: true, + ticks: { + color: theme.palette.text.secondary, + font: { + family: theme.typography.fontFamily, + size: 11 + } + }, grid: { drawOnChartArea: false, + color: theme.palette.divider, + drawBorder: false, }, }, }, - }; + }), [theme]); return ( - - Resource Distribution by Node + + + Resource Distribution + - + {isLoading ? : } - - + + + {chartData.labels.length > 0 ? ( + + ) : ( + + + {isLoading ? 'Loading data...' : 'No data available'} + + + )} - {isLoading && ( - - Loading... - - )} ); diff --git a/src/components/Migration/SummaryStats.tsx b/src/components/Migration/SummaryStats.tsx index d4bf3ab..af98552 100644 --- a/src/components/Migration/SummaryStats.tsx +++ b/src/components/Migration/SummaryStats.tsx @@ -1,54 +1,33 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { Grid, Paper, Typography, Box, Divider } from '@mui/material'; import StorageIcon from '@mui/icons-material/Storage'; import ComputerIcon from '@mui/icons-material/Computer'; import { config } from '../../config/env'; - -interface VM { - name: string; - power: number; -} - -interface VMPlacementData { - data_center: string; - id: number; - physical_machines: Array<{ - name: string; - power_consumption: number; - vms: { - active: VM[]; - inactive: VM[]; - }; - }>; -} +import { VMPlacementData } from './types'; +import { useSmartPolling } from './hooks'; const ENDPOINT = `${config.apiUrl}/prom/get_chart_data/vm_placement`; -const REFRESH_INTERVAL = 30000; // 30 seconds const SummaryStats: React.FC = () => { - const [data, setData] = useState(null); - - const fetchData = async () => { - try { - const response = await fetch(ENDPOINT); - if (!response.ok) { - throw new Error(`Failed to fetch data: ${response.status}`); - } - const jsonData = await response.json(); - setData(jsonData); - } catch (err) { - console.error('Error fetching VM placement data:', err); + const fetchData = useCallback(async (): Promise => { + const response = await fetch(ENDPOINT); + if (!response.ok) { + throw new Error(`Failed to fetch data: ${response.status}`); } - }; - - useEffect(() => { - fetchData(); - const interval = setInterval(fetchData, REFRESH_INTERVAL); - return () => clearInterval(interval); + const jsonData = await response.json(); + console.log('SummaryStats - Raw API response:', jsonData); + return jsonData; }, []); + const { data, pollingInterval } = useSmartPolling( + fetchData, + null, + 5000, // min interval: 5 seconds + 30000 // max interval: 30 seconds + ); + const stats = React.useMemo(() => { - if (!data?.physical_machines) { + if (!data) { return { activeComputes: 0, totalComputes: 0, @@ -57,30 +36,28 @@ const SummaryStats: React.FC = () => { }; } - const activeComputes = data.physical_machines.filter( - pm => pm.power_consumption > 0 || pm.vms.active.length > 0 - ).length; + // Count from vm_placement object + let totalPMs = Object.keys(data.vm_placement).length; + let activePMs = Object.values(data.vm_placement).filter(pm => pm.power > 0).length; + let totalActiveVMs = 0; + let totalInactiveVMs = 0; - const totalActiveVMs = data.physical_machines.reduce( - (sum, pm) => sum + pm.vms.active.length, - 0 - ); - - const totalInactiveVMs = data.physical_machines.reduce( - (sum, pm) => sum + pm.vms.inactive.length, - 0 - ); + Object.values(data.vm_placement).forEach(pm => { + const vms = Object.values(pm.vms); + totalActiveVMs += vms.filter(vm => vm.state === 'active').length; + totalInactiveVMs += vms.filter(vm => vm.state === 'inactive').length; + }); return { - activeComputes, - totalComputes: data.physical_machines.length, + activeComputes: activePMs, + totalComputes: totalPMs, activeVMs: totalActiveVMs, inactiveVMs: totalInactiveVMs, }; }, [data]); return ( - + { Virtual Machines - - - {stats.activeVMs} - - - active - - - / - - - {stats.activeVMs + stats.inactiveVMs} - - - total - - + + {stats.activeVMs}/{stats.activeVMs + stats.inactiveVMs} + diff --git a/src/components/Migration/VerifiedMigration.tsx b/src/components/Migration/VerifiedMigration.tsx index 5d59bf3..8ef824f 100644 --- a/src/components/Migration/VerifiedMigration.tsx +++ b/src/components/Migration/VerifiedMigration.tsx @@ -70,7 +70,7 @@ const VerifiedMigration: React.FC = ({ Current Power - {gainAfterData.cur_power.toFixed(2)} W + {gainAfterData.cur_power.toFixed(2)}W @@ -91,7 +91,7 @@ const VerifiedMigration: React.FC = ({ {(gainAfterData.val_ratio * 100).toFixed(2)}% @@ -114,7 +114,7 @@ const VerifiedMigration: React.FC = ({ 0 ? '#4caf50' : '#ff9800' + color: gainAfterData.actual_ratio > 0 ? '#28c76f' : '#FF1744' }}> {(gainAfterData.actual_ratio * 100).toFixed(2)}% diff --git a/src/components/Migration/hooks.ts b/src/components/Migration/hooks.ts index 02346c3..291a2f2 100644 --- a/src/components/Migration/hooks.ts +++ b/src/components/Migration/hooks.ts @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback, useRef } from 'react'; import { VMDetails, GainBeforeData, MigrationAdviceData } from './types'; import { config } from '../../config/env'; @@ -15,6 +15,76 @@ interface GainAfterData { val_difference: number; } +// Smart polling hook +export const useSmartPolling = ( + fetchFunction: () => Promise, + initialState: T | null, + minInterval: number = 5000, + maxInterval: number = 30000 +) => { + const [data, setData] = useState(initialState); + const [pollingInterval, setPollingInterval] = useState(minInterval); + const [isLoading, setIsLoading] = useState(false); + const [isInitialLoad, setIsInitialLoad] = useState(true); + const lastDataRef = useRef(''); + const intervalRef = useRef(null); + + const poll = useCallback(async () => { + try { + if (isInitialLoad) { + setIsLoading(true); + } + const newData = await fetchFunction(); + const newDataString = JSON.stringify(newData); + + // Only update if data actually changed + if (lastDataRef.current !== newDataString) { + console.log('Data changed, resetting polling interval to', minInterval, 'ms'); + setData(newData); + setPollingInterval(minInterval); + lastDataRef.current = newDataString; + } else { + // Gradually increase interval when data is stable + setPollingInterval(prevInterval => { + const newInterval = Math.min(prevInterval * 1.5, maxInterval); + if (newInterval !== prevInterval) { + console.log('Data stable, increasing polling interval to', newInterval, 'ms'); + } + return newInterval; + }); + } + } catch (error) { + console.error('Error in smart polling:', error); + // On error, keep current interval + } finally { + setIsLoading(false); + setIsInitialLoad(false); + } + }, [fetchFunction, minInterval, maxInterval, isInitialLoad]); + + useEffect(() => { + // Initial fetch + poll(); + + // Clear any existing interval + if (intervalRef.current) { + clearInterval(intervalRef.current); + } + + // Set new interval + intervalRef.current = setInterval(poll, pollingInterval); + + return () => { + if (intervalRef.current) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } + }; + }, [poll, pollingInterval]); + + return { data, isLoading, pollingInterval }; +}; + export const useMigrationData = () => { const [gainBeforeData, setGainBeforeData] = useState(null); const [migrationAdviceData, setMigrationAdviceData] = useState(null); @@ -125,7 +195,7 @@ export const useMonitoringData = () => { }; export const useVMDetails = () => { - const [vmDetails, setVmDetails] = useState>({}); + const [vmDetails, setVmDetails] = useState(null); const [expandedVMs, setExpandedVMs] = useState>({}); const toggleVMDetails = (vmId: string) => { @@ -135,28 +205,21 @@ export const useVMDetails = () => { })); }; - useEffect(() => { - const fetchVMDetails = async () => { - try { - const response = await fetch(`${API_BASE_URL}/prom/vm_mac_details`); - if (!response.ok) { - throw new Error('Failed to fetch VM details'); - } - const data = await response.json(); - if (data?.res) { - setVmDetails(data.res); - } - } catch (error) { - console.error('Error fetching VM details:', error); + const fetchVMDetails = async (vmName: string) => { + try { + const response = await fetch(`${API_BASE_URL}/prom/vm_details/${vmName}`); + if (!response.ok) { + throw new Error('Failed to fetch VM details'); } - }; + const data = await response.json(); + setVmDetails(data); + } catch (error) { + console.error('Error fetching VM details:', error); + setVmDetails(null); + } + }; - fetchVMDetails(); - const interval = setInterval(fetchVMDetails, REFRESH_INTERVAL); - return () => clearInterval(interval); - }, []); - - return { vmDetails, expandedVMs, toggleVMDetails }; + return { vmDetails, expandedVMs, toggleVMDetails, fetchVMDetails }; }; export const useGainAfterData = () => { @@ -167,19 +230,18 @@ export const useGainAfterData = () => { try { setIsLoading(true); const response = await fetch(`${API_BASE_URL}/prom/get_chart_data/gain_after`); - if (!response.ok) { - throw new Error('Failed to fetch gain-after data'); + throw new Error('Failed to fetch gain after data'); } - const data = await response.json(); setGainAfterData(data); } catch (error) { - console.error('Error fetching gain-after data:', error); + console.error('Error fetching gain after data:', error); + setGainAfterData(null); } finally { setIsLoading(false); } }; - return { gainAfterData, isLoading, fetchGainAfterData }; + return { gainAfterData, isLoading: isLoading, fetchGainAfterData }; }; \ No newline at end of file diff --git a/src/components/Migration/types.ts b/src/components/Migration/types.ts index 02f1d16..86a95e2 100644 --- a/src/components/Migration/types.ts +++ b/src/components/Migration/types.ts @@ -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; +} + +// Updated VMPlacementData interface to match the actual API response +export interface VMPlacementData { + data_center: string; + id: number; + physical_machines: PhysicalMachine[]; + vm_placement: Record; +} + export interface ChartData { labels: string[]; datasets: { diff --git a/src/config/env.ts b/src/config/env.ts index b5b6693..64df7b8 100644 --- a/src/config/env.ts +++ b/src/config/env.ts @@ -5,7 +5,7 @@ const getApiUrl = (): string => { return '/api'; } // In development, use the direct URL - return import.meta.env.VITE_API_URL || 'http://141.196.166.241:8003'; + return import.meta.env.VITE_API_URL || 'http://aypos-api.blc-css.com'; }; export const config = { diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 979bf3c..f4128cf 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -232,7 +232,8 @@ const Home = () => { const [migrationTime, setMigrationTime] = useState('5'); const [migrationModel, setMigrationModel] = useState('mul_reg'); const [migrationMethod, setMigrationMethod] = useState('mathematical'); - const [migrationMode, setMigrationMode] = useState<'auto' | 'semiauto'>('auto'); + // Default to semiauto since auto mode is not available yet + const [migrationMode, setMigrationMode] = useState<'auto' | 'semiauto'>('semiauto'); const [isMonitoring, setIsMonitoring] = useState(false); @@ -517,10 +518,10 @@ const Home = () => { Time Configuration - Script Time Unit + {"{script time unit (mins)}"} @@ -535,10 +536,10 @@ const Home = () => { Steps Configuration - Number of Steps + Estimation Steps @@ -581,10 +582,10 @@ const Home = () => { Time Configuration - Script Time Unit + {"{script time unit (mins)}"} @@ -599,10 +600,10 @@ const Home = () => { Steps Configuration - Number of Steps + Estimation Steps @@ -665,11 +666,19 @@ const Home = () => { } }} > - + Auto - + Semi-Auto @@ -681,10 +690,10 @@ const Home = () => { Time Configuration - Script Time Unit + {"{script time unit (mins)}"} diff --git a/src/pages/Maintenance.tsx b/src/pages/Maintenance.tsx index 28f5c76..0b6e079 100644 --- a/src/pages/Maintenance.tsx +++ b/src/pages/Maintenance.tsx @@ -1,8 +1,32 @@ import { useState, useEffect } from 'react'; -import { Box, Paper, Typography, Fade, useTheme, AppBar, Toolbar, Chip } from '@mui/material'; -import { LineChart } from '@mui/x-charts/LineChart'; +import { Box, Paper, Typography, Fade, useTheme, AppBar, Toolbar, Chip, Slider, FormControlLabel, Switch } from '@mui/material'; +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + PointElement, + LineElement, + Title, + Tooltip, + Legend, + TimeScale +} from 'chart.js'; +import 'chartjs-adapter-date-fns'; +import { Line } from 'react-chartjs-2'; import { config } from '../config/env'; +// Register Chart.js components +ChartJS.register( + CategoryScale, + LinearScale, + PointElement, + LineElement, + Title, + Tooltip, + Legend, + TimeScale +); + interface DataItem { now_timestamp: string; future_timestamp: string; @@ -23,6 +47,7 @@ const Maintenance = () => { const [data, setData] = useState([]); const [currentFlag, setCurrentFlag] = useState(''); const [, setLoading] = useState(true); + const [windowSize, setWindowSize] = useState(20); useEffect(() => { const fetchData = async () => { @@ -49,7 +74,7 @@ const Maintenance = () => { return () => clearInterval(interval); }, []); - // Process data for charts + // Process data for charts with sliding window const prepareChartData = () => { if (!data || data.length === 0) { console.log('No data available, using fallback data'); @@ -84,52 +109,184 @@ const Maintenance = () => { }; }); - console.log('Processed chart data:', processedData); + // Apply sliding window - show only last N records + const slidingData = processedData.slice(-windowSize); + + console.log('Processed chart data:', { + totalRecords: processedData.length, + showingRecords: slidingData.length, + timeRange: { + start: slidingData[0]?.currentTimestamp, + end: slidingData[slidingData.length - 1]?.currentTimestamp + } + }); + console.log('Data validation:', { - hasCurrentPower: processedData.some(d => d.currentPower > 0), - hasPredictedPower: processedData.some(d => d.predictedPower > 0), - currentPowerRange: [Math.min(...processedData.map(d => d.currentPower)), Math.max(...processedData.map(d => d.currentPower))], - predictedPowerRange: [Math.min(...processedData.map(d => d.predictedPower)), Math.max(...processedData.map(d => d.predictedPower))], - rawDataSample: data.slice(0, 2).map(item => ({ + hasCurrentPower: slidingData.some(d => d.currentPower > 0), + hasPredictedPower: slidingData.some(d => d.predictedPower > 0), + currentPowerRange: [Math.min(...slidingData.map(d => d.currentPower)), Math.max(...slidingData.map(d => d.currentPower))], + predictedPowerRange: [Math.min(...slidingData.map(d => d.predictedPower)), Math.max(...slidingData.map(d => d.predictedPower))], + rawDataSample: data.slice(-2).map(item => ({ power: item.power, power_future_min: item.power_future_min, parsedCurrent: parseFloat(item.power), parsedPredicted: parseFloat(item.power_future_min) })) }); - return processedData; + + return slidingData; }; const chartData = prepareChartData(); - // Extract data arrays for charts - const currentTimestamps = chartData.map(item => item.currentTimestamp); - const futureTimestamps = chartData.map(item => item.futureTimestamp); - - const currentPowerData = chartData.map(item => item.currentPower); - const predictedPowerData = chartData.map(item => item.predictedPower); - const positive3pData = chartData.map(item => item.positive3p); - const negative3pData = chartData.map(item => item.negative3p); - const positive7pData = chartData.map(item => item.positive7p); - const negative7pData = chartData.map(item => item.negative7p); + // Prepare Chart.js data structure + const chartJsData = { + datasets: [ + { + label: 'Current Power', + data: chartData.map(item => ({ + x: item.currentTimestamp, + y: item.currentPower + })), + borderColor: '#028a4a', + backgroundColor: '#028a4a', + pointBackgroundColor: '#028a4a', + pointBorderColor: '#028a4a', + pointRadius: 0, + pointHoverRadius: 6, + tension: 0.4, + fill: false + }, + { + label: 'Predicted (Dynamic)', + data: chartData.map(item => ({ + x: item.futureTimestamp, + y: item.predictedPower + })), + borderColor: '#ff9800', + backgroundColor: '#ff9800', + pointBackgroundColor: '#ff9800', + pointBorderColor: '#ff9800', + pointRadius: 0, + pointHoverRadius: 6, + tension: 0.4, + fill: false + }, + { + label: '+3% Positive', + data: chartData.map(item => ({ + x: item.futureTimestamp, + y: item.positive3p + })), + borderColor: '#ffb400', + backgroundColor: 'rgba(255, 180, 0, 0.1)', + pointRadius: 0, + tension: 0.4, + fill: false + }, + { + label: '-3% Negative', + data: chartData.map(item => ({ + x: item.futureTimestamp, + y: item.negative3p + })), + borderColor: '#ffb400', + backgroundColor: 'rgba(255, 180, 0, 0.1)', + pointRadius: 0, + tension: 0.4, + fill: false + }, + { + label: '+7% Positive', + data: chartData.map(item => ({ + x: item.futureTimestamp, + y: item.positive7p + })), + borderColor: '#FF1744', + backgroundColor: 'rgba(255, 23, 68, 0.1)', + pointRadius: 0, + tension: 0.4, + fill: false + }, + { + label: '-7% Negative', + data: chartData.map(item => ({ + x: item.futureTimestamp, + y: item.negative7p + })), + borderColor: '#FF1744', + backgroundColor: 'rgba(255, 23, 68, 0.1)', + pointRadius: 0, + tension: 0.4, + fill: false + } + ] + }; + + // Chart.js options + const chartOptions = { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: 'top' as const, + labels: { + usePointStyle: true, + padding: 20, + font: { + size: 12, + weight: 'normal' as const + } + } + }, + tooltip: { + mode: 'index' as const, + intersect: false, + callbacks: { + title: function(context: any) { + const date = new Date(context[0].parsed.x); + return date.toLocaleTimeString(); + }, + label: function(context: any) { + return `${context.dataset.label}: ${context.parsed.y.toFixed(2)} W`; + } + } + } + }, + scales: { + x: { + type: 'time' as const, + time: { + displayFormats: { + minute: 'HH:mm:ss' + } + }, + title: { + display: true, + text: 'Time' + } + }, + y: { + title: { + display: true, + text: 'Power (W)' + }, + ticks: { + callback: function(value: any) { + return `${value} W`; + } + } + } + }, + interaction: { + mode: 'nearest' as const, + axis: 'x' as const, + intersect: false + } + }; // Debug logging - console.log('Chart arrays:', { - currentTimestamps: currentTimestamps.length, - futureTimestamps: futureTimestamps.length, - currentPower: currentPowerData.length, - predictedPower: predictedPowerData.length, - positive3p: positive3pData.length, - negative3p: negative3pData.length, - positive7p: positive7pData.length, - negative7p: negative7pData.length, - }); - console.log('Sample timestamps:', { - current: currentTimestamps.slice(0, 3), - future: futureTimestamps.slice(0, 3), - currentPower: currentPowerData.slice(0, 3), - predictedPower: predictedPowerData.slice(0, 3), - }); + console.log('Chart.js data structure:', chartJsData); return ( @@ -156,33 +313,50 @@ const Maintenance = () => { > Preventive Maintenance - {currentFlag && ( + + {currentFlag && ( + + )} - )} + @@ -197,71 +371,42 @@ const Maintenance = () => { border: `1px solid ${theme.palette.divider}`, }} > - - value.toLocaleTimeString(), - }, - ]} - yAxis={[ - { - width: 50, - valueFormatter: (value: number) => `${value} W`, - } - ]} - margin={{ right: 24 }} - slotProps={{ - legend: { - direction: 'horizontal', - position: { vertical: 'top', horizontal: 'center' }, - }, - }} - /> + + + + + {/* Chart Controls */} + + + Chart Settings + + + + Records to show: + + 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 }} + /> + + {windowSize} + + diff --git a/src/pages/Migration.tsx b/src/pages/Migration.tsx index f26eba1..9fa653b 100644 --- a/src/pages/Migration.tsx +++ b/src/pages/Migration.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import { Box, Paper, @@ -16,6 +16,9 @@ import { DialogContent, DialogActions, LinearProgress, + List, + ListItemButton, + ListItemText, } from '@mui/material'; import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew'; @@ -28,44 +31,15 @@ import SummaryStats from '../components/Migration/SummaryStats'; import ResourceDistributionChart from '../components/Migration/ResourceDistributionChart'; import MigrationAdviceCard from '../components/Migration/MigrationAdviceCard'; import VerifiedMigration from '../components/Migration/VerifiedMigration'; -import { useMigrationData, useGainAfterData } from '../components/Migration/hooks'; +import { useMigrationData, useGainAfterData, useSmartPolling } from '../components/Migration/hooks'; import { config } from '../config/env'; +import { VMPlacementData } from '../components/Migration/types'; // Constants const API_BASE_URL = config.apiUrl; const REFRESH_INTERVAL = 30000; // 30 seconds -interface VMPlacementData { - data_center: string; - id: number; - physical_machines: Array<{ - status: 'blocked' | 'open'; - name: string; - power_consumption: number; - vms: { - active: Array<{ - status: 'blocked' | 'open'; - name: string; - power: number; - confg: { - cpu: number; - ram: number; - disk: number; - }; - }>; - inactive: Array<{ - status: 'blocked' | 'open'; - name: string; - power: number; - confg: { - cpu: number; - ram: number; - disk: number; - }; - }>; - }; - }>; -} + interface VMCardProps { vm: { @@ -367,8 +341,6 @@ const Migration = () => { const theme = useTheme(); // Essential states - const [vmPlacementData, setVmPlacementData] = useState(null); - const [isLoadingVmPlacement, setIsLoadingVmPlacement] = useState(false); const [expandedVMs, setExpandedVMs] = useState>({}); const [showVerifiedSection, setShowVerifiedSection] = useState(false); const [isCardExpanded, setIsCardExpanded] = useState(false); @@ -378,36 +350,37 @@ const Migration = () => { const [showProgress, setShowProgress] = useState(false); const [hasProgress, setHasProgress] = useState(false); + // Smart polling for VM placement data - memoized to prevent re-renders + const fetchVmPlacementData = useCallback(async (): Promise => { + 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( + fetchVmPlacementData, + null, + 5000, // min interval: 5 seconds + 30000 // max interval: 30 seconds + ); + // Hooks for migration functionality const { gainBeforeData, migrationAdviceData, isLoadingGainData, fetchMigrationData } = useMigrationData(); const { gainAfterData, isLoading: isLoadingGainAfter, fetchGainAfterData } = useGainAfterData(); // Essential functions - const toggleVMDetails = (vmId: string) => { + const toggleVMDetails = useCallback((vmId: string) => { setExpandedVMs(prev => ({ ...prev, [vmId]: !prev[vmId] })); - }; + }, []); - const fetchVmPlacementData = async () => { - try { - setIsLoadingVmPlacement(true); - const response = await fetch(`${API_BASE_URL}/prom/get_chart_data/vm_placement`); - if (!response.ok) { - throw new Error(`Failed to fetch VM placement data: ${response.status}`); - } - const data = await response.json(); - console.log('Raw API response:', data); // Debug log - setVmPlacementData(data); // Use the data directly since it already has the correct structure - } catch (error) { - console.error('Error fetching VM placement data:', error); - } finally { - setIsLoadingVmPlacement(false); - } - }; - - const handleApproveMigration = async () => { + const handleApproveMigration = useCallback(async () => { try { setIsProcessing(true); setMigrationProgress([]); @@ -449,9 +422,9 @@ const Migration = () => { } finally { setIsProcessing(false); } - }; + }, [fetchGainAfterData]); - const handleDeclineMigration = async () => { + const handleDeclineMigration = useCallback(async () => { try { setIsProcessing(true); @@ -474,40 +447,37 @@ const Migration = () => { } finally { setIsProcessing(false); } - }; - - // Data fetching effect - useEffect(() => { - console.log('Initial data fetch'); - fetchVmPlacementData(); - const intervalId = setInterval(() => { - console.log('Interval data fetch'); - fetchVmPlacementData(); - }, REFRESH_INTERVAL); - return () => clearInterval(intervalId); }, []); // Add effect to monitor vmPlacementData changes useEffect(() => { if (vmPlacementData) { - const blockedPMs = vmPlacementData.physical_machines.filter(pm => pm.status === 'blocked').length; - const blockedVMs = vmPlacementData.physical_machines.reduce((acc, pm) => { - const activeBlocked = pm.vms.active.filter(vm => vm.status === 'blocked').length; - const inactiveBlocked = pm.vms.inactive.filter(vm => vm.status === 'blocked').length; - return acc + activeBlocked + inactiveBlocked; - }, 0); + let totalPMs = 0; + let totalVMs = 0; + let activeVMs = 0; + let inactiveVMs = 0; + + // Count from vm_placement object + totalPMs = Object.keys(vmPlacementData.vm_placement).length; + + Object.values(vmPlacementData.vm_placement).forEach(pm => { + const vms = Object.values(pm.vms); + totalVMs += vms.length; + activeVMs += vms.filter(vm => vm.state === 'active').length; + inactiveVMs += vms.filter(vm => vm.state === 'inactive').length; + }); console.log('VM Placement Data updated:', { timestamp: new Date().toISOString(), - pmCount: vmPlacementData.physical_machines.length, - blockedPMs, - totalVMs: vmPlacementData.physical_machines.reduce((acc, pm) => - acc + pm.vms.active.length + pm.vms.inactive.length, 0 - ), - blockedVMs + pmCount: totalPMs, + totalVMs, + activeVMs, + inactiveVMs, + dataCenter: vmPlacementData.data_center, + pollingInterval: `${pollingInterval}ms` }); } - }, [vmPlacementData]); + }, [vmPlacementData, pollingInterval]); return ( @@ -568,7 +538,7 @@ const Migration = () => { variant="contained" color="error" onClick={handleDeclineMigration} - disabled={isProcessing} + disabled={isProcessing || !migrationAdviceData || Object.keys(migrationAdviceData).length === 0} sx={{ py: 1.5, px: 4, @@ -588,7 +558,7 @@ const Migration = () => { variant="contained" startIcon={!isProcessing && } onClick={handleApproveMigration} - disabled={isProcessing} + disabled={isProcessing || !migrationAdviceData || Object.keys(migrationAdviceData).length === 0} sx={{ py: 1.5, px: 4, @@ -619,127 +589,236 @@ const Migration = () => { )} - {/* PM & VM Monitoring Section */} + {/* PMs & VMs Monitoring Section */} PMs & VMs Monitoring + + Polling: {pollingInterval / 1000}s + {isLoadingVmPlacement ? ( - ) : vmPlacementData?.physical_machines ? ( + ) : vmPlacementData?.vm_placement ? ( - {vmPlacementData.physical_machines.map((pm) => ( - + {Object.values(vmPlacementData.vm_placement).map((pm, index) => ( + - + + {/* PM Header */} - {pm.name} - {pm.status === 'blocked' && ( - - )} - - {pm.power_consumption.toFixed(2)}W + {pm.power.toFixed(2)}W + {/* PM Info */} + + + Aggregate: {pm.tag ?? '-'} + + + Config: CPU {pm.confg[1]}, RAM {pm.confg[2]}, Disk {pm.confg[3]} + + + + {/* VMs Container */} - {/* Active VMs */} - {pm.vms.active.map((vm, index) => ( - - ))} - - {/* Inactive VMs */} - {pm.vms.inactive.map((vm, index) => ( - - ))} - - {pm.vms.active.length === 0 && pm.vms.inactive.length === 0 && ( + {Object.values(pm.vms).map((vm, index) => { + const vmId = `${pm.name}-${vm.vm_name}-${index}`; + const isExpanded = expandedVMs[vmId] || false; + + return ( + + + {/* Status Indicator */} + + + {/* VM Name */} + + {vm.vm_name} + + + {/* Power Consumption */} + + {vm.power.toFixed(2)}W + + + {/* Info Icon */} + toggleVMDetails(vmId)} + sx={{ + color: isExpanded ? theme.palette.primary.main : theme.palette.text.secondary, + p: 0.5, + '&:hover': { + color: theme.palette.primary.main + } + }} + > + + + + + {/* Expandable Details Section */} + + + + {/* Power Consumption */} + + + + + {/* Configuration */} + + + + CPU: {vm.confg[1]} + + + RAM: {vm.confg[2]} + + + Disk: {vm.confg[3]} + + + } + primaryTypographyProps={{ + sx: theme.typography.body2 + }} + /> + + + {/* Project ID */} + + + + + {/* Aggregate */} + + + + + + + + ); + })} + {Object.values(pm.vms).length === 0 && ( No VMs running diff --git a/src/pages/Temperature.tsx b/src/pages/Temperature.tsx index a800b44..f90b614 100644 --- a/src/pages/Temperature.tsx +++ b/src/pages/Temperature.tsx @@ -1,11 +1,37 @@ import { useEffect, useState, useCallback, useRef } from 'react'; -import { Box, Paper, Typography, Fade, useTheme, Grid, AppBar, Toolbar, CircularProgress, IconButton, Tooltip, Chip, Button, Snackbar, Alert } from '@mui/material'; +import { Box, Paper, Typography, Fade, useTheme, Grid, AppBar, Toolbar, CircularProgress, IconButton, Tooltip as MuiTooltip, Chip, Button, Snackbar, Alert, Slider } from '@mui/material'; import RefreshIcon from '@mui/icons-material/Refresh'; import CheckCircleIcon from '@mui/icons-material/CheckCircle'; import CancelIcon from '@mui/icons-material/Cancel'; -import { LineChart } from '@mui/x-charts/LineChart'; +import { monitoringService } from '../services/monitoringService'; +import { MonitoringStatus } from '../types/monitoring'; +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + PointElement, + LineElement, + Title, + Tooltip as ChartTooltip, + Legend, + TimeScale +} from 'chart.js'; +import 'chartjs-adapter-date-fns'; +import { Line } from 'react-chartjs-2'; import { config } from '../config/env'; +// Register Chart.js components +ChartJS.register( + CategoryScale, + LinearScale, + PointElement, + LineElement, + Title, + ChartTooltip, + Legend, + TimeScale +); + // Define the structure of our data interface ChartData { power: string; @@ -26,12 +52,16 @@ const Temperature = () => { const [lastUpdated, setLastUpdated] = useState(null); const [refreshing, setRefreshing] = useState(false); const [decisionLoading, setDecisionLoading] = useState(false); + const [windowSize, setWindowSize] = useState(20); const [alert, setAlert] = useState<{ open: boolean; message: string; severity: 'success' | 'error' }>({ open: false, message: '', severity: 'success' }); + // Monitoring status state + const [monitoringStatus, setMonitoringStatus] = useState(null); + // Use refs to keep track of the interval const intervalRef = useRef | null>(null); const updateIntervalMs = 5000; // 5 seconds refresh rate @@ -113,7 +143,20 @@ const Temperature = () => { }; }, [fetchData]); - // Process data for charts + // Set up monitoring status polling + useEffect(() => { + // Start polling for monitoring status + monitoringService.startStatusPolling((status) => { + setMonitoringStatus(status); + }, 5000); // Poll every 5 seconds + + // Cleanup function to stop polling when component unmounts + return () => { + monitoringService.stopStatusPolling(); + }; + }, []); + + // Process data for charts with sliding window const prepareChartData = () => { if (!data || data.length === 0) { console.log('No data available, using fallback data'); @@ -130,7 +173,7 @@ const Temperature = () => { return fallbackData; } - return data.map(item => ({ + const processedData = data.map(item => ({ currentTimestamp: new Date(item.now_timestamp), futureTimestamp: new Date(item.future_timestamp), currentPower: parseFloat(item.power), @@ -138,20 +181,154 @@ const Temperature = () => { currentTemp: parseFloat(item.env_temp_cur), predictedTemp: parseFloat(item.env_temp_min), })); + + // Apply sliding window - show only last N records + const slidingData = processedData.slice(-windowSize); + + console.log('Processed chart data:', { + totalRecords: processedData.length, + showingRecords: slidingData.length, + timeRange: { + start: slidingData[0]?.currentTimestamp, + end: slidingData[slidingData.length - 1]?.currentTimestamp + } + }); + + return slidingData; }; const chartData = prepareChartData(); - // Extract data arrays for charts - const currentTimestamps = chartData.map(item => item.currentTimestamp); - const futureTimestamps = chartData.map(item => item.futureTimestamp); - const currentPowerData = chartData.map(item => item.currentPower); - const predictedPowerData = chartData.map(item => item.predictedPower); - const currentTempData = chartData.map(item => item.currentTemp); - const predictedTempData = chartData.map(item => item.predictedTemp); + // Prepare Chart.js data structures + const powerChartData = { + datasets: [ + { + label: 'Current Power', + data: chartData.map(item => ({ + x: item.currentTimestamp, + y: item.currentPower + })), + borderColor: '#028a4a', + backgroundColor: '#028a4a', + pointBackgroundColor: '#028a4a', + pointBorderColor: '#028a4a', + pointRadius: 0, + pointHoverRadius: 6, + tension: 0.4, + fill: false + }, + { + label: 'Predicted Power (Dynamic)', + data: chartData.map(item => ({ + x: item.futureTimestamp, + y: item.predictedPower + })), + borderColor: '#ff9800', + backgroundColor: '#ff9800', + pointBackgroundColor: '#ff9800', + pointBorderColor: '#ff9800', + pointRadius: 0, + pointHoverRadius: 6, + tension: 0.4, + fill: false + } + ] + }; + + const temperatureChartData = { + datasets: [ + { + label: 'Current Temperature', + data: chartData.map(item => ({ + x: item.currentTimestamp, + y: item.currentTemp + })), + borderColor: '#028a4a', + backgroundColor: '#028a4a', + pointBackgroundColor: '#028a4a', + pointBorderColor: '#028a4a', + pointRadius: 0, + pointHoverRadius: 6, + tension: 0.4, + fill: false + }, + { + label: 'Predicted Temperature (Dynamic)', + data: chartData.map(item => ({ + x: item.futureTimestamp, + y: item.predictedTemp + })), + borderColor: '#ff9800', + backgroundColor: '#ff9800', + pointBackgroundColor: '#ff9800', + pointBorderColor: '#ff9800', + pointRadius: 0, + pointHoverRadius: 6, + tension: 0.4, + fill: false + } + ] + }; + + // Chart.js options + const chartOptions = { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: 'top' as const, + labels: { + usePointStyle: true, + padding: 20, + font: { + size: 12, + weight: 'normal' as const + } + } + }, + tooltip: { + mode: 'index' as const, + intersect: false, + callbacks: { + title: function(context: any) { + const date = new Date(context[0].parsed.x); + return date.toLocaleTimeString(); + }, + label: function(context: any) { + return `${context.dataset.label}: ${context.parsed.y.toFixed(2)}`; + } + } + } + }, + scales: { + x: { + type: 'time' as const, + time: { + displayFormats: { + minute: 'HH:mm:ss' + } + }, + title: { + display: true, + text: 'Time' + } + }, + y: { + title: { + display: true, + text: 'Value' + } + } + }, + interaction: { + mode: 'nearest' as const, + axis: 'x' as const, + intersect: false + } + }; // Debug logging - console.log('Chart data:', chartData); + console.log('Chart.js data structures:', { powerChartData, temperatureChartData }); console.log('Raw data length:', data.length); // Handle temperature decision @@ -206,19 +383,35 @@ const Temperature = () => { }} > - - Environmental Temperature & Power Monitoring (Last 20 Records) - + + + Environmental Temperature & Power Monitoring + + + {lastUpdated && ( { Last updated: {lastUpdated.toLocaleTimeString()} )} - + { > - + - {/* Temperature Decision Panel */} - - - - Temperature Change Decision - - - - - - - @@ -365,44 +511,26 @@ const Temperature = () => { ) : ( - value.toLocaleTimeString(), - }, - ]} - yAxis={[ - { - width: 50, - valueFormatter: (value: number) => `${value} W`, + )} @@ -474,43 +602,26 @@ const Temperature = () => { ) : ( - value.toLocaleTimeString(), - }, - ]} - yAxis={[ - { - width: 50, - valueFormatter: (value: number) => `${value} °C`, + )} @@ -518,6 +629,136 @@ const Temperature = () => { + + {/* Chart Controls */} + + + + + Chart Settings + + + Configure chart display and temperature change proposals + + + + {/* Temperature Change Proposal Section */} + + + Temperature Change Proposal + + + {(!monitoringStatus?.statuses?.environmental?.is_running || !monitoringStatus?.statuses?.preventive?.is_running) ? ( + + + + Waiting for services... + + + ) : data.length > 0 && chartData.some(item => !isNaN(item.currentTemp) && item.currentTemp > 0) ? ( + + + + + ) : ( + + No temperature data available + + )} + + + + + + Records to show: + + 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 }} + /> + + {windowSize} + + + {/* Snackbar for alerts */}