diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..35a43d3 --- /dev/null +++ b/.env.example @@ -0,0 +1,10 @@ +# API Configuration +VITE_API_URL=http://your-api-url:8003 + +# Vercel Deployment Configuration +NEXT_PUBLIC_ALLOWED_HOSTS=your-allowed-hosts +NEXT_PUBLIC_VERCEL_URL=${VERCEL_URL} +NODE_ENV=development + +# CORS Configuration +CORS_ORIGIN=* \ No newline at end of file diff --git a/src/components/Charts/MaintenanceChart.tsx.bak b/src/components/Charts/MaintenanceChart.tsx.bak deleted file mode 100644 index f6e4541..0000000 --- a/src/components/Charts/MaintenanceChart.tsx.bak +++ /dev/null @@ -1,441 +0,0 @@ -import React, { useMemo } from 'react'; -import ReactECharts from 'echarts-for-react'; -import { useTheme } from '@mui/material/styles'; -import { Box, Typography } from '@mui/material'; - fontWeight: 500, - color: theme.palette.text.secondary - }, - icon: 'circle', - itemHeight: 10, - itemWidth: 10, - selectedMode: false - },t { Box, Typography } from '@mui/material'; - -interface MaintenanceDataPoint { - currentTimestamp: Date; - futureTimestamp: Date; - currentPower: number; - predictedPower: number; - positive3p: number; - negative3p: number; - positive7p: number; - negative7p: number; -} - -interface MaintenanceChartProps { - data: MaintenanceDataPoint[]; - height?: number; - zoomRange?: [number, number]; - onZoomChange?: (range: [number, number]) => void; -} - -const MaintenanceChart: React.FC = React.memo(({ - data, - height = 400, - zoomRange, - onZoomChange -}) => { - const theme = useTheme(); - - const commonSeriesSettings = { - sampling: 'lttb', - animation: false, - emphasis: { - focus: 'series', - itemStyle: { - borderWidth: 3, - shadowBlur: 10 - } - } - }; - - const option = useMemo(() => { - if (!data || data.length === 0) { - return {}; - } - - const currentData = data.map(item => [ - item.currentTimestamp.getTime(), - Number(item.currentPower.toFixed(2)) - ]); - - const predictedData = data.map(item => [ - item.futureTimestamp.getTime(), - Number(item.predictedPower.toFixed(2)) - ]); - - const positive3pData = data.map(item => [ - item.futureTimestamp.getTime(), - Number(item.positive3p.toFixed(2)) - ]); - - const negative3pData = data.map(item => [ - item.futureTimestamp.getTime(), - Number(item.negative3p.toFixed(2)) - ]); - - const positive7pData = data.map(item => [ - item.futureTimestamp.getTime(), - Number(item.positive7p.toFixed(2)) - ]); - - const negative7pData = data.map(item => [ - item.futureTimestamp.getTime(), - Number(item.negative7p.toFixed(2)) - ]); - - const allValues = [ - ...data.map(d => d.currentPower), - ...data.map(d => d.predictedPower), - ...data.map(d => d.positive3p), - ...data.map(d => d.negative3p), - ...data.map(d => d.positive7p), - ...data.map(d => d.negative7p) - ]; - - const minValue = Math.min(...allValues); - const maxValue = Math.max(...allValues); - const valuePadding = (maxValue - minValue) * 0.1; - - return { - animation: false, - progressive: 500, - progressiveThreshold: 1000, - renderer: 'canvas', - title: { - text: 'Preventive Maintenance Monitoring', - subtext: 'Power consumption with predictive thresholds', - textStyle: { - fontSize: 20, - fontWeight: 'bold', - color: theme.palette.text.primary, - fontFamily: 'Montserrat, sans-serif' - }, - subtextStyle: { - fontSize: 14, - color: theme.palette.text.secondary, - fontFamily: 'Montserrat, sans-serif' - }, - left: '5%', - top: 0, - padding: [0, 0, 12, 0] - }, - tooltip: { - trigger: 'axis', - backgroundColor: 'rgba(255, 255, 255, 0.95)', - borderColor: theme.palette.primary.main, - borderWidth: 1, - textStyle: { - color: theme.palette.text.primary, - fontFamily: 'Montserrat, sans-serif' - }, - formatter: function (params: any) { - const time = new Date(params[0].value[0]).toLocaleString(); - let html = `
${time}
`; - params.forEach((param: any) => { - html += ` -
- - ${param.seriesName}: - ${param.value[1]}W -
- `; - }); - return html; - } - }, - legend: { - data: ['Current Power', 'Predicted Power', '+3% Threshold', '-3% Threshold', '+7% Threshold', '-7% Threshold'], - top: 40, - textStyle: { - fontFamily: 'Montserrat, sans-serif', - fontSize: 12, - color: theme.palette.text.secondary - } - }, - grid: { - left: '5%', - right: '5%', - bottom: '12%', - top: '15%', - containLabel: true - }, - toolbox: { - feature: { - dataZoom: { - yAxisIndex: 'none', - title: { - zoom: 'Zoom', - back: 'Reset Zoom' - } - }, - restore: { - title: 'Reset' - }, - saveAsImage: { - title: 'Save' - } - }, - right: 15, - top: 5 - }, - dataZoom: [ - { - type: 'slider', - show: true, - xAxisIndex: [0], - start: zoomRange?.[0] ?? 0, - end: zoomRange?.[1] ?? 100, - height: 20, - bottom: 0, - borderColor: theme.palette.divider, - fillerColor: 'rgba(2, 138, 74, 0.1)', - textStyle: { - color: theme.palette.text.secondary, - fontFamily: 'Montserrat, sans-serif' - }, - handleStyle: { - color: theme.palette.primary.main - } - }, - { - type: 'inside', - xAxisIndex: [0], - start: 0, - end: 100, - zoomOnMouseWheel: 'shift' - } - ], - xAxis: { - type: 'time', - boundaryGap: false, - axisLabel: { - formatter: function (value: number) { - const date = new Date(value); - return date.toLocaleTimeString('en-US', { - hour: '2-digit', - minute: '2-digit' - }); - }, - color: theme.palette.text.secondary, - fontFamily: 'Montserrat, sans-serif' - }, - axisLine: { - lineStyle: { - color: theme.palette.divider - } - }, - splitLine: { - show: true, - lineStyle: { - color: theme.palette.divider, - type: 'dashed', - opacity: 0.5 - } - } - }, - yAxis: { - type: 'value', - name: 'Power (W)', - nameTextStyle: { - color: theme.palette.text.secondary, - fontFamily: 'Montserrat, sans-serif', - fontWeight: 'bold' - }, - min: Math.max(0, minValue - valuePadding), - max: maxValue + valuePadding, - axisLabel: { - formatter: '{value}W', - color: theme.palette.text.secondary, - fontFamily: 'Montserrat, sans-serif' - }, - axisLine: { - lineStyle: { - color: theme.palette.divider - } - }, - splitLine: { - lineStyle: { - color: theme.palette.divider, - type: 'dashed', - opacity: 0.3 - } - } - }, - series: [ - { - ...commonSeriesSettings, - name: 'Current Power', - type: 'line', - data: currentData, - smooth: true, - lineStyle: { - color: theme.palette.primary.main, - width: 3 - }, - itemStyle: { - color: theme.palette.primary.main, - borderWidth: 2, - borderColor: '#fff' - }, - areaStyle: { - color: { - type: 'linear', - x: 0, - y: 0, - x2: 0, - y2: 1, - colorStops: [{ - offset: 0, - color: `rgba(2, 138, 74, 0.3)` - }, { - offset: 1, - color: `rgba(2, 138, 74, 0.05)` - }] - } - }, - symbol: 'circle', - symbolSize: 6, - showSymbol: true - }, - { - ...commonSeriesSettings, - name: 'Predicted Power', - type: 'line', - data: predictedData, - smooth: true, - lineStyle: { - color: theme.palette.warning.main, - width: 3, - type: 'dashed' - }, - itemStyle: { - color: theme.palette.warning.main, - borderWidth: 2, - borderColor: '#fff' - }, - symbol: 'circle', - symbolSize: 6, - showSymbol: true - }, - { - ...commonSeriesSettings, - name: '±3% Threshold', - type: 'line', - data: positive3pData.concat(negative3pData.reverse()), - smooth: true, - lineStyle: { - color: theme.palette.warning.light, - width: 1, - type: 'dashed' - }, - areaStyle: { - color: theme.palette.warning.light, - opacity: 0.1 - }, - symbol: 'none' - }, - { - ...commonSeriesSettings, - name: '±7% Threshold', - type: 'line', - data: positive7pData.concat(negative7pData.reverse()), - smooth: true, - lineStyle: { - color: theme.palette.error.light, - width: 1, - type: 'dashed' - }, - areaStyle: { - color: theme.palette.error.light, - opacity: 0.1 - }, - symbol: 'none' - } - ] - }; - }, [data, theme, zoomRange]); - - if (!data || data.length === 0) { - return ( - - - No maintenance data available - - - ); - } - - return ( - - { - if (onZoomChange && params.batch?.[0]) { - onZoomChange([params.batch[0].start, params.batch[0].end]); - } - } - }} - /> - - ); -}, (prevProps, nextProps) => { - if (prevProps.height !== nextProps.height) return false; - if (prevProps.zoomRange?.[0] !== nextProps.zoomRange?.[0] || - prevProps.zoomRange?.[1] !== nextProps.zoomRange?.[1]) return false; - if (prevProps.data.length !== nextProps.data.length) return false; - - const compareCount = 5; // Compare last 5 points for smooth transitions - for (let i = 1; i <= compareCount; i++) { - const prevItem = prevProps.data[prevProps.data.length - i]; - const nextItem = nextProps.data[nextProps.data.length - i]; - - if (!prevItem || !nextItem) return false; - - if (prevItem.currentTimestamp.getTime() !== nextItem.currentTimestamp.getTime() || - prevItem.currentPower !== nextItem.currentPower || - prevItem.predictedPower !== nextItem.predictedPower) { - return false; - } - } - - return true; -}); - -MaintenanceChart.displayName = 'MaintenanceChart'; - -export default MaintenanceChart; \ No newline at end of file diff --git a/src/components/Charts/MaintenanceChart.tsx.new b/src/components/Charts/MaintenanceChart.tsx.new deleted file mode 100644 index 04571c7..0000000 --- a/src/components/Charts/MaintenanceChart.tsx.new +++ /dev/null @@ -1,446 +0,0 @@ -import React, { useMemo } from 'react'; -import ReactECharts from 'echarts-for-react'; -import { useTheme } from '@mui/material/styles'; -import { Box, Typography } from '@mui/material'; - -interface MaintenanceDataPoint { - currentTimestamp: Date; - futureTimestamp: Date; - currentPower: number; - predictedPower: number; - positive3p: number; - negative3p: number; - positive7p: number; - negative7p: number; -} - -interface MaintenanceChartProps { - data: MaintenanceDataPoint[]; - height?: number; - zoomRange?: [number, number]; - onZoomChange?: (range: [number, number]) => void; -} - -const MaintenanceChart: React.FC = React.memo(({ - data, - height = 400, - zoomRange, - onZoomChange -}) => { - const theme = useTheme(); - - const commonSeriesSettings = { - sampling: 'lttb', - animation: false, - emphasis: { - focus: 'series', - itemStyle: { - borderWidth: 3, - shadowBlur: 10 - } - } - }; - - const option = useMemo(() => { - if (!data || data.length === 0) { - return {}; - } - - const currentData = data.map(item => [ - item.currentTimestamp.getTime(), - Number(item.currentPower.toFixed(2)) - ]); - - const predictedData = data.map(item => [ - item.futureTimestamp.getTime(), - Number(item.predictedPower.toFixed(2)) - ]); - - const positive3pData = data.map(item => [ - item.futureTimestamp.getTime(), - Number(item.positive3p.toFixed(2)) - ]); - - const negative3pData = data.map(item => [ - item.futureTimestamp.getTime(), - Number(item.negative3p.toFixed(2)) - ]); - - const positive7pData = data.map(item => [ - item.futureTimestamp.getTime(), - Number(item.positive7p.toFixed(2)) - ]); - - const negative7pData = data.map(item => [ - item.futureTimestamp.getTime(), - Number(item.negative7p.toFixed(2)) - ]); - - const allValues = [ - ...data.map(d => d.currentPower), - ...data.map(d => d.predictedPower), - ...data.map(d => d.positive3p), - ...data.map(d => d.negative3p), - ...data.map(d => d.positive7p), - ...data.map(d => d.negative7p) - ]; - - const minValue = Math.min(...allValues); - const maxValue = Math.max(...allValues); - const valuePadding = (maxValue - minValue) * 0.1; - - return { - animation: false, - progressive: 500, - progressiveThreshold: 1000, - renderer: 'canvas', - title: { - text: 'Preventive Maintenance Monitoring', - subtext: 'Power consumption with predictive thresholds', - textStyle: { - fontSize: 20, - fontWeight: 'bold', - color: theme.palette.text.primary, - fontFamily: 'Montserrat, sans-serif' - }, - subtextStyle: { - fontSize: 14, - color: theme.palette.text.secondary, - fontFamily: 'Montserrat, sans-serif' - }, - left: '5%', - top: 0, - padding: [0, 0, 12, 0] - }, - tooltip: { - trigger: 'axis', - backgroundColor: 'rgba(255, 255, 255, 0.95)', - borderColor: theme.palette.primary.main, - borderWidth: 1, - textStyle: { - color: theme.palette.text.primary, - fontFamily: 'Montserrat, sans-serif' - }, - formatter: function (params: any) { - const time = new Date(params[0].value[0]).toLocaleString(); - let html = `
${time}
`; - params.forEach((param: any) => { - html += ` -
- - ${param.seriesName}: - ${param.value[1]}W -
- `; - }); - return html; - } - }, - legend: { - data: ['Current Power', 'Predicted Power', '\u00b13% Threshold', '\u00b17% Threshold'], - top: 'top', - right: '5%', - padding: [8, 16], - itemGap: 24, - textStyle: { - fontFamily: 'Montserrat, sans-serif', - fontSize: 13, - fontWeight: 500, - color: theme.palette.text.secondary - }, - icon: 'circle', - itemHeight: 10, - itemWidth: 10, - selectedMode: false - }, - grid: { - left: '5%', - right: '5%', - bottom: '12%', - top: '15%', - containLabel: true - }, - toolbox: { - right: 90, - top: 0, - feature: { - dataZoom: { - yAxisIndex: 'none', - title: { - zoom: 'Zoom', - back: 'Reset Zoom' - } - }, - restore: { - title: 'Reset' - }, - saveAsImage: { - title: 'Save' - } - } - }, - dataZoom: [ - { - type: 'slider', - show: true, - xAxisIndex: [0], - start: zoomRange?.[0] ?? 0, - end: zoomRange?.[1] ?? 100, - height: 20, - bottom: 10, - borderColor: theme.palette.divider, - fillerColor: 'rgba(2, 138, 74, 0.1)', - textStyle: { - color: theme.palette.text.secondary, - fontFamily: 'Montserrat, sans-serif' - }, - handleStyle: { - color: theme.palette.primary.main - } - }, - { - type: 'inside', - xAxisIndex: [0], - start: 0, - end: 100, - zoomOnMouseWheel: 'shift' - } - ], - xAxis: { - type: 'time', - boundaryGap: false, - axisLabel: { - formatter: function (value: number) { - const date = new Date(value); - return date.toLocaleTimeString('en-US', { - hour: '2-digit', - minute: '2-digit' - }); - }, - color: theme.palette.text.secondary, - fontFamily: 'Montserrat, sans-serif', - fontSize: 12 - }, - axisLine: { - lineStyle: { - color: theme.palette.divider - } - }, - splitLine: { - show: true, - lineStyle: { - color: theme.palette.divider, - type: 'dashed', - opacity: 0.5 - } - } - }, - yAxis: { - type: 'value', - name: 'Power (W)', - nameTextStyle: { - color: theme.palette.text.secondary, - fontFamily: 'Montserrat, sans-serif', - fontSize: 13, - fontWeight: 'bold', - padding: [0, 0, 8, 0] - }, - min: Math.max(0, minValue - valuePadding), - max: maxValue + valuePadding, - axisLabel: { - formatter: '{value}W', - color: theme.palette.text.secondary, - fontFamily: 'Montserrat, sans-serif', - fontSize: 12 - }, - axisLine: { - lineStyle: { - color: theme.palette.divider - } - }, - splitLine: { - lineStyle: { - color: theme.palette.divider, - type: 'dashed', - opacity: 0.3 - } - } - }, - series: [ - { - ...commonSeriesSettings, - name: 'Current Power', - type: 'line', - data: currentData, - smooth: true, - lineStyle: { - color: theme.palette.primary.main, - width: 3 - }, - itemStyle: { - color: theme.palette.primary.main, - borderWidth: 2, - borderColor: '#fff' - }, - areaStyle: { - color: { - type: 'linear', - x: 0, - y: 0, - x2: 0, - y2: 1, - colorStops: [{ - offset: 0, - color: `rgba(2, 138, 74, 0.3)` - }, { - offset: 1, - color: `rgba(2, 138, 74, 0.05)` - }] - } - }, - symbol: 'circle', - symbolSize: 6, - showSymbol: true - }, - { - ...commonSeriesSettings, - name: 'Predicted Power', - type: 'line', - data: predictedData, - smooth: true, - lineStyle: { - color: theme.palette.warning.main, - width: 3, - type: 'dashed' - }, - itemStyle: { - color: theme.palette.warning.main, - borderWidth: 2, - borderColor: '#fff' - }, - symbol: 'circle', - symbolSize: 6, - showSymbol: true - }, - { - ...commonSeriesSettings, - name: '\u00b13% Threshold', - type: 'line', - data: positive3pData.concat(negative3pData.reverse()), - smooth: true, - lineStyle: { - color: theme.palette.warning.light, - width: 1, - type: 'dashed' - }, - areaStyle: { - color: theme.palette.warning.light, - opacity: 0.1 - }, - symbol: 'none' - }, - { - ...commonSeriesSettings, - name: '\u00b17% Threshold', - type: 'line', - data: positive7pData.concat(negative7pData.reverse()), - smooth: true, - lineStyle: { - color: theme.palette.error.light, - width: 1, - type: 'dashed' - }, - areaStyle: { - color: theme.palette.error.light, - opacity: 0.1 - }, - symbol: 'none' - } - ] - }; - }, [data, theme, zoomRange]); - - if (!data || data.length === 0) { - return ( - - - No maintenance data available - - - ); - } - - return ( - - { - if (onZoomChange && params.batch?.[0]) { - onZoomChange([params.batch[0].start, params.batch[0].end]); - } - } - }} - /> - - ); -}, (prevProps, nextProps) => { - if (prevProps.height !== nextProps.height) return false; - if (prevProps.zoomRange?.[0] !== nextProps.zoomRange?.[0] || - prevProps.zoomRange?.[1] !== nextProps.zoomRange?.[1]) return false; - if (prevProps.data.length !== nextProps.data.length) return false; - - const compareCount = 5; // Compare last 5 points for smooth transitions - for (let i = 1; i <= compareCount; i++) { - const prevItem = prevProps.data[prevProps.data.length - i]; - const nextItem = nextProps.data[nextProps.data.length - i]; - - if (!prevItem || !nextItem) return false; - - if (prevItem.currentTimestamp.getTime() !== nextItem.currentTimestamp.getTime() || - prevItem.currentPower !== nextItem.currentPower || - prevItem.predictedPower !== nextItem.predictedPower) { - return false; - } - } - - return true; -}); - -MaintenanceChart.displayName = 'MaintenanceChart'; - -export default MaintenanceChart; \ No newline at end of file diff --git a/src/components/Charts/TemperatureControl.tsx b/src/components/Charts/TemperatureControl.tsx new file mode 100644 index 0000000..0280bee --- /dev/null +++ b/src/components/Charts/TemperatureControl.tsx @@ -0,0 +1,279 @@ +import React, { useState } from 'react'; +import { + Box, + Typography, + Button, + CircularProgress, + Paper, + Chip, + Tooltip, + alpha +} from '@mui/material'; +import CheckCircleIcon from '@mui/icons-material/CheckCircle'; +import CancelIcon from '@mui/icons-material/Cancel'; +import { MonitoringStatus } from '../../types/monitoring'; +import { useTheme } from '@mui/material/styles'; + +interface TemperatureControlProps { + onApprove: () => Promise; + onDecline: () => Promise; + monitoringStatus: MonitoringStatus | null; + disabled?: boolean; + isLoading?: boolean; + hasData: boolean; + temperatureFlag?: string; +} + +const TemperatureControl: React.FC = ({ + onApprove, + onDecline, + monitoringStatus, + disabled = false, + isLoading = false, + hasData, + temperatureFlag +}) => { + const theme = useTheme(); + const [decisionLoading, setDecisionLoading] = useState(false); + + const handleApprove = async () => { + try { + setDecisionLoading(true); + await onApprove(); + } finally { + setDecisionLoading(false); + } + }; + + const handleDecline = async () => { + try { + setDecisionLoading(true); + await onDecline(); + } finally { + setDecisionLoading(false); + } + }; + + const servicesRunning = monitoringStatus?.statuses?.environmental?.is_running && + monitoringStatus?.statuses?.preventive?.is_running; + + const getButtonTooltip = () => { + if (!servicesRunning) return 'Waiting for monitoring services to start'; + if (!hasData) return 'No temperature data available'; + if (isLoading) return 'Loading data...'; + if (decisionLoading) return 'Processing request...'; + return ''; + }; + + return ( + + + + + + + Temperature Control + + {temperatureFlag && ( + + Flag: + + )} + + + Review and respond to temperature change proposals + + + + + + + + + + + {!servicesRunning && ( + + + + Waiting for services to initialize... + + + )} + + {!hasData && ( + + No temperature data available for review + + )} + + + + + + + + + + + + + + + + + + ); +}; + +export default TemperatureControl; \ No newline at end of file diff --git a/src/config/env.ts b/src/config/env.ts index 64df7b8..079a899 100644 --- a/src/config/env.ts +++ b/src/config/env.ts @@ -4,10 +4,24 @@ const getApiUrl = (): string => { if (import.meta.env.PROD) { return '/api'; } - // In development, use the direct URL - return import.meta.env.VITE_API_URL || 'http://aypos-api.blc-css.com'; + // In development, use the direct URL from environment variable + return import.meta.env.VITE_API_URL || 'http://141.196.166.241:8003'; +}; + +const getVercelUrl = (): string => { + return import.meta.env.NEXT_PUBLIC_VERCEL_URL || ''; +}; + +const getAllowedHosts = (): string[] => { + const hosts = import.meta.env.NEXT_PUBLIC_ALLOWED_HOSTS; + return hosts ? hosts.split(',') : ['141.196.166.241']; }; export const config = { apiUrl: getApiUrl(), + vercelUrl: getVercelUrl(), + allowedHosts: getAllowedHosts(), + isDevelopment: import.meta.env.DEV, + isProduction: import.meta.env.PROD, + corsOrigin: import.meta.env.CORS_ORIGIN || '*' } as const; \ No newline at end of file diff --git a/src/pages/Temperature.tsx b/src/pages/Temperature.tsx index fa3a7fb..ebda422 100644 --- a/src/pages/Temperature.tsx +++ b/src/pages/Temperature.tsx @@ -1,7 +1,10 @@ import { useEffect, useState, useCallback, useMemo, useRef } from 'react'; -import { Box, Paper, Grid } from '@mui/material'; +import { Box, Paper, Grid, Alert, Snackbar } from '@mui/material'; import { config } from '../config/env'; import MonitoringChart from '../components/Charts/MonitoringChart'; +import TemperatureControl from '../components/Charts/TemperatureControl'; +import { monitoringService } from '../services/monitoringService'; +import { MonitoringStatus } from '../types/monitoring'; interface ChartData { power: string; @@ -20,7 +23,13 @@ const Temperature = () => { const [timeRange, setTimeRange] = useState(20); const [powerZoom, setPowerZoom] = useState<[number, number]>([0, 100]); const [tempZoom, setTempZoom] = useState<[number, number]>([0, 100]); - + const [monitoringStatus, setMonitoringStatus] = useState(null); + const [decisionLoading, setDecisionLoading] = useState(false); + const [alert, setAlert] = useState<{ open: boolean; message: string; severity: 'success' | 'error' }>({ + open: false, + message: '', + severity: 'success' + }); const [isLoading, setIsLoading] = useState(false); const lastFetchRef = useRef(0); const THROTTLE_INTERVAL = 2000; // Minimum 2 seconds between updates @@ -70,9 +79,50 @@ const Temperature = () => { useEffect(() => { fetchData(); const interval = setInterval(fetchData, 5000); - return () => clearInterval(interval); + + // Start monitoring status polling + monitoringService.startStatusPolling(setMonitoringStatus, 5000); + + return () => { + clearInterval(interval); + monitoringService.stopStatusPolling(); + }; }, [fetchData]); + const handleTemperatureDecision = async (approval: boolean) => { + try { + setDecisionLoading(true); + const response = await fetch(`${config.apiUrl}/prom/temperature/decisions?approval=${approval}`, { + method: 'POST', + headers: { + 'accept': 'application/json' + } + }); + + if (!response.ok) { + throw new Error(`Failed to ${approval ? 'approve' : 'decline'} temperature change: ${response.statusText}`); + } + + const result = await response.json(); + setAlert({ + open: true, + message: result.message || `Temperature change ${approval ? 'approved' : 'declined'} successfully`, + severity: 'success' + }); + } catch (error) { + console.error('Error with temperature decision:', error); + setAlert({ + open: true, + message: error instanceof Error ? error.message : 'Failed to process temperature change', + severity: 'error' + }); + } + }; + + const handleCloseAlert = () => { + setAlert(prev => ({ ...prev, open: false })); + }; + // Memoize the transformed data to prevent unnecessary recalculations const { temperatureData, powerData } = useMemo(() => ({ temperatureData: data.map(item => ({ @@ -90,66 +140,97 @@ const Temperature = () => { }), [data]); return ( - - - {[20, 50, 100].map((range) => ( - setTimeRange(range as TimeRange)} - sx={{ - px: 2, - py: 1, - border: '1px solid rgba(0, 0, 0, 0.08)', - cursor: 'pointer', - bgcolor: timeRange === range ? 'primary.main' : 'background.paper', - color: timeRange === range ? 'primary.contrastText' : 'text.primary', - '&:hover': { - bgcolor: timeRange === range ? 'primary.dark' : 'action.hover' - }, - transition: 'all 0.2s' - }} - > - {range} points - - ))} + + + + {[20, 50, 100].map((range) => ( + setTimeRange(range as TimeRange)} + sx={{ + px: 2, + py: 1, + border: '1px solid rgba(0, 0, 0, 0.08)', + cursor: 'pointer', + bgcolor: timeRange === range ? 'primary.main' : 'background.paper', + color: timeRange === range ? 'primary.contrastText' : 'text.primary', + '&:hover': { + bgcolor: timeRange === range ? 'primary.dark' : 'action.hover' + }, + transition: 'all 0.2s' + }} + > + {range} points + + ))} + + + + + + + + + + + + + + + handleTemperatureDecision(true)} + onDecline={() => handleTemperatureDecision(false)} + monitoringStatus={monitoringStatus} + disabled={decisionLoading} + isLoading={isLoading} + hasData={data.length > 0} + temperatureFlag={data[data.length - 1]?.flag} + /> + + temperatureFlag={data[data.length - 1]?.flag} + /> + + - - - - - - - - - - - - + + + {alert.message} + + ); }; -export default Temperature; +export default Temperature; \ No newline at end of file