Enhance Dashboard UI and Status Indicators

- Added a Popover component to provide information about Backend & Tabs status.
- Improved layout and styling of the System Status section, including connection status indicators.
- Updated PLC connection buttons for better user experience.
- Refactored CPU Status and UDP Streaming cards for consistency and clarity.
- Adjusted performance metrics display with improved loading states and error handling.
- Updated last_update timestamp in system_state.json for accurate tracking.
This commit is contained in:
Miguel 2025-08-28 15:50:56 +02:00
parent 4eed5d2687
commit f979817876
3 changed files with 19359 additions and 18695 deletions

File diff suppressed because it is too large Load Diff

View File

@ -51,9 +51,17 @@ import {
ModalHeader,
ModalFooter,
ModalBody,
ModalCloseButton
ModalCloseButton,
IconButton,
Popover,
PopoverTrigger,
PopoverContent,
PopoverArrow,
PopoverCloseButton,
PopoverHeader,
PopoverBody
} from '@chakra-ui/react'
import { ChevronDownIcon, ChevronUpIcon } from '@chakra-ui/icons'
import { ChevronDownIcon, ChevronUpIcon, InfoIcon } from '@chakra-ui/icons'
import { FiUpload } from 'react-icons/fi'
import Form from '@rjsf/chakra-ui'
import validator from '@rjsf/validator-ajv8'
@ -915,208 +923,226 @@ function StatusBar({ status, isConnected, isLeader, connectionError }) {
}
return (
<SimpleGrid columns={{ base: 1, md: 2, lg: 3, xl: 5 }} spacing={4} mb={6}>
<Card>
<CardBody>
<Stat>
<StatLabel>🔌 {t('status.plcConnection')}</StatLabel>
<StatNumber fontSize="lg" color={plcConnected ? 'green.500' : 'red.500'}>
{plcConnected ? t('status.connected') : t('status.disconnected')}
</StatNumber>
{status?.plc_reconnection?.enabled && (
<StatHelpText>
🔄 {t('status.autoReconnection')}: {status?.plc_reconnection?.active ? t('status.reconnecting') : t('status.enabled')}
</StatHelpText>
)}
<Box mt={2}>
{plcConnected ? (
<Button
size="sm"
colorScheme="red"
variant="outline"
onClick={handleDisconnectPlc}
isLoading={actionLoading.disconnect}
loadingText={t('status.disconnecting')}
>
{t('status.disconnect')}
</Button>
) : (
<Button
size="sm"
colorScheme="blue"
variant="outline"
onClick={handleConnectPlc}
isLoading={actionLoading.connect}
loadingText={t('status.connecting')}
>
🔗 {t('status.connect')}
</Button>
)}
</Box>
</Stat>
</CardBody>
</Card>
{/* CPU Status Card - Shows when PLC is connected */}
{plcConnected && cpuStatus && (
<Card>
<CardBody>
<Stat>
<StatLabel>🖥 CPU Status</StatLabel>
<StatNumber fontSize="lg" color={cpuStatus.state === 'RUN' ? 'green.500' : 'orange.500'}>
{cpuStatus.state || 'UNKNOWN'}
</StatNumber>
{cpuStatus.cycle_time_ms !== undefined && (
<StatHelpText>
Cycle: {cpuStatus.cycle_time_ms}ms
{cpuStatus.cpu_info?.module_type_name && (
<><br/>📟 {cpuStatus.cpu_info.module_type_name}</>
<Box mb={6}>
<Flex align="center" justify="space-between" mb={4}>
<Text fontSize="lg" fontWeight="bold">System Status</Text>
<Popover>
<PopoverTrigger>
<IconButton
aria-label="Backend & Tabs Info"
icon={<InfoIcon />}
size="sm"
variant="outline"
colorScheme="blue"
/>
</PopoverTrigger>
<PopoverContent>
<PopoverArrow />
<PopoverCloseButton />
<PopoverHeader fontWeight="bold">🔗 Backend & Tabs</PopoverHeader>
<PopoverBody>
<VStack spacing={2} align="stretch">
<Text fontSize="md" fontWeight="bold" color={
connectionError ? 'red.500' :
isConnected ? 'green.500' : 'orange.500'
}>
{connectionError ? '❌ Backend offline' :
isLeader ? `👑 ${t('status.leader')}` : `👥 ${t('status.follower')}`}
</Text>
<Text fontSize="sm" color="gray.600">
{connectionError ? (
<> Backend connection lost<br/>🔄 Retrying connection...</>
) : (
<>
{isConnected ? `${t('status.connected_status')}` : `${t('status.connecting_status')}`}<br/>
{isLeader ? t('status.makingConnections') : t('status.receivingData')}
</>
)}
</Text>
</VStack>
</PopoverBody>
</PopoverContent>
</Popover>
</Flex>
<SimpleGrid columns={{ base: 1, sm: 2, md: 3, lg: 4, xl: 5 }} spacing={2} mb={6}>
<Card size="sm">
<CardBody py={3}>
<Stat>
<StatLabel fontSize="sm">🔌 {t('status.plcConnection')}</StatLabel>
<StatNumber fontSize="md" color={plcConnected ? 'green.500' : 'red.500'}>
{plcConnected ? t('status.connected') : t('status.disconnected')}
</StatNumber>
{status?.plc_reconnection?.enabled && (
<StatHelpText fontSize="xs">
🔄 {t('status.autoReconnection')}: {status?.plc_reconnection?.active ? t('status.reconnecting') : t('status.enabled')}
</StatHelpText>
)}
<Box mt={2}>
{plcConnected ? (
<Button
size="xs"
colorScheme="red"
variant="outline"
onClick={handleDisconnectPlc}
isLoading={actionLoading.disconnect}
loadingText={t('status.disconnecting')}
>
{t('status.disconnect')}
</Button>
) : (
<Button
size="xs"
colorScheme="blue"
variant="outline"
onClick={handleConnectPlc}
isLoading={actionLoading.connect}
loadingText={t('status.connecting')}
>
🔗 {t('status.connect')}
</Button>
)}
</Box>
</Stat>
</CardBody>
</Card>
{/* CPU Status Card - Shows when PLC is connected */}
{plcConnected && cpuStatus && (
<Card size="sm">
<CardBody py={3}>
<Stat>
<StatLabel fontSize="sm">🖥 CPU Status</StatLabel>
<StatNumber fontSize="sm" color={cpuStatus.state === 'RUN' || cpuStatus.state?.includes('Run') ? 'green.500' : 'orange.500'}>
{cpuStatus.state?.replace('STATE_S7CpuStatus', '').toUpperCase() || 'UNKNOWN'}
</StatNumber>
{cpuStatus.cycle_time_ms !== undefined && (
<StatHelpText fontSize="xs">
Cycle: {cpuStatus.cycle_time_ms}ms
{cpuStatus.cpu_info?.module_type_name && (
<><br/>📟 {cpuStatus.cpu_info.module_type_name}</>
)}
</StatHelpText>
)}
</Stat>
</CardBody>
</Card>
)}
<Card size="sm">
<CardBody py={3}>
<Stat>
<StatLabel fontSize="sm">📡 {t('status.udpStreaming')}</StatLabel>
<StatNumber fontSize="md" color={streaming ? 'green.500' : 'gray.500'}>
{streaming ? t('status.active') : t('status.inactive')}
</StatNumber>
<Box mt={2}>
<VStack spacing={1} align="stretch">
{streaming ? (
<Button
size="xs"
colorScheme="red"
variant="outline"
onClick={handleStopStreaming}
isLoading={actionLoading.stopStream}
loadingText={t('status.stopping')}
>
{t('status.stop')}
</Button>
) : (
<Button
size="xs"
colorScheme="blue"
variant="outline"
onClick={handleStartStreaming}
isLoading={actionLoading.startStream}
loadingText={t('status.starting')}
>
{t('status.start')}
</Button>
)}
{plotJugglerFound && (
<Button
size="xs"
colorScheme="green"
variant="outline"
onClick={handleLaunchPlotJuggler}
isLoading={actionLoading.launchPlotJuggler}
loadingText={t('status.launching')}
title="Launch PlotJuggler with UDP streamer configured"
>
🚀 {t('status.plotJuggler')}
</Button>
)}
{!plotJugglerFound && (
<Text fontSize="xs" color="gray.500" textAlign="center">
{t('status.notFound')}
</Text>
)}
</VStack>
</Box>
</Stat>
</CardBody>
</Card>
<Card size="sm">
<CardBody py={3}>
<Stat>
<StatLabel fontSize="sm">💾 {t('status.csvRecording')}</StatLabel>
<StatNumber fontSize="md" color={csvRecording ? 'green.500' : 'gray.500'}>
{csvRecording ? t('status.recording') : t('status.inactive')}
</StatNumber>
{status?.disk_space_info && (
<StatHelpText fontSize="xs">
💽 {status.disk_space_info.free_space} {t('status.free')}<br/>
~{status.disk_space_info.recording_time_left}
</StatHelpText>
)}
</Stat>
</CardBody>
</Card>
)}
<Card>
<CardBody>
<Stat>
<StatLabel>📡 {t('status.udpStreaming')}</StatLabel>
<StatNumber fontSize="lg" color={streaming ? 'green.500' : 'gray.500'}>
{streaming ? t('status.active') : t('status.inactive')}
</StatNumber>
<Box mt={2}>
<VStack spacing={2} align="stretch">
{streaming ? (
<Button
size="sm"
colorScheme="red"
variant="outline"
onClick={handleStopStreaming}
isLoading={actionLoading.stopStream}
loadingText={t('status.stopping')}
>
{t('status.stop')}
</Button>
) : (
<Button
size="sm"
colorScheme="blue"
variant="outline"
onClick={handleStartStreaming}
isLoading={actionLoading.startStream}
loadingText={t('status.starting')}
>
{t('status.start')}
</Button>
)}
{plotJugglerFound && (
<Button
size="sm"
colorScheme="green"
variant="outline"
onClick={handleLaunchPlotJuggler}
isLoading={actionLoading.launchPlotJuggler}
loadingText={t('status.launching')}
title="Launch PlotJuggler with UDP streamer configured"
>
🚀 {t('status.plotJuggler')}
</Button>
)}
{!plotJugglerFound && (
<Text fontSize="xs" color="gray.500" textAlign="center">
{t('status.notFound')}
</Text>
)}
</VStack>
</Box>
</Stat>
</CardBody>
</Card>
<Card>
<CardBody>
<Stat>
<StatLabel>💾 {t('status.csvRecording')}</StatLabel>
<StatNumber fontSize="lg" color={csvRecording ? 'green.500' : 'gray.500'}>
{csvRecording ? t('status.recording') : t('status.inactive')}
</StatNumber>
{status?.disk_space_info && (
<StatHelpText>
💽 {status.disk_space_info.free_space} {t('status.free')}<br/>
~{status.disk_space_info.recording_time_left}
</StatHelpText>
)}
</Stat>
</CardBody>
</Card>
<Card>
<CardBody>
<Stat>
<StatLabel>📊 Performance</StatLabel>
{connectionError ? (
<StatNumber fontSize="sm" color="red.500">
Backend disconnected
</StatNumber>
) : performanceLoading ? (
<Flex align="center" justify="center" py={2}>
<Spinner size="sm" mr={2} />
<Text fontSize="sm">Loading...</Text>
</Flex>
) : performanceData ? (
<>
<StatNumber fontSize="lg" color={
performanceData.points_lost > 0 ? 'red.500' :
performanceData.cpu_avg > 50 ? 'orange.500' : 'green.500'
}>
{performanceData.points_rate?.toFixed(1) || '0'} pts/s
</StatNumber>
<StatHelpText>
🧠 CPU: {performanceData.cpu_avg?.toFixed(1) || '0'}%<br/>
📦 Lost: {performanceData.points_lost || 0}<br/>
Errors: {(performanceData.read_errors || 0) + (performanceData.csv_errors || 0) + (performanceData.udp_errors || 0)}
</StatHelpText>
</>
) : (plcConnected && csvRecording) ? (
<StatNumber fontSize="sm" color="orange.500">
No data
</StatNumber>
) : (
<StatNumber fontSize="sm" color="gray.500">
Inactive
</StatNumber>
)}
</Stat>
</CardBody>
</Card>
<Card>
<CardBody>
<Stat>
<StatLabel>🔗 Backend & Tabs</StatLabel>
<StatNumber fontSize="lg" color={
connectionError ? 'red.500' :
isConnected ? 'green.500' : 'orange.500'
}>
{connectionError ? '❌ Backend offline' :
isLeader ? `👑 ${t('status.leader')}` : `👥 ${t('status.follower')}`}
</StatNumber>
<StatHelpText>
<Card size="sm">
<CardBody py={3}>
<Stat>
<StatLabel fontSize="sm">📊 Performance</StatLabel>
{connectionError ? (
<> Backend connection lost<br/>🔄 Retrying connection...</>
) : (
<StatNumber fontSize="sm" color="red.500">
Backend disconnected
</StatNumber>
) : performanceLoading ? (
<Flex align="center" justify="center" py={1}>
<Spinner size="sm" mr={2} />
<Text fontSize="xs">Loading...</Text>
</Flex>
) : performanceData ? (
<>
{isConnected ? `${t('status.connected_status')}` : `${t('status.connecting_status')}`}<br/>
{isLeader ? t('status.makingConnections') : t('status.receivingData')}
<StatNumber fontSize="md" color={
performanceData.points_lost > 0 ? 'red.500' :
performanceData.cpu_avg > 50 ? 'orange.500' : 'green.500'
}>
{performanceData.points_rate?.toFixed(1) || '0'} pts/s
</StatNumber>
<StatHelpText fontSize="xs">
🧠 CPU: {performanceData.cpu_avg?.toFixed(1) || '0'}%<br/>
📦 Lost: {performanceData.points_lost || 0}<br/>
Errors: {(performanceData.read_errors || 0) + (performanceData.csv_errors || 0) + (performanceData.udp_errors || 0)}
</StatHelpText>
</>
) : (plcConnected && csvRecording) ? (
<StatNumber fontSize="sm" color="orange.500">
No data
</StatNumber>
) : (
<StatNumber fontSize="sm" color="gray.500">
Inactive
</StatNumber>
)}
</StatHelpText>
</Stat>
</CardBody>
</Card>
</SimpleGrid>
</Stat>
</CardBody>
</Card>
</SimpleGrid>
</Box>
)
}

View File

@ -7,6 +7,6 @@
]
},
"auto_recovery_enabled": true,
"last_update": "2025-08-28T13:30:03.511773",
"last_update": "2025-08-28T15:37:08.750644",
"plotjuggler_path": "C:\\Program Files\\PlotJuggler\\plotjuggler.exe"
}