Refactor VariableSelectorWidget for improved data handling and loading states

- Enhanced handleRefresh to process dataset variables more efficiently.
- Updated useEffect hooks to manage initial loading and refresh triggers separately.
- Removed auto-refresh on focus to streamline user experience.
- Adjusted loading state display logic to only show during initial load.
- Improved StatusBar component layout and responsiveness with flex properties.
- Ensured consistent minimum heights for card elements to enhance UI stability.
- Simplified conditional rendering for performance data and error states.
This commit is contained in:
Miguel 2025-08-29 20:51:53 +02:00
parent 4d41b7b9b3
commit 5f73f77618
3 changed files with 2959 additions and 2697 deletions

File diff suppressed because it is too large Load Diff

View File

@ -74,22 +74,71 @@ export function VariableSelectorWidget(props) {
}
}, [])
// Manual refresh function
// Manual refresh function - doesn't use loading state when we have existing data
const handleRefresh = useCallback(async () => {
setRefreshing(true)
await loadDatasetVariables()
setRefreshing(false)
}, [loadDatasetVariables])
try {
const response = await readExpandedDatasetVariables()
const datasetVariablesArray = response?.variables || []
// Convert array format to object format for easier processing
const datasetVariablesObj = {}
datasetVariablesArray.forEach(item => {
if (item.dataset_id && item.variables) {
// Handle both array format and object format for variables
let variablesObj = item.variables
if (Array.isArray(item.variables)) {
// Convert array to object format indexed by variable name
variablesObj = {}
item.variables.forEach(variable => {
if (variable.name) {
variablesObj[variable.name] = variable
}
})
}
datasetVariablesObj[item.dataset_id] = {
variables: variablesObj
}
}
})
setDatasetVariables(datasetVariablesObj)
} catch (error) {
console.error('Error refreshing dataset variables:', error)
} finally {
setRefreshing(false)
}
}, [])
// Load dataset variables on mount and when refresh trigger changes
useEffect(() => {
// Small delay to ensure context is properly initialized
const timer = setTimeout(() => {
loadDatasetVariables()
}, 100)
return () => clearTimeout(timer)
}, [loadDatasetVariables, refreshTrigger, contextTrigger])
// Only do initial load if we don't have any variables yet
if (Object.keys(datasetVariables).length === 0) {
// Small delay to ensure context is properly initialized
const timer = setTimeout(() => {
loadDatasetVariables()
}, 100)
return () => clearTimeout(timer)
}
}, [loadDatasetVariables]) // Removed refresh triggers to prevent constant reloading
// Separate effect for manual refresh triggers
useEffect(() => {
// Only refresh if we have existing data and there was an explicit refresh request
if (Object.keys(datasetVariables).length > 0 && (refreshTrigger > 0 || contextTrigger > 0)) {
const lastTrigger = Math.max(refreshTrigger, contextTrigger)
const currentTime = Date.now()
const lastRefreshTime = window._lastVariableWidgetRefresh || 0
// Debounce refreshes to avoid excessive loading
if (currentTime - lastRefreshTime > 5000) { // 5 second minimum between refreshes
window._lastVariableWidgetRefresh = currentTime
handleRefresh()
}
}
}, [refreshTrigger, contextTrigger, datasetVariables, handleRefresh])
// Auto-refresh when component gains focus (optional behavior)
const handleFocusWithRefresh = useCallback((event) => {
@ -98,14 +147,9 @@ export function VariableSelectorWidget(props) {
onFocus(id, event.target.value)
}
// Auto-refresh data on focus (debounced to avoid excessive calls)
const now = Date.now()
const lastRefresh = window._lastVariableRefresh || 0
if (now - lastRefresh > 30000) { // Refresh at most once every 30 seconds
window._lastVariableRefresh = now
handleRefresh()
}
}, [onFocus, id, handleRefresh])
// Skip auto-refresh on focus to prevent loading states during normal usage
// Users can manually refresh using the refresh button if needed
}, [onFocus, id])
// Create flattened list of all variables with their metadata
const allVariables = useMemo(() => {
@ -273,7 +317,8 @@ export function VariableSelectorWidget(props) {
'usint': 'green'
}
if (loading) {
// Only show loading for initial load, not for refreshes when we already have variables
if (loading && Object.keys(datasetVariables).length === 0) {
return (
<FormControl isRequired={required} isDisabled={disabled} isReadOnly={readonly}>
{label && <FormLabel htmlFor={id}>{label}</FormLabel>}

View File

@ -966,18 +966,18 @@ function StatusBar({ status, isConnected, isLeader, connectionError }) {
</Flex>
<SimpleGrid columns={{ base: 1, sm: 2, md: 3, lg: 4, xl: 5 }} spacing={2} mb={6}>
<Card size="sm">
<CardBody py={3}>
<Card size="sm" minHeight="140px">
<CardBody py={3} display="flex" flexDirection="column" justifyContent="space-between">
<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>
)}
<StatHelpText fontSize="xs" minHeight="32px">
{status?.plc_reconnection?.enabled && (
<>🔄 {t('status.autoReconnection')}: {status?.plc_reconnection?.active ? t('status.reconnecting') : t('status.enabled')}</>
)}
</StatHelpText>
<Box mt={2}>
{plcConnected ? (
<Button
@ -1009,34 +1009,36 @@ function StatusBar({ status, isConnected, isLeader, connectionError }) {
{/* CPU Status Card - Shows when PLC is connected */}
{plcConnected && cpuStatus && (
<Card size="sm">
<CardBody py={3}>
<Card size="sm" minHeight="140px">
<CardBody py={3} display="flex" flexDirection="column" justifyContent="space-between">
<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>
)}
<StatHelpText fontSize="xs" minHeight="32px">
{cpuStatus.cycle_time_ms !== undefined && (
<>
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}>
<Card size="sm" minHeight="140px">
<CardBody py={3} display="flex" flexDirection="column" justifyContent="space-between">
<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}>
<Box mt={2} flex="1" display="flex" flexDirection="column" justifyContent="end">
<VStack spacing={1} align="stretch">
{streaming ? (
<Button
@ -1061,7 +1063,7 @@ function StatusBar({ status, isConnected, isLeader, connectionError }) {
{t('status.start')}
</Button>
)}
{plotJugglerFound && (
{plotJugglerFound ? (
<Button
size="xs"
colorScheme="green"
@ -1073,9 +1075,8 @@ function StatusBar({ status, isConnected, isLeader, connectionError }) {
>
🚀 {t('status.plotJuggler')}
</Button>
)}
{!plotJugglerFound && (
<Text fontSize="xs" color="gray.500" textAlign="center">
) : (
<Text fontSize="xs" color="gray.500" textAlign="center" minHeight="24px">
{t('status.notFound')}
</Text>
)}
@ -1085,59 +1086,65 @@ function StatusBar({ status, isConnected, isLeader, connectionError }) {
</CardBody>
</Card>
<Card size="sm">
<CardBody py={3}>
<Card size="sm" minHeight="140px">
<CardBody py={3} display="flex" flexDirection="column" justifyContent="space-between">
<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>
)}
<StatHelpText fontSize="xs" minHeight="32px">
{status?.disk_space_info && (
<>
💽 {status.disk_space_info.free_space} {t('status.free')}<br />
~{status.disk_space_info.recording_time_left}
</>
)}
</StatHelpText>
</Stat>
</CardBody>
</Card>
<Card size="sm">
<CardBody py={3}>
<Card size="sm" minHeight="140px">
<CardBody py={3} display="flex" flexDirection="column" justifyContent="space-between">
<Stat>
<StatLabel fontSize="sm">📊 Performance</StatLabel>
{connectionError ? (
<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 ? (
<>
<Box minHeight="40px" display="flex" flexDirection="column" justifyContent="center">
{connectionError ? (
<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 ? (
<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">
) : (plcConnected && csvRecording) ? (
<StatNumber fontSize="sm" color="orange.500">
No data
</StatNumber>
) : (
<StatNumber fontSize="sm" color="gray.500">
Inactive
</StatNumber>
)}
</Box>
<StatHelpText fontSize="xs" minHeight="48px">
{performanceData && (
<>
🧠 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>