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:
@@ -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 */}
|
||||
|
||||
Reference in New Issue
Block a user