forked from BLC/AyposWeb
785 lines
25 KiB
TypeScript
785 lines
25 KiB
TypeScript
import { useEffect, useState, useCallback, useRef } from 'react';
|
|
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 { 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;
|
|
flag: string;
|
|
env_temp_cur: string;
|
|
now_timestamp: string;
|
|
future_timestamp: string;
|
|
env_temp_min: string;
|
|
power_future_min: string;
|
|
}
|
|
|
|
const API_BASE_URL = config.apiUrl;
|
|
|
|
const Temperature = () => {
|
|
const theme = useTheme();
|
|
const [data, setData] = useState<ChartData[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
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
|
|
|
|
// Create a memoized fetchData function with useCallback
|
|
const fetchData = useCallback(async (showLoadingIndicator = false) => {
|
|
try {
|
|
if (showLoadingIndicator) {
|
|
setRefreshing(true);
|
|
}
|
|
|
|
const response = await fetch(`${API_BASE_URL}/prom/get_chart_data/temperature/20`);
|
|
const result = await response.json();
|
|
|
|
if (result.data && result.data.length > 0) {
|
|
// Sort by timestamp first to ensure we get the latest data
|
|
const sortedData = [...result.data].sort((a, b) =>
|
|
new Date(b.now_timestamp).getTime() - new Date(a.now_timestamp).getTime()
|
|
);
|
|
|
|
// Log the most recent flag
|
|
console.log('Most recent flag:', sortedData[0].flag);
|
|
|
|
// Filter valid data points
|
|
const validData = sortedData.filter((item: any) =>
|
|
item.now_timestamp &&
|
|
item.future_timestamp &&
|
|
item.power &&
|
|
item.power_future_min &&
|
|
item.env_temp_cur &&
|
|
item.env_temp_min
|
|
);
|
|
|
|
// Limit to last 20 records but maintain chronological order
|
|
const last20Data = validData.slice(-20).sort((a, b) =>
|
|
new Date(a.now_timestamp).getTime() - new Date(b.now_timestamp).getTime()
|
|
);
|
|
|
|
console.log(`Data updated at ${new Date().toLocaleTimeString()}:`, {
|
|
totalRecords: result.data.length,
|
|
validRecords: validData.length,
|
|
displayedRecords: last20Data.length,
|
|
latestFlag: last20Data[last20Data.length - 1]?.flag
|
|
});
|
|
|
|
setData(last20Data);
|
|
setLastUpdated(new Date());
|
|
}
|
|
} catch (error) {
|
|
console.error('Error fetching data:', error);
|
|
} finally {
|
|
setLoading(false);
|
|
setRefreshing(false);
|
|
}
|
|
}, []);
|
|
|
|
// Manual refresh handler
|
|
const handleRefresh = () => {
|
|
fetchData(true);
|
|
};
|
|
|
|
// Set up the interval for real-time updates
|
|
useEffect(() => {
|
|
// Initial fetch
|
|
fetchData(true);
|
|
|
|
// Set up interval for auto-refresh
|
|
intervalRef.current = setInterval(() => {
|
|
console.log(`Auto-refreshing data at ${new Date().toLocaleTimeString()}`);
|
|
fetchData(false);
|
|
}, updateIntervalMs);
|
|
|
|
// Cleanup function to clear the interval when component unmounts
|
|
return () => {
|
|
if (intervalRef.current) {
|
|
clearInterval(intervalRef.current);
|
|
intervalRef.current = null;
|
|
}
|
|
};
|
|
}, [fetchData]);
|
|
|
|
// 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');
|
|
// Fallback data for testing
|
|
const now = new Date();
|
|
const fallbackData = Array.from({ length: 10 }, (_, i) => ({
|
|
currentTimestamp: new Date(now.getTime() - (9 - i) * 60000), // 1 minute intervals
|
|
futureTimestamp: new Date(now.getTime() - (9 - i) * 60000 + 3 * 60000), // 3 minutes in the future
|
|
currentPower: 95 + Math.random() * 10,
|
|
predictedPower: 115 + Math.random() * 10,
|
|
currentTemp: 25 + Math.random() * 5,
|
|
predictedTemp: 28 + Math.random() * 5,
|
|
}));
|
|
return fallbackData;
|
|
}
|
|
|
|
const processedData = data.map(item => ({
|
|
currentTimestamp: new Date(item.now_timestamp),
|
|
futureTimestamp: new Date(item.future_timestamp),
|
|
currentPower: parseFloat(item.power),
|
|
predictedPower: parseFloat(item.power_future_min),
|
|
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();
|
|
|
|
// 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.js data structures:', { powerChartData, temperatureChartData });
|
|
console.log('Raw data length:', data.length);
|
|
|
|
// Handle temperature decision
|
|
const handleTemperatureDecision = async (approval: boolean) => {
|
|
try {
|
|
setDecisionLoading(true);
|
|
const response = await fetch(`${API_BASE_URL}/prom/temperature/decisions?approval=${approval}`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'accept': 'application/json',
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to send temperature decision: ${response.statusText}`);
|
|
}
|
|
|
|
const result = await response.json();
|
|
setAlert({
|
|
open: true,
|
|
message: result.message || `Temperature change ${approval ? 'approved' : 'declined'} successfully`,
|
|
severity: 'success'
|
|
});
|
|
|
|
// Refresh data after decision
|
|
await fetchData(true);
|
|
} catch (error) {
|
|
console.error('Error sending temperature decision:', error);
|
|
setAlert({
|
|
open: true,
|
|
message: error instanceof Error ? error.message : 'Failed to send temperature decision',
|
|
severity: 'error'
|
|
});
|
|
} finally {
|
|
setDecisionLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleCloseAlert = () => {
|
|
setAlert(prev => ({ ...prev, open: false }));
|
|
};
|
|
|
|
return (
|
|
<Box sx={{ flexGrow: 1, bgcolor: theme.palette.background.default }}>
|
|
<AppBar
|
|
position="static"
|
|
elevation={0}
|
|
sx={{
|
|
bgcolor: 'background.paper',
|
|
borderBottom: `1px solid ${theme.palette.divider}`,
|
|
mb: 3
|
|
}}
|
|
>
|
|
<Toolbar sx={{ px: { xs: 2, sm: 4 } }}>
|
|
<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
|
|
variant="body2"
|
|
color="text.secondary"
|
|
sx={{ mr: 2, display: { xs: 'none', sm: 'block' } }}
|
|
>
|
|
Last updated: {lastUpdated.toLocaleTimeString()}
|
|
</Typography>
|
|
)}
|
|
<MuiTooltip title="Refresh data">
|
|
<IconButton
|
|
onClick={handleRefresh}
|
|
color="primary"
|
|
disabled={loading || refreshing}
|
|
sx={{
|
|
animation: refreshing ? 'spin 1s linear infinite' : 'none',
|
|
'@keyframes spin': {
|
|
'0%': { transform: 'rotate(0deg)' },
|
|
'100%': { transform: 'rotate(360deg)' }
|
|
}
|
|
}}
|
|
>
|
|
<RefreshIcon />
|
|
</IconButton>
|
|
</MuiTooltip>
|
|
</Box>
|
|
</Toolbar>
|
|
</AppBar>
|
|
|
|
<Box sx={{ p: { xs: 2, sm: 4 } }}>
|
|
|
|
<Fade in timeout={800}>
|
|
<Grid container spacing={3}>
|
|
{/* Power Chart */}
|
|
<Grid item xs={12} md={6}>
|
|
<Paper
|
|
elevation={0}
|
|
sx={{
|
|
p: { xs: 2, sm: 3 },
|
|
bgcolor: 'background.paper',
|
|
borderRadius: 2,
|
|
border: `1px solid ${theme.palette.divider}`,
|
|
height: '100%',
|
|
position: 'relative'
|
|
}}
|
|
>
|
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
|
<Typography
|
|
variant="h6"
|
|
sx={{
|
|
color: theme.palette.text.primary,
|
|
fontWeight: 500
|
|
}}
|
|
>
|
|
Power Consumption
|
|
</Typography>
|
|
{data.length > 0 && (
|
|
<Chip
|
|
label={data[data.length - 1]?.flag || 'N/A'}
|
|
color={data[data.length - 1]?.flag === '25' ? 'success' : 'warning'}
|
|
size="medium"
|
|
sx={{
|
|
height: 32,
|
|
'& .MuiChip-label': {
|
|
px: 2,
|
|
fontSize: '0.875rem',
|
|
fontWeight: 600
|
|
}
|
|
}}
|
|
/>
|
|
)}
|
|
</Box>
|
|
|
|
{refreshing && (
|
|
<Box
|
|
sx={{
|
|
position: 'absolute',
|
|
top: 10,
|
|
right: 10,
|
|
zIndex: 10,
|
|
bgcolor: 'rgba(255,255,255,0.8)',
|
|
borderRadius: '50%',
|
|
p: 0.5
|
|
}}
|
|
>
|
|
<CircularProgress size={24} thickness={5} />
|
|
</Box>
|
|
)}
|
|
|
|
{loading ? (
|
|
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '400px' }}>
|
|
<CircularProgress size={40} thickness={4} />
|
|
<Typography variant="h6" sx={{ ml: 2 }} color="text.secondary">
|
|
Loading power data...
|
|
</Typography>
|
|
</Box>
|
|
) : (
|
|
<Box sx={{ height: 400 }}>
|
|
<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`;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}}
|
|
/>
|
|
</Box>
|
|
)}
|
|
</Paper>
|
|
</Grid>
|
|
|
|
{/* Temperature Chart */}
|
|
<Grid item xs={12} md={6}>
|
|
<Paper
|
|
elevation={0}
|
|
sx={{
|
|
p: { xs: 2, sm: 3 },
|
|
bgcolor: 'background.paper',
|
|
borderRadius: 2,
|
|
border: `1px solid ${theme.palette.divider}`,
|
|
height: '100%',
|
|
position: 'relative'
|
|
}}
|
|
>
|
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
|
<Typography
|
|
variant="h6"
|
|
sx={{
|
|
color: theme.palette.text.primary,
|
|
fontWeight: 500
|
|
}}
|
|
>
|
|
Environmental Temperature
|
|
</Typography>
|
|
{data.length > 0 && (
|
|
<Chip
|
|
label={data[data.length - 1]?.flag || 'N/A'}
|
|
color={data[data.length - 1]?.flag === '25' ? 'success' : 'warning'}
|
|
size="medium"
|
|
sx={{
|
|
height: 32,
|
|
'& .MuiChip-label': {
|
|
px: 2,
|
|
fontSize: '0.875rem',
|
|
fontWeight: 600
|
|
}
|
|
}}
|
|
/>
|
|
)}
|
|
</Box>
|
|
|
|
{refreshing && (
|
|
<Box
|
|
sx={{
|
|
position: 'absolute',
|
|
top: 10,
|
|
right: 10,
|
|
zIndex: 10,
|
|
bgcolor: 'rgba(255,255,255,0.8)',
|
|
borderRadius: '50%',
|
|
p: 0.5
|
|
}}
|
|
>
|
|
<CircularProgress size={24} thickness={5} />
|
|
</Box>
|
|
)}
|
|
|
|
{loading ? (
|
|
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '400px' }}>
|
|
<CircularProgress size={40} thickness={4} />
|
|
<Typography variant="h6" sx={{ ml: 2 }} color="text.secondary">
|
|
Loading temperature data...
|
|
</Typography>
|
|
</Box>
|
|
) : (
|
|
<Box sx={{ height: 400 }}>
|
|
<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`;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}}
|
|
/>
|
|
</Box>
|
|
)}
|
|
</Paper>
|
|
</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 */}
|
|
<Snackbar
|
|
open={alert.open}
|
|
autoHideDuration={6000}
|
|
onClose={handleCloseAlert}
|
|
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
|
>
|
|
<Alert
|
|
onClose={handleCloseAlert}
|
|
severity={alert.severity}
|
|
variant="filled"
|
|
sx={{ width: '100%' }}
|
|
>
|
|
{alert.message}
|
|
</Alert>
|
|
</Snackbar>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
export default Temperature;
|