Update application event logging, refine PLC configuration, and enhance PlotManager functionality. Added multiple application start events, corrected PLC rack configuration, and introduced PlotRealtimeSession for improved real-time plotting capabilities.
This commit is contained in:
parent
bb275dd279
commit
748e8d5b0e
|
@ -1,181 +1,5 @@
|
|||
{
|
||||
"events": [
|
||||
{
|
||||
"timestamp": "2025-07-17T15:42:38.053687",
|
||||
"level": "info",
|
||||
"event_type": "csv_started",
|
||||
"message": "CSV recording started for 4 variables",
|
||||
"details": {
|
||||
"variables_count": 4,
|
||||
"output_directory": "records\\17-07-2025"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-07-17T15:42:38.055690",
|
||||
"level": "info",
|
||||
"event_type": "streaming_started",
|
||||
"message": "Streaming started with 4 variables",
|
||||
"details": {
|
||||
"variables_count": 4,
|
||||
"streaming_variables_count": 4,
|
||||
"sampling_interval": 0.1,
|
||||
"udp_host": "127.0.0.1",
|
||||
"udp_port": 9870
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-07-17T15:43:12.383366",
|
||||
"level": "info",
|
||||
"event_type": "variable_added",
|
||||
"message": "Variable added: test -> DB2124.14 (real)",
|
||||
"details": {
|
||||
"name": "test",
|
||||
"db": 2124,
|
||||
"offset": 14,
|
||||
"type": "real",
|
||||
"total_variables": 5
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-07-17T15:43:12.385360",
|
||||
"level": "info",
|
||||
"event_type": "csv_file_created",
|
||||
"message": "New CSV file created after variable modification: _15_43_12.csv",
|
||||
"details": {
|
||||
"file_path": "records\\17-07-2025\\_15_43_12.csv",
|
||||
"variables_count": 5,
|
||||
"reason": "variable_modification"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-07-17T15:43:12.407642",
|
||||
"level": "error",
|
||||
"event_type": "streaming_error",
|
||||
"message": "Error in streaming loop: dictionary changed size during iteration",
|
||||
"details": {
|
||||
"error": "dictionary changed size during iteration",
|
||||
"consecutive_errors": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-07-17T15:43:33.392876",
|
||||
"level": "error",
|
||||
"event_type": "streaming_error",
|
||||
"message": "Error in streaming loop: dictionary changed size during iteration",
|
||||
"details": {
|
||||
"error": "dictionary changed size during iteration",
|
||||
"consecutive_errors": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-07-17T15:43:33.394375",
|
||||
"level": "info",
|
||||
"event_type": "variable_removed",
|
||||
"message": "Variable removed: test",
|
||||
"details": {
|
||||
"name": "test",
|
||||
"removed_config": {
|
||||
"db": 2124,
|
||||
"offset": 14,
|
||||
"type": "real",
|
||||
"streaming": false
|
||||
},
|
||||
"total_variables": 4
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-07-17T15:43:33.397370",
|
||||
"level": "info",
|
||||
"event_type": "csv_file_created",
|
||||
"message": "New CSV file created after variable modification: _15_43_33.csv",
|
||||
"details": {
|
||||
"file_path": "records\\17-07-2025\\_15_43_33.csv",
|
||||
"variables_count": 4,
|
||||
"reason": "variable_modification"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-07-17T15:43:37.383086",
|
||||
"level": "info",
|
||||
"event_type": "config_change",
|
||||
"message": "UDP configuration updated: 127.0.0.1:9870",
|
||||
"details": {
|
||||
"old_config": {
|
||||
"host": "127.0.0.1",
|
||||
"port": 9870
|
||||
},
|
||||
"new_config": {
|
||||
"host": "127.0.0.1",
|
||||
"port": 9870
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-07-17T15:43:38.917840",
|
||||
"level": "info",
|
||||
"event_type": "config_change",
|
||||
"message": "PLC configuration updated: 10.1.33.11:0/2",
|
||||
"details": {
|
||||
"old_config": {
|
||||
"ip": "10.1.33.11",
|
||||
"rack": 0,
|
||||
"slot": 2
|
||||
},
|
||||
"new_config": {
|
||||
"ip": "10.1.33.11",
|
||||
"rack": 0,
|
||||
"slot": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-07-17T16:02:11.949781",
|
||||
"level": "info",
|
||||
"event_type": "Application started",
|
||||
"message": "Application initialization completed successfully",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-07-17T16:02:11.964986",
|
||||
"level": "info",
|
||||
"event_type": "plc_connection",
|
||||
"message": "Successfully connected to PLC 10.1.33.11",
|
||||
"details": {
|
||||
"ip": "10.1.33.11",
|
||||
"rack": 0,
|
||||
"slot": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-07-17T16:02:11.966271",
|
||||
"level": "info",
|
||||
"event_type": "csv_started",
|
||||
"message": "CSV recording started for 4 variables",
|
||||
"details": {
|
||||
"variables_count": 4,
|
||||
"output_directory": "records\\17-07-2025"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-07-17T16:02:11.967664",
|
||||
"level": "info",
|
||||
"event_type": "streaming_started",
|
||||
"message": "Streaming started with 4 variables",
|
||||
"details": {
|
||||
"variables_count": 4,
|
||||
"streaming_variables_count": 4,
|
||||
"sampling_interval": 0.1,
|
||||
"udp_host": "127.0.0.1",
|
||||
"udp_port": 9870
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-07-17T16:08:42.495109",
|
||||
"level": "info",
|
||||
"event_type": "Application started",
|
||||
"message": "Application initialization completed successfully",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-07-17T16:08:42.524816",
|
||||
"level": "info",
|
||||
|
@ -10470,8 +10294,131 @@
|
|||
"event_type": "application_started",
|
||||
"message": "Application initialization completed successfully",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T23:47:06.282724",
|
||||
"level": "info",
|
||||
"event_type": "application_started",
|
||||
"message": "Application initialization completed successfully",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T23:48:59.679823",
|
||||
"level": "info",
|
||||
"event_type": "application_started",
|
||||
"message": "Application initialization completed successfully",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T23:49:16.453495",
|
||||
"level": "error",
|
||||
"event_type": "csv_recording_error",
|
||||
"message": "Cannot start CSV recording: No datasets configured",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T23:49:16.464824",
|
||||
"level": "info",
|
||||
"event_type": "plc_connection",
|
||||
"message": "Successfully connected to PLC 10.1.33.11 (no datasets for recording)",
|
||||
"details": {
|
||||
"ip": "10.1.33.11",
|
||||
"rack": 1,
|
||||
"slot": 2,
|
||||
"auto_started_recording": false,
|
||||
"recording_datasets": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T23:51:02.704338",
|
||||
"level": "info",
|
||||
"event_type": "application_started",
|
||||
"message": "Application initialization completed successfully",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T23:51:21.839288",
|
||||
"level": "info",
|
||||
"event_type": "application_started",
|
||||
"message": "Application initialization completed successfully",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T23:54:25.455173",
|
||||
"level": "info",
|
||||
"event_type": "application_started",
|
||||
"message": "Application initialization completed successfully",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T23:55:05.790024",
|
||||
"level": "info",
|
||||
"event_type": "application_started",
|
||||
"message": "Application initialization completed successfully",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T23:56:14.341605",
|
||||
"level": "error",
|
||||
"event_type": "csv_recording_error",
|
||||
"message": "Cannot start CSV recording: No datasets configured",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T23:56:14.354938",
|
||||
"level": "info",
|
||||
"event_type": "plc_connection",
|
||||
"message": "Successfully connected to PLC 10.1.33.11 (no datasets for recording)",
|
||||
"details": {
|
||||
"ip": "10.1.33.11",
|
||||
"rack": 1,
|
||||
"slot": 2,
|
||||
"auto_started_recording": false,
|
||||
"recording_datasets": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T23:57:58.596445",
|
||||
"level": "info",
|
||||
"event_type": "application_started",
|
||||
"message": "Application initialization completed successfully",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-14T00:00:13.670046",
|
||||
"level": "info",
|
||||
"event_type": "application_started",
|
||||
"message": "Application initialization completed successfully",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-14T00:03:19.284260",
|
||||
"level": "info",
|
||||
"event_type": "application_started",
|
||||
"message": "Application initialization completed successfully",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-14T00:03:39.376430",
|
||||
"level": "error",
|
||||
"event_type": "csv_recording_error",
|
||||
"message": "Cannot start CSV recording: No datasets configured",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-14T00:03:39.391445",
|
||||
"level": "info",
|
||||
"event_type": "plc_connection",
|
||||
"message": "Successfully connected to PLC 10.1.33.11 (no datasets for recording)",
|
||||
"details": {
|
||||
"ip": "10.1.33.11",
|
||||
"rack": 0,
|
||||
"slot": 2,
|
||||
"auto_started_recording": false,
|
||||
"recording_datasets": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"last_updated": "2025-08-13T23:35:44.039580",
|
||||
"last_updated": "2025-08-14T00:03:39.391445",
|
||||
"total_entries": 1000
|
||||
}
|
|
@ -7,7 +7,7 @@
|
|||
},
|
||||
"plc_config": {
|
||||
"ip": "10.1.33.11",
|
||||
"rack": 1,
|
||||
"rack": 0,
|
||||
"slot": 2
|
||||
},
|
||||
"udp_config": {
|
||||
|
|
|
@ -56,18 +56,11 @@
|
|||
"type": "string"
|
||||
},
|
||||
"rack": {
|
||||
"default": 0,
|
||||
"description": "Rack of PLC",
|
||||
"maximum": 7,
|
||||
"minimum": 0,
|
||||
"title": "Rack",
|
||||
"type": "integer"
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"slot": {
|
||||
"default": 2,
|
||||
"description": "Normally 2",
|
||||
"maximum": 31,
|
||||
"minimum": 0,
|
||||
"title": "Slot",
|
||||
"type": "integer"
|
||||
}
|
||||
|
|
|
@ -23,13 +23,6 @@ import {
|
|||
TableContainer,
|
||||
Badge,
|
||||
IconButton,
|
||||
AlertDialog,
|
||||
AlertDialogBody,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogContent,
|
||||
AlertDialogOverlay,
|
||||
useDisclosure,
|
||||
Tabs,
|
||||
TabList,
|
||||
TabPanels,
|
||||
|
@ -41,6 +34,7 @@ import Form from '@rjsf/chakra-ui'
|
|||
import validator from '@rjsf/validator-ajv8'
|
||||
import allWidgets from './widgets/AllWidgets'
|
||||
import LayoutObjectFieldTemplate from './rjsf/LayoutObjectFieldTemplate'
|
||||
import PlotRealtimeSession from './PlotRealtimeSession'
|
||||
import * as api from '../services/api'
|
||||
|
||||
// Pure RJSF Plot Manager Component
|
||||
|
@ -52,14 +46,10 @@ export default function PlotManager() {
|
|||
const [plotsVariablesConfig, setPlotsVariablesConfig] = useState(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [actionLoading, setActionLoading] = useState({})
|
||||
const [selectedPlot, setSelectedPlot] = useState(null)
|
||||
|
||||
const toast = useToast()
|
||||
const cardBg = useColorModeValue('white', 'gray.700')
|
||||
const borderColor = useColorModeValue('gray.200', 'gray.600')
|
||||
|
||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||
const cancelRef = React.useRef()
|
||||
|
||||
const setActionState = (key, loading) => {
|
||||
setActionLoading(prev => ({ ...prev, [key]: loading }))
|
||||
|
@ -99,6 +89,80 @@ export default function PlotManager() {
|
|||
}
|
||||
}, [toast])
|
||||
|
||||
// Helper function to get plot definitions from config
|
||||
const getPlotDefinitions = () => {
|
||||
return plotsConfig?.plots || []
|
||||
}
|
||||
|
||||
// Helper function to get variables for a specific plot
|
||||
const getPlotVariables = (plotId) => {
|
||||
const plotVarsConfig = plotsVariablesConfig?.variables || []
|
||||
const plotVarEntry = plotVarsConfig.find(entry => entry.plot_id === plotId)
|
||||
return plotVarEntry?.variables || []
|
||||
}
|
||||
|
||||
// Handle plot configuration updates
|
||||
const handlePlotConfigUpdate = async (plotId, newConfig) => {
|
||||
try {
|
||||
// Update the plot definition in local state
|
||||
const updatedPlots = getPlotDefinitions().map(plot =>
|
||||
plot.id === plotId ? { ...plot, ...newConfig } : plot
|
||||
)
|
||||
|
||||
const updatedConfig = { ...plotsConfig, plots: updatedPlots }
|
||||
await savePlotsConfig(updatedConfig)
|
||||
|
||||
// Reload data to get fresh state
|
||||
await loadPlotData()
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to update plot configuration: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle plot removal
|
||||
const handlePlotRemove = async (plotId) => {
|
||||
try {
|
||||
// Remove from plot definitions
|
||||
const updatedPlots = getPlotDefinitions().filter(plot => plot.id !== plotId)
|
||||
const updatedPlotsConfig = { ...plotsConfig, plots: updatedPlots }
|
||||
|
||||
// Remove from plot variables
|
||||
const updatedPlotVars = (plotsVariablesConfig?.variables || []).filter(
|
||||
entry => entry.plot_id !== plotId
|
||||
)
|
||||
const updatedVarsConfig = { ...plotsVariablesConfig, variables: updatedPlotVars }
|
||||
|
||||
// Save both configurations
|
||||
await Promise.all([
|
||||
savePlotsConfig(updatedPlotsConfig),
|
||||
savePlotsVariablesConfig(updatedVarsConfig)
|
||||
])
|
||||
|
||||
// Stop the plot session in backend
|
||||
try {
|
||||
await api.controlPlotSession(plotId, 'stop')
|
||||
} catch (error) {
|
||||
// Plot session may not exist, that's OK
|
||||
}
|
||||
|
||||
// Reload data
|
||||
await loadPlotData()
|
||||
|
||||
toast({
|
||||
title: '✅ Plot removed successfully',
|
||||
status: 'success',
|
||||
duration: 2000
|
||||
})
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: '❌ Failed to remove plot',
|
||||
description: error.message,
|
||||
status: 'error',
|
||||
duration: 3000
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const savePlotsConfig = async (formData) => {
|
||||
try {
|
||||
setActionState('savePlots', true)
|
||||
|
@ -143,58 +207,6 @@ export default function PlotManager() {
|
|||
}
|
||||
}
|
||||
|
||||
const handlePlotControl = async (sessionId, action) => {
|
||||
try {
|
||||
setActionState(`${sessionId}_${action}`, true)
|
||||
const result = await api.controlPlot(sessionId, action)
|
||||
toast({
|
||||
title: `🎛️ Plot ${action}`,
|
||||
description: result.message || `Plot ${action} successful`,
|
||||
status: 'success',
|
||||
duration: 2000
|
||||
})
|
||||
// Refresh plots list to get updated status
|
||||
await loadPlotData()
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: `❌ Failed to ${action} plot`,
|
||||
description: error.message,
|
||||
status: 'error',
|
||||
duration: 3000
|
||||
})
|
||||
} finally {
|
||||
setActionState(`${sessionId}_${action}`, false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeletePlot = async (sessionId) => {
|
||||
try {
|
||||
setActionState(`${sessionId}_delete`, true)
|
||||
await api.deletePlot(sessionId)
|
||||
toast({
|
||||
title: '🗑️ Plot deleted',
|
||||
status: 'success',
|
||||
duration: 2000
|
||||
})
|
||||
await loadPlotData()
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: '❌ Failed to delete plot',
|
||||
description: error.message,
|
||||
status: 'error',
|
||||
duration: 3000
|
||||
})
|
||||
} finally {
|
||||
setActionState(`${sessionId}_delete`, false)
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
|
||||
const confirmDelete = (sessionId) => {
|
||||
setSelectedPlot(sessionId)
|
||||
onOpen()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadPlotData()
|
||||
}, [loadPlotData])
|
||||
|
@ -219,97 +231,31 @@ export default function PlotManager() {
|
|||
</Button>
|
||||
</Flex>
|
||||
|
||||
{/* Active Plots Overview */}
|
||||
{/* Active Plot Sessions with Real Chart.js Plots */}
|
||||
<Card bg={cardBg} borderColor={borderColor}>
|
||||
<CardHeader>
|
||||
<Heading size="md">🎛️ Active Plot Sessions</Heading>
|
||||
<Text fontSize="sm" color="gray.500" mt={1}>
|
||||
Real-time Chart.js plots with streaming data from PLC
|
||||
</Text>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
{Object.keys(plots).length === 0 ? (
|
||||
<Text color="gray.500" textAlign="center" py={4}>
|
||||
No active plot sessions
|
||||
{getPlotDefinitions().length === 0 ? (
|
||||
<Text color="gray.500" textAlign="center" py={8}>
|
||||
No plot sessions configured. Create plot definitions below to get started.
|
||||
</Text>
|
||||
) : (
|
||||
<TableContainer>
|
||||
<Table size="sm">
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>Session ID</Th>
|
||||
<Th>Name</Th>
|
||||
<Th>Status</Th>
|
||||
<Th>Variables</Th>
|
||||
<Th>Actions</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{Object.entries(plots).map(([sessionId, plot]) => (
|
||||
<Tr key={sessionId}>
|
||||
<Td>
|
||||
<Text fontSize="sm" fontFamily="mono">
|
||||
{sessionId}
|
||||
</Text>
|
||||
</Td>
|
||||
<Td>{plot.name || 'Unnamed Plot'}</Td>
|
||||
<Td>
|
||||
<Badge
|
||||
colorScheme={plot.running ? 'green' : 'gray'}
|
||||
>
|
||||
{plot.running ? 'Running' : 'Stopped'}
|
||||
</Badge>
|
||||
</Td>
|
||||
<Td>
|
||||
<Badge variant="outline">
|
||||
{plot.variable_count || 0} vars
|
||||
</Badge>
|
||||
</Td>
|
||||
<Td>
|
||||
<HStack spacing={1}>
|
||||
{plot.running ? (
|
||||
<Button
|
||||
size="xs"
|
||||
colorScheme="red"
|
||||
variant="outline"
|
||||
onClick={() => handlePlotControl(sessionId, 'stop')}
|
||||
isLoading={actionLoading[`${sessionId}_stop`]}
|
||||
>
|
||||
⏹️ Stop
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
size="xs"
|
||||
colorScheme="green"
|
||||
variant="outline"
|
||||
onClick={() => handlePlotControl(sessionId, 'start')}
|
||||
isLoading={actionLoading[`${sessionId}_start`]}
|
||||
>
|
||||
▶️ Start
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size="xs"
|
||||
colorScheme="blue"
|
||||
variant="outline"
|
||||
onClick={() => handlePlotControl(sessionId, 'clear')}
|
||||
isLoading={actionLoading[`${sessionId}_clear`]}
|
||||
>
|
||||
🧹 Clear
|
||||
</Button>
|
||||
<Button
|
||||
size="xs"
|
||||
colorScheme="red"
|
||||
variant="ghost"
|
||||
onClick={() => confirmDelete(sessionId)}
|
||||
isLoading={actionLoading[`${sessionId}_delete`]}
|
||||
>
|
||||
🗑️
|
||||
</Button>
|
||||
</HStack>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<VStack spacing={6} align="stretch">
|
||||
{getPlotDefinitions().map((plotDef) => (
|
||||
<PlotRealtimeSession
|
||||
key={plotDef.id}
|
||||
plotDefinition={plotDef}
|
||||
plotVariables={getPlotVariables(plotDef.id)}
|
||||
onConfigUpdate={handlePlotConfigUpdate}
|
||||
onRemove={handlePlotRemove}
|
||||
/>
|
||||
))}
|
||||
</VStack>
|
||||
)}
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
@ -403,40 +349,6 @@ export default function PlotManager() {
|
|||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
<AlertDialog
|
||||
isOpen={isOpen}
|
||||
leastDestructiveRef={cancelRef}
|
||||
onClose={onClose}
|
||||
>
|
||||
<AlertDialogOverlay>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader fontSize="lg" fontWeight="bold">
|
||||
Delete Plot Session
|
||||
</AlertDialogHeader>
|
||||
|
||||
<AlertDialogBody>
|
||||
Are you sure you want to delete plot session "{selectedPlot}"?
|
||||
This action cannot be undone.
|
||||
</AlertDialogBody>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<Button ref={cancelRef} onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
colorScheme="red"
|
||||
onClick={() => handleDeletePlot(selectedPlot)}
|
||||
ml={3}
|
||||
isLoading={actionLoading[`${selectedPlot}_delete`]}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialogOverlay>
|
||||
</AlertDialog>
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,423 @@
|
|||
import React, { useEffect, useRef, useState, useCallback } from 'react'
|
||||
import {
|
||||
Box,
|
||||
VStack,
|
||||
HStack,
|
||||
Text,
|
||||
Button,
|
||||
Card,
|
||||
CardBody,
|
||||
CardHeader,
|
||||
Heading,
|
||||
useColorModeValue,
|
||||
Badge,
|
||||
IconButton,
|
||||
Divider,
|
||||
Spacer,
|
||||
NumberInput,
|
||||
NumberInputField,
|
||||
NumberInputStepper,
|
||||
NumberIncrementStepper,
|
||||
NumberDecrementStepper,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Switch,
|
||||
Grid,
|
||||
GridItem,
|
||||
Flex,
|
||||
useToast
|
||||
} from '@chakra-ui/react'
|
||||
import { SettingsIcon } from '@chakra-ui/icons'
|
||||
import ChartjsPlot from './ChartjsPlot.jsx'
|
||||
import * as api from '../services/api'
|
||||
|
||||
/**
|
||||
* PlotRealtimeSession - Individual real-time Chart.js plot component
|
||||
* Mimics the functionality from the legacy plotting.js system
|
||||
*/
|
||||
export default function PlotRealtimeSession({
|
||||
plotDefinition,
|
||||
plotVariables = [],
|
||||
onRemove,
|
||||
onConfigUpdate
|
||||
}) {
|
||||
const [session, setSession] = useState({
|
||||
session_id: plotDefinition.id,
|
||||
name: plotDefinition.name,
|
||||
is_active: false,
|
||||
is_paused: false,
|
||||
variables_count: plotVariables.length
|
||||
})
|
||||
|
||||
const [showSettings, setShowSettings] = useState(false)
|
||||
const [localConfig, setLocalConfig] = useState({
|
||||
time_window: plotDefinition.time_window || 60,
|
||||
y_min: plotDefinition.y_min,
|
||||
y_max: plotDefinition.y_max,
|
||||
trigger_enabled: plotDefinition.trigger_enabled || false,
|
||||
trigger_variable: plotDefinition.trigger_variable,
|
||||
trigger_on_true: plotDefinition.trigger_on_true || true
|
||||
})
|
||||
|
||||
const chartControlsRef = useRef(null)
|
||||
const intervalRef = useRef(null)
|
||||
const toast = useToast()
|
||||
|
||||
const cardBg = useColorModeValue('white', 'gray.700')
|
||||
const borderColor = useColorModeValue('gray.200', 'gray.600')
|
||||
const muted = useColorModeValue('gray.600', 'gray.300')
|
||||
|
||||
// Enhanced session object for ChartjsPlot
|
||||
const enhancedSession = {
|
||||
...session,
|
||||
config: {
|
||||
...plotDefinition,
|
||||
...localConfig,
|
||||
variables: plotVariables
|
||||
},
|
||||
onChartReady: (controls) => {
|
||||
chartControlsRef.current = controls
|
||||
}
|
||||
}
|
||||
|
||||
// Load session status from backend (optional - session may not exist until started)
|
||||
const refreshSessionStatus = useCallback(async () => {
|
||||
try {
|
||||
const response = await api.getPlotSession(plotDefinition.id)
|
||||
if (response?.config) {
|
||||
setSession(prev => ({
|
||||
...prev,
|
||||
is_active: response.config.is_active || false,
|
||||
is_paused: response.config.is_paused || false
|
||||
}))
|
||||
}
|
||||
} catch (error) {
|
||||
// Session may not exist in backend yet
|
||||
if (error.message.includes('404')) {
|
||||
// Try to create the session automatically
|
||||
await createPlotSessionFromConfig()
|
||||
} else {
|
||||
// Backend not available - use local state silently
|
||||
// This allows the component to work even when backend is offline
|
||||
}
|
||||
}
|
||||
}, [plotDefinition.id])
|
||||
|
||||
// Create plot session in backend based on static configuration
|
||||
const createPlotSessionFromConfig = useCallback(async () => {
|
||||
try {
|
||||
// Convert plotVariables array to the format expected by the API
|
||||
const variableNames = plotVariables.map(v => v.variable_name)
|
||||
|
||||
const plotConfig = {
|
||||
session_id: plotDefinition.id,
|
||||
name: plotDefinition.name,
|
||||
variables: variableNames,
|
||||
time_window: plotDefinition.time_window || 60,
|
||||
trigger_enabled: plotDefinition.trigger_enabled || false,
|
||||
trigger_variable: plotDefinition.trigger_variable,
|
||||
trigger_on_true: plotDefinition.trigger_on_true || true,
|
||||
y_min: plotDefinition.y_min,
|
||||
y_max: plotDefinition.y_max
|
||||
}
|
||||
|
||||
// Create the plot session
|
||||
await api.createPlot(plotConfig)
|
||||
|
||||
console.log(`✅ Created plot session: ${plotDefinition.id}`)
|
||||
} catch (error) {
|
||||
console.warn(`Could not create plot session ${plotDefinition.id}:`, error)
|
||||
}
|
||||
}, [plotDefinition, plotVariables])
|
||||
|
||||
// Control plot session (start, pause, stop, clear)
|
||||
const handleControlClick = async (action) => {
|
||||
// Apply immediate local feedback
|
||||
if (chartControlsRef.current) {
|
||||
switch (action) {
|
||||
case 'pause':
|
||||
chartControlsRef.current.pauseStreaming()
|
||||
setSession(prev => ({ ...prev, is_paused: true }))
|
||||
break
|
||||
case 'start':
|
||||
case 'resume':
|
||||
chartControlsRef.current.resumeStreaming()
|
||||
setSession(prev => ({ ...prev, is_active: true, is_paused: false }))
|
||||
break
|
||||
case 'clear':
|
||||
chartControlsRef.current.clearChart()
|
||||
break
|
||||
case 'stop':
|
||||
chartControlsRef.current.pauseStreaming()
|
||||
setSession(prev => ({ ...prev, is_active: false, is_paused: false }))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Send command to backend
|
||||
try {
|
||||
// For 'start' action, create the plot session first if it doesn't exist
|
||||
if (action === 'start') {
|
||||
try {
|
||||
// Try to create the plot session with current configuration
|
||||
await api.createPlot({
|
||||
session_id: plotDefinition.id,
|
||||
name: plotDefinition.name,
|
||||
variables: plotVariables.map(v => v.variable_name), // Simplified format
|
||||
time_window: localConfig.time_window,
|
||||
trigger_enabled: localConfig.trigger_enabled,
|
||||
trigger_variable: localConfig.trigger_variable,
|
||||
trigger_on_true: localConfig.trigger_on_true,
|
||||
y_min: localConfig.y_min,
|
||||
y_max: localConfig.y_max
|
||||
})
|
||||
} catch (createError) {
|
||||
// Plot may already exist, that's OK
|
||||
console.log('Plot session may already exist:', createError.message)
|
||||
}
|
||||
}
|
||||
|
||||
await api.controlPlotSession(plotDefinition.id, action)
|
||||
// Refresh status after backend command
|
||||
setTimeout(refreshSessionStatus, 500)
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: `❌ Failed to ${action} plot`,
|
||||
description: error.message,
|
||||
status: 'error',
|
||||
duration: 3000
|
||||
})
|
||||
|
||||
// Revert local state on error
|
||||
await refreshSessionStatus()
|
||||
}
|
||||
}
|
||||
|
||||
// Apply configuration changes
|
||||
const applyConfigChanges = async () => {
|
||||
try {
|
||||
// Update backend configuration
|
||||
await onConfigUpdate?.(plotDefinition.id, localConfig)
|
||||
|
||||
// Apply changes to chart if possible
|
||||
if (chartControlsRef.current?.updateConfig) {
|
||||
chartControlsRef.current.updateConfig(localConfig)
|
||||
}
|
||||
|
||||
toast({
|
||||
title: '✅ Configuration updated',
|
||||
status: 'success',
|
||||
duration: 2000
|
||||
})
|
||||
|
||||
setShowSettings(false)
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: '❌ Failed to update configuration',
|
||||
description: error.message,
|
||||
status: 'error',
|
||||
duration: 3000
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const resetConfigChanges = () => {
|
||||
setLocalConfig({
|
||||
time_window: plotDefinition.time_window || 60,
|
||||
y_min: plotDefinition.y_min,
|
||||
y_max: plotDefinition.y_max,
|
||||
trigger_enabled: plotDefinition.trigger_enabled || false,
|
||||
trigger_variable: plotDefinition.trigger_variable,
|
||||
trigger_on_true: plotDefinition.trigger_on_true || true
|
||||
})
|
||||
setShowSettings(false)
|
||||
}
|
||||
|
||||
// Auto-refresh session status
|
||||
useEffect(() => {
|
||||
// Try to get session status first, if it fails, create the session
|
||||
refreshSessionStatus()
|
||||
intervalRef.current = setInterval(refreshSessionStatus, 5000)
|
||||
return () => {
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current)
|
||||
}
|
||||
}
|
||||
}, [refreshSessionStatus])
|
||||
|
||||
return (
|
||||
<Card bg={cardBg} borderColor={borderColor} shadow="md">
|
||||
<CardHeader pb={2}>
|
||||
<HStack align="center">
|
||||
<Box>
|
||||
<Heading size="sm">📈 {plotDefinition.name || plotDefinition.id}</Heading>
|
||||
<Text fontSize="sm" color={muted} mt={1}>
|
||||
Variables: {plotVariables.length} |
|
||||
Status: <strong>
|
||||
{session.is_active
|
||||
? (session.is_paused ? 'Paused' : 'Active')
|
||||
: 'Stopped'
|
||||
}
|
||||
</strong>
|
||||
{localConfig.trigger_enabled && (
|
||||
<> | Trigger: {localConfig.trigger_variable}</>
|
||||
)}
|
||||
</Text>
|
||||
</Box>
|
||||
<Spacer />
|
||||
<HStack>
|
||||
<IconButton
|
||||
icon={<SettingsIcon />}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
aria-label="Settings"
|
||||
onClick={() => setShowSettings(!showSettings)}
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
colorScheme="red"
|
||||
variant="ghost"
|
||||
onClick={() => onRemove?.(plotDefinition.id)}
|
||||
>
|
||||
❌
|
||||
</Button>
|
||||
</HStack>
|
||||
</HStack>
|
||||
</CardHeader>
|
||||
|
||||
<CardBody pt={0}>
|
||||
{/* Settings Panel */}
|
||||
{showSettings && (
|
||||
<Box mb={4} p={4} bg={useColorModeValue('gray.50', 'gray.600')} borderRadius="md">
|
||||
<Grid templateColumns="repeat(auto-fit, minmax(200px, 1fr))" gap={4}>
|
||||
<GridItem>
|
||||
<FormControl>
|
||||
<FormLabel fontSize="sm">Time Window (seconds)</FormLabel>
|
||||
<NumberInput
|
||||
value={localConfig.time_window}
|
||||
onChange={(valueString) => setLocalConfig(prev => ({
|
||||
...prev,
|
||||
time_window: parseInt(valueString) || 60
|
||||
}))}
|
||||
min={10}
|
||||
max={3600}
|
||||
size="sm"
|
||||
>
|
||||
<NumberInputField />
|
||||
<NumberInputStepper>
|
||||
<NumberIncrementStepper />
|
||||
<NumberDecrementStepper />
|
||||
</NumberInputStepper>
|
||||
</NumberInput>
|
||||
</FormControl>
|
||||
</GridItem>
|
||||
|
||||
<GridItem>
|
||||
<FormControl>
|
||||
<FormLabel fontSize="sm">Y Min (auto if empty)</FormLabel>
|
||||
<NumberInput
|
||||
value={localConfig.y_min || ''}
|
||||
onChange={(valueString) => setLocalConfig(prev => ({
|
||||
...prev,
|
||||
y_min: valueString === '' ? null : parseFloat(valueString)
|
||||
}))}
|
||||
size="sm"
|
||||
>
|
||||
<NumberInputField placeholder="Auto" />
|
||||
<NumberInputStepper>
|
||||
<NumberIncrementStepper />
|
||||
<NumberDecrementStepper />
|
||||
</NumberInputStepper>
|
||||
</NumberInput>
|
||||
</FormControl>
|
||||
</GridItem>
|
||||
|
||||
<GridItem>
|
||||
<FormControl>
|
||||
<FormLabel fontSize="sm">Y Max (auto if empty)</FormLabel>
|
||||
<NumberInput
|
||||
value={localConfig.y_max || ''}
|
||||
onChange={(valueString) => setLocalConfig(prev => ({
|
||||
...prev,
|
||||
y_max: valueString === '' ? null : parseFloat(valueString)
|
||||
}))}
|
||||
size="sm"
|
||||
>
|
||||
<NumberInputField placeholder="Auto" />
|
||||
<NumberInputStepper>
|
||||
<NumberIncrementStepper />
|
||||
<NumberDecrementStepper />
|
||||
</NumberInputStepper>
|
||||
</NumberInput>
|
||||
</FormControl>
|
||||
</GridItem>
|
||||
|
||||
<GridItem>
|
||||
<FormControl>
|
||||
<FormLabel fontSize="sm">Enable Trigger</FormLabel>
|
||||
<Switch
|
||||
isChecked={localConfig.trigger_enabled}
|
||||
onChange={(e) => setLocalConfig(prev => ({
|
||||
...prev,
|
||||
trigger_enabled: e.target.checked
|
||||
}))}
|
||||
size="sm"
|
||||
/>
|
||||
</FormControl>
|
||||
</GridItem>
|
||||
</Grid>
|
||||
|
||||
<Flex mt={4} gap={2}>
|
||||
<Button size="sm" colorScheme="blue" onClick={applyConfigChanges}>
|
||||
💾 Apply
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={resetConfigChanges}>
|
||||
↩️ Cancel
|
||||
</Button>
|
||||
</Flex>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Chart.js Plot */}
|
||||
<Box mb={4}>
|
||||
<ChartjsPlot session={enhancedSession} height="320px" />
|
||||
</Box>
|
||||
|
||||
{/* Control Buttons */}
|
||||
<HStack spacing={2} justify="center">
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => handleControlClick('start')}
|
||||
colorScheme="green"
|
||||
isDisabled={session.is_active && !session.is_paused}
|
||||
>
|
||||
▶️ Start
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => handleControlClick('pause')}
|
||||
colorScheme="yellow"
|
||||
isDisabled={!session.is_active || session.is_paused}
|
||||
>
|
||||
⏸️ Pause
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => handleControlClick('clear')}
|
||||
variant="outline"
|
||||
>
|
||||
🗑️ Clear
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => handleControlClick('stop')}
|
||||
colorScheme="red"
|
||||
isDisabled={!session.is_active}
|
||||
>
|
||||
⏹️ Stop
|
||||
</Button>
|
||||
</HStack>
|
||||
</CardBody>
|
||||
</Card>
|
||||
)
|
||||
}
|
|
@ -49,7 +49,7 @@ export const UpDownWidget = ({ id, placeholder, required, readonly, disabled, la
|
|||
{label && <FormLabel htmlFor={id}>{label}</FormLabel>}
|
||||
<NumberInput
|
||||
id={id}
|
||||
value={value || ''}
|
||||
value={value ?? ''}
|
||||
onChange={(_, num) => onChange(isNaN(num) ? undefined : num)}
|
||||
onBlur={onBlur && (() => onBlur(id, value))}
|
||||
onFocus={onFocus && (() => onFocus(id, value))}
|
||||
|
|
|
@ -193,4 +193,15 @@ export async function getPlotVariables() {
|
|||
return toJsonOrThrow(res)
|
||||
}
|
||||
|
||||
// Plot session status and control (aliases for existing functions)
|
||||
export async function getPlotSession(sessionId) {
|
||||
// Use existing getPlotConfig to get session info
|
||||
return await getPlotConfig(sessionId)
|
||||
}
|
||||
|
||||
export async function controlPlotSession(sessionId, action) {
|
||||
// Use existing controlPlot function
|
||||
return await controlPlot(sessionId, action)
|
||||
}
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue