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

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

View File

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