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:
parent
4d41b7b9b3
commit
5f73f77618
File diff suppressed because it is too large
Load Diff
|
@ -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>}
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue