Refactor PlotManager and PlotRealtimeSession components for improved variable handling and session control
- Updated CollapsiblePlotChart to display correct variable count. - Added getPlotVariables helper function to retrieve plot variables based on plot ID. - Enhanced PlotManager to log plot data loading and adjust dependencies for useEffect. - Refactored handleControlClick in PlotRealtimeSession to use useCallback and maintain session state during refresh. - Implemented logic to restart active plots after configuration refresh in PlotRealtimeSession. - Removed unused DashboardNew component to streamline codebase. - Updated system_state.json to reflect changes in active datasets and last update timestamp. - Deleted validate_schema.py as it is no longer needed for schema validation.
This commit is contained in:
parent
f5db758698
commit
a9396ec309
|
@ -1,129 +0,0 @@
|
|||
# 🏭 PLC S7-31x Streamer & Logger - Pure RJSF Dashboard
|
||||
|
||||
## 🚀 Mejoras Implementadas
|
||||
|
||||
### ✅ Dashboard Completamente Nuevo con RJSF Puro
|
||||
- **Archivo:** `frontend/src/pages/DashboardNew.jsx`
|
||||
- Implementado con React JSON Schema Form (RJSF) y tema Chakra UI
|
||||
- Elimina todos los wrappers innecesarios y usa widgets puros y extensibles
|
||||
- Todas las configuraciones se manejan directamente con esquemas JSON
|
||||
|
||||
### 🔧 StatusBar Mejorado con Control Robusto
|
||||
- **Problema resuelto:** Botones de conexión/desconexión ahora funcionan correctamente
|
||||
- Agregados estados de carga individuales para cada acción
|
||||
- Manejo adecuado de errores con toast notifications
|
||||
- Actualización automática del estado después de las acciones
|
||||
|
||||
### 📊 Dataset Manager con RJSF Puro
|
||||
- Formularios completamente basados en esquemas JSON
|
||||
- Edición directa de configuraciones sin wrappers
|
||||
- Dos pestañas: Dataset Definitions y Dataset Variables
|
||||
- Validación automática mediante esquemas JSON
|
||||
|
||||
### 📈 Plot Manager Completamente Funcional
|
||||
- **Archivo:** `frontend/src/components/PlotManager.jsx`
|
||||
- Control completo de sesiones de plotting (start/stop/clear/delete)
|
||||
- **Problema resuelto:** Botones de charts ahora funcionan correctamente
|
||||
- Configuración de plots mediante RJSF puro con esquemas
|
||||
- Vista de sesiones activas con controles en tiempo real
|
||||
|
||||
### 🔌 APIs de Plotting Añadidas
|
||||
- **Archivo:** `frontend/src/services/api.js`
|
||||
- Funciones completas para manejo de plots:
|
||||
- `getPlots()`, `createPlot()`, `deletePlot()`
|
||||
- `controlPlot()` (start/stop/clear)
|
||||
- `getPlotData()`, `getPlotConfig()`, `updatePlotConfig()`
|
||||
- `getPlotVariables()`
|
||||
|
||||
### 🎯 Arquitectura RJSF Pura
|
||||
- **Sin wrappers innecesarios:** Solo widgets extensibles y reutilizables
|
||||
- **Esquemas JSON:** Toda la configuración basada en `/config/schema/`
|
||||
- **UI Schemas:** Layout y configuración visual mediante esquemas UI
|
||||
- **Validación:** Automática con `@rjsf/validator-ajv8`
|
||||
|
||||
### 🗑️ Sistema Legacy Preparado para Eliminación
|
||||
- **Archivo de notas:** `main_cleanup_notes.py`
|
||||
- Identificadas todas las rutas legacy a eliminar
|
||||
- APIs esenciales documentadas para mantener
|
||||
- `templates/index.html` puede ser eliminado
|
||||
- JavaScript legacy en `/static/js/` puede ser eliminado
|
||||
|
||||
## 🔄 Uso de la Aplicación
|
||||
|
||||
### 1. StatusBar (Control Principal)
|
||||
```jsx
|
||||
// Conexión PLC con estado de carga
|
||||
<Button onClick={handleConnectPlc} isLoading={loading}>
|
||||
🔗 Connect
|
||||
</Button>
|
||||
|
||||
// UDP Streaming con feedback
|
||||
<Button onClick={handleStartStreaming} isLoading={loading}>
|
||||
▶️ Start
|
||||
</Button>
|
||||
```
|
||||
|
||||
### 2. Configuración RJSF
|
||||
```jsx
|
||||
// Formularios puros sin wrappers
|
||||
<Form
|
||||
schema={schema}
|
||||
formData={formData}
|
||||
validator={validator}
|
||||
onSubmit={({ formData }) => saveConfig(formData)}
|
||||
/>
|
||||
```
|
||||
|
||||
### 3. Control de Plots
|
||||
```jsx
|
||||
// Botones funcionales para charts
|
||||
<Button onClick={() => controlPlot(sessionId, 'start')}>
|
||||
▶️ Start
|
||||
</Button>
|
||||
<Button onClick={() => controlPlot(sessionId, 'stop')}>
|
||||
⏹️ Stop
|
||||
</Button>
|
||||
```
|
||||
|
||||
## 📁 Estructura de Archivos Actualizada
|
||||
|
||||
```
|
||||
frontend/src/
|
||||
├── pages/
|
||||
│ ├── Dashboard.jsx (old - puede ser eliminado)
|
||||
│ └── DashboardNew.jsx ⭐ (nuevo, RJSF puro)
|
||||
├── components/
|
||||
│ ├── DatasetManager.jsx (mejorado con RJSF puro)
|
||||
│ └── PlotManager.jsx ⭐ (nuevo, control completo de charts)
|
||||
└── services/
|
||||
└── api.js (APIs de plotting añadidas)
|
||||
```
|
||||
|
||||
## 🚦 Rutas de la Aplicación
|
||||
|
||||
- **`/`** → React SPA principal
|
||||
- **`/app`** → Dashboard principal
|
||||
- **`/app/*`** → Rutas internas de React
|
||||
|
||||
## 🎨 Tema Chakra UI Completo
|
||||
|
||||
- Todos los componentes usan tema Chakra UI consistente
|
||||
- Color modes (light/dark) funcionales
|
||||
- Cards, Buttons, Tables con estilos uniformes
|
||||
- Toast notifications para feedback
|
||||
|
||||
## ✅ Problemas Resueltos
|
||||
|
||||
1. **✅ Botón connect/disconnect:** Ahora funciona correctamente con estados de carga
|
||||
2. **✅ Edición JSON:** RJSF puro permite edición completa de configuraciones
|
||||
3. **✅ Botones de charts:** Plot Manager implementado con controles funcionales
|
||||
4. **✅ RJSF puro:** Sin wrappers, solo widgets extensibles
|
||||
5. **✅ Layout y esquemas:** Configuración dual (schema + uiSchema) implementada
|
||||
|
||||
## 🚀 Listo para Producción
|
||||
|
||||
- Build exitoso: ✅
|
||||
- RJSF + Chakra UI: ✅
|
||||
- APIs funcionionales: ✅
|
||||
- Sistema legacy preparado para eliminación: ✅
|
||||
- Dashboard completamente funcional: ✅
|
|
@ -1,115 +0,0 @@
|
|||
# UI Schema Layout Support Enhancement Summary
|
||||
|
||||
## Changes Made
|
||||
|
||||
### 1. Enhanced `DashboardNew.jsx`
|
||||
- **Added imports**: `LayoutObjectFieldTemplate` and comprehensive widget collection
|
||||
- **Updated Form components**: All RJSF Form components now use:
|
||||
- `widgets={allWidgets}` - Comprehensive widget collection
|
||||
- `templates={{ ObjectFieldTemplate: LayoutObjectFieldTemplate }}` - Layout support
|
||||
- **Enhanced Configuration Panel**: Now supports full UI schema features
|
||||
- **Updated Dataset Manager**: Both definitions and variables forms support layouts
|
||||
- **Added comprehensive documentation**: Detailed comments explaining UI schema features
|
||||
|
||||
### 2. Created `AllWidgets.jsx`
|
||||
- **Comprehensive widget collection**: Merges all available widgets
|
||||
- **Widget aliases**: Support for different naming conventions in UI schemas
|
||||
- **Custom widget integration**: Includes VariableSelectorWidget and PLC widgets
|
||||
- **Backward compatibility**: Ensures existing UI schemas continue to work
|
||||
|
||||
### 3. Enhanced `CustomWidgets.jsx`
|
||||
- **Added widget aliases**: `variableSelector` for UI schema compatibility
|
||||
- **Maintained existing functionality**: VariableSelectorWidget continues to work
|
||||
|
||||
### 4. Updated `PlotManager.jsx`
|
||||
- **Enhanced Form components**: Added layout template and comprehensive widgets
|
||||
- **Consistent widget usage**: Uses same widget collection as DashboardNew
|
||||
|
||||
### 5. Created Demo Files
|
||||
- **`layout-demo.schema.json`**: Example schema for testing layouts
|
||||
- **`layout-demo.uischema.json`**: Comprehensive UI schema example
|
||||
|
||||
## UI Schema Features Now Supported
|
||||
|
||||
### Layout Management
|
||||
```json
|
||||
{
|
||||
"ui:layout": [
|
||||
[
|
||||
{ "name": "field1", "width": 6 },
|
||||
{ "name": "field2", "width": 6 }
|
||||
],
|
||||
[
|
||||
{ "name": "field3", "width": 12 }
|
||||
]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Widget Types
|
||||
- `updown` - Number input with +/- buttons
|
||||
- `checkbox` - Boolean checkbox
|
||||
- `text` - Text input
|
||||
- `textarea` - Multi-line text
|
||||
- `select` - Dropdown selection
|
||||
- `VariableSelectorWidget` - Custom PLC variable selector
|
||||
|
||||
### Field Properties
|
||||
- `ui:help` - Help text for fields
|
||||
- `ui:placeholder` - Placeholder text
|
||||
- `ui:readonly` - Read-only fields
|
||||
- `ui:order` - Field ordering
|
||||
- `ui:column` - Column width (1-12 grid)
|
||||
|
||||
### Examples from Existing Schemas
|
||||
|
||||
#### PLC Configuration with Layout
|
||||
```json
|
||||
{
|
||||
"plc_config": {
|
||||
"ui:layout": [
|
||||
[
|
||||
{ "name": "ip", "width": 6 },
|
||||
{ "name": "rack", "width": 3 },
|
||||
{ "name": "slot", "width": 3 }
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Dataset Definitions with Responsive Layout
|
||||
```json
|
||||
{
|
||||
"datasets": {
|
||||
"ui:layout": [
|
||||
[
|
||||
{ "name": "name", "width": 3 },
|
||||
{ "name": "prefix", "width": 3 },
|
||||
{ "name": "sampling_interval", "width": 3 },
|
||||
{ "name": "enabled", "width": 3 }
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Responsive Design**: 12-column grid system adapts to different screen sizes
|
||||
2. **Better UX**: Logical field grouping and intuitive layouts
|
||||
3. **Consistent Styling**: All forms use the same Chakra UI components
|
||||
4. **Extensible**: Easy to add new widgets and layout patterns
|
||||
5. **Backward Compatible**: Existing configurations continue to work
|
||||
6. **Documentation**: Clear examples and comprehensive comments
|
||||
|
||||
## Testing
|
||||
|
||||
The enhanced UI schema support can be tested by:
|
||||
1. Loading any existing configuration (PLC, datasets, plots)
|
||||
2. Observing the improved layout with proper field grouping
|
||||
3. Testing different screen sizes for responsive behavior
|
||||
4. Adding new configurations with custom layouts
|
||||
5. Using the demo schema files for comprehensive testing
|
||||
|
||||
All existing functionality is preserved while adding powerful new layout capabilities.
|
|
@ -825,8 +825,94 @@
|
|||
"event_type": "csv_cleanup_failed",
|
||||
"message": "CSV cleanup failed: 'max_hours'",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-14T15:00:00.054694",
|
||||
"level": "error",
|
||||
"event_type": "csv_cleanup_failed",
|
||||
"message": "CSV cleanup failed: 'max_hours'",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-14T15:03:55.324074",
|
||||
"level": "info",
|
||||
"event_type": "application_started",
|
||||
"message": "Application initialization completed successfully",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-14T15:03:55.388267",
|
||||
"level": "info",
|
||||
"event_type": "dataset_activated",
|
||||
"message": "Dataset activated: DAR",
|
||||
"details": {
|
||||
"dataset_id": "DAR",
|
||||
"variables_count": 2,
|
||||
"streaming_count": 2,
|
||||
"prefix": "gateway_phoenix"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-14T15:03:55.390258",
|
||||
"level": "info",
|
||||
"event_type": "csv_recording_started",
|
||||
"message": "CSV recording started: 1 datasets activated",
|
||||
"details": {
|
||||
"activated_datasets": 1,
|
||||
"total_datasets": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-14T15:03:55.393257",
|
||||
"level": "info",
|
||||
"event_type": "udp_streaming_started",
|
||||
"message": "UDP streaming to PlotJuggler started",
|
||||
"details": {
|
||||
"udp_host": "127.0.0.1",
|
||||
"udp_port": 9870,
|
||||
"datasets_available": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-14T15:03:55.436491",
|
||||
"level": "error",
|
||||
"event_type": "csv_cleanup_failed",
|
||||
"message": "CSV cleanup failed: 'max_hours'",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-14T15:04:08.406394",
|
||||
"level": "info",
|
||||
"event_type": "plot_session_created",
|
||||
"message": "Plot session 'UR29' created and started",
|
||||
"details": {
|
||||
"session_id": "plot_1",
|
||||
"variables": [
|
||||
"UR29_Brix",
|
||||
"UR29_ma"
|
||||
],
|
||||
"time_window": 20,
|
||||
"trigger_variable": null,
|
||||
"auto_started": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-14T15:04:12.217187",
|
||||
"level": "info",
|
||||
"event_type": "plot_session_created",
|
||||
"message": "Plot session 'UR29' created and started",
|
||||
"details": {
|
||||
"session_id": "plot_1",
|
||||
"variables": [
|
||||
"UR29_Brix",
|
||||
"UR29_ma"
|
||||
],
|
||||
"time_window": 20,
|
||||
"trigger_variable": null,
|
||||
"auto_started": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"last_updated": "2025-08-14T14:47:13.244962",
|
||||
"total_entries": 82
|
||||
"last_updated": "2025-08-14T15:04:12.217187",
|
||||
"total_entries": 90
|
||||
}
|
|
@ -1,611 +0,0 @@
|
|||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
CardBody,
|
||||
CardHeader,
|
||||
Button,
|
||||
Text,
|
||||
Grid,
|
||||
Flex,
|
||||
Spacer,
|
||||
HStack,
|
||||
VStack,
|
||||
useColorModeValue,
|
||||
useToast,
|
||||
Heading,
|
||||
Table,
|
||||
Thead,
|
||||
Tbody,
|
||||
Tr,
|
||||
Th,
|
||||
Td,
|
||||
TableContainer,
|
||||
Badge,
|
||||
IconButton,
|
||||
Tabs,
|
||||
TabList,
|
||||
TabPanels,
|
||||
Tab,
|
||||
TabPanel,
|
||||
Divider,
|
||||
Select,
|
||||
Accordion,
|
||||
AccordionItem,
|
||||
AccordionButton,
|
||||
AccordionPanel,
|
||||
AccordionIcon,
|
||||
Collapse,
|
||||
useDisclosure,
|
||||
Spinner
|
||||
} from '@chakra-ui/react'
|
||||
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 { useVariableContext } from '../contexts/VariableContext'
|
||||
import * as api from '../services/api'
|
||||
|
||||
// Collapsible Form Component for Plot Definitions
|
||||
function CollapsiblePlotForm({ data, schema, uiSchema, onSave, title, icon, getItemLabel }) {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [formData, setFormData] = useState(data)
|
||||
|
||||
useEffect(() => {
|
||||
setFormData(data)
|
||||
}, [data])
|
||||
|
||||
if (!schema || !formData) {
|
||||
return (
|
||||
<Card>
|
||||
<CardBody>
|
||||
<Flex align="center" justify="center" py={4}>
|
||||
<Spinner size="sm" mr={2} />
|
||||
<Text>Loading {title}...</Text>
|
||||
</Flex>
|
||||
</CardBody>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
const items = formData[Object.keys(formData)[0]] || []
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Flex align="center" justify="space-between">
|
||||
<Box>
|
||||
<Heading size="md">{icon} {title}</Heading>
|
||||
<Text fontSize="sm" color="gray.500" mt={1}>
|
||||
{items.length} item{items.length !== 1 ? 's' : ''} configured
|
||||
</Text>
|
||||
</Box>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
rightIcon={<AccordionIcon transform={isOpen ? 'rotate(180deg)' : 'rotate(0deg)'} />}
|
||||
>
|
||||
{isOpen ? 'Collapse' : 'Expand'} Form
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
{/* Show summary when collapsed */}
|
||||
{!isOpen && items.length > 0 && (
|
||||
<Box mt={3}>
|
||||
<Text fontSize="sm" fontWeight="semibold" mb={2}>Quick Overview:</Text>
|
||||
<HStack spacing={2} flexWrap="wrap">
|
||||
{items.slice(0, 5).map((item, index) => (
|
||||
<Badge key={index} colorScheme="green" variant="subtle">
|
||||
{getItemLabel ? getItemLabel(item) : (item.name || item.id || `Item ${index + 1}`)}
|
||||
</Badge>
|
||||
))}
|
||||
{items.length > 5 && (
|
||||
<Badge colorScheme="gray" variant="subtle">
|
||||
+{items.length - 5} more...
|
||||
</Badge>
|
||||
)}
|
||||
</HStack>
|
||||
</Box>
|
||||
)}
|
||||
</CardHeader>
|
||||
|
||||
<Collapse in={isOpen}>
|
||||
<CardBody pt={0}>
|
||||
<Form
|
||||
schema={schema}
|
||||
uiSchema={uiSchema}
|
||||
formData={formData}
|
||||
validator={validator}
|
||||
widgets={allWidgets}
|
||||
templates={{ ObjectFieldTemplate: LayoutObjectFieldTemplate }}
|
||||
onSubmit={({ formData }) => onSave(formData)}
|
||||
onChange={({ formData }) => setFormData(formData)}
|
||||
>
|
||||
<HStack spacing={2} mt={4}>
|
||||
<Button type="submit" colorScheme="blue">
|
||||
💾 Save {title}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => setFormData(data)}>
|
||||
🔄 Reset
|
||||
</Button>
|
||||
</HStack>
|
||||
</Form>
|
||||
</CardBody>
|
||||
</Collapse>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
// Pure RJSF Plot Manager Component
|
||||
export default function PlotManager() {
|
||||
const { triggerVariableRefresh } = useVariableContext()
|
||||
const [plots, setPlots] = useState({})
|
||||
const [plotsSchemaData, setPlotsSchemaData] = useState(null)
|
||||
const [plotsVariablesSchemaData, setPlotsVariablesSchemaData] = useState(null)
|
||||
const [plotsConfig, setPlotsConfig] = useState(null)
|
||||
const [plotsVariablesConfig, setPlotsVariablesConfig] = useState(null)
|
||||
const [selectedPlotId, setSelectedPlotId] = useState('')
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [actionLoading, setActionLoading] = useState({})
|
||||
|
||||
const toast = useToast()
|
||||
const cardBg = useColorModeValue('white', 'gray.700')
|
||||
const borderColor = useColorModeValue('gray.200', 'gray.600')
|
||||
|
||||
const setActionState = (key, loading) => {
|
||||
setActionLoading(prev => ({ ...prev, [key]: loading }))
|
||||
}
|
||||
|
||||
const loadPlotData = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const [
|
||||
plotsData,
|
||||
plotsSchemaResponse,
|
||||
plotsVariablesSchemaResponse,
|
||||
plotsConfigData,
|
||||
plotsVariablesConfigData
|
||||
] = await Promise.all([
|
||||
api.getPlots(),
|
||||
api.getSchema('plot-definitions'),
|
||||
api.getSchema('plot-variables'),
|
||||
api.readConfig('plot-definitions'),
|
||||
api.readConfig('plot-variables')
|
||||
])
|
||||
|
||||
setPlots(plotsData?.plots || {})
|
||||
setPlotsSchemaData(plotsSchemaResponse)
|
||||
setPlotsVariablesSchemaData(plotsVariablesSchemaResponse)
|
||||
setPlotsConfig(plotsConfigData)
|
||||
setPlotsVariablesConfig(plotsVariablesConfigData)
|
||||
|
||||
// Auto-select first plot if none selected
|
||||
if (!selectedPlotId && plotsConfigData?.plots?.length > 0) {
|
||||
setSelectedPlotId(plotsConfigData.plots[0].id)
|
||||
}
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: '❌ Failed to load plot data',
|
||||
description: error.message,
|
||||
status: 'error',
|
||||
duration: 3000
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [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 || []
|
||||
}
|
||||
|
||||
// Type 3 Pattern Helper Functions
|
||||
// Get filtered variables for selected plot
|
||||
const getSelectedPlotVariables = () => {
|
||||
if (!plotsVariablesConfig?.variables || !selectedPlotId) {
|
||||
return { variables: [] }
|
||||
}
|
||||
|
||||
const plotVars = plotsVariablesConfig.variables.find(v => v.plot_id === selectedPlotId)
|
||||
return plotVars || { variables: [] }
|
||||
}
|
||||
|
||||
// Update variables for selected plot
|
||||
const updateSelectedPlotVariables = (newVariableData) => {
|
||||
if (!plotsVariablesConfig?.variables || !selectedPlotId) return
|
||||
|
||||
const updatedVariables = plotsVariablesConfig.variables.map(v =>
|
||||
v.plot_id === selectedPlotId
|
||||
? { ...v, ...newVariableData }
|
||||
: v
|
||||
)
|
||||
|
||||
// If plot not found, add new entry
|
||||
if (!plotsVariablesConfig.variables.find(v => v.plot_id === selectedPlotId)) {
|
||||
updatedVariables.push({
|
||||
plot_id: selectedPlotId,
|
||||
...newVariableData
|
||||
})
|
||||
}
|
||||
|
||||
const updatedConfig = { ...plotsVariablesConfig, variables: updatedVariables }
|
||||
setPlotsVariablesConfig(updatedConfig)
|
||||
}
|
||||
|
||||
// Available plots for combo selector
|
||||
const availablePlots = plotsConfig?.plots || []
|
||||
|
||||
// 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)
|
||||
await api.writeConfig('plot-definitions', formData)
|
||||
toast({
|
||||
title: '✅ Plot definitions saved',
|
||||
status: 'success',
|
||||
duration: 2000
|
||||
})
|
||||
setPlotsConfig(formData)
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: '❌ Failed to save plot definitions',
|
||||
description: error.message,
|
||||
status: 'error',
|
||||
duration: 3000
|
||||
})
|
||||
} finally {
|
||||
setActionState('savePlots', false)
|
||||
}
|
||||
}
|
||||
|
||||
const savePlotsVariablesConfig = async (formData) => {
|
||||
try {
|
||||
setActionState('savePlotsVariables', true)
|
||||
await api.writeConfig('plot-variables', formData)
|
||||
toast({
|
||||
title: '✅ Plot variables saved',
|
||||
status: 'success',
|
||||
duration: 2000
|
||||
})
|
||||
setPlotsVariablesConfig(formData)
|
||||
// Trigger refresh of variable selectors (though they don't depend on plot vars directly)
|
||||
triggerVariableRefresh()
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: '❌ Failed to save plot variables',
|
||||
description: error.message,
|
||||
status: 'error',
|
||||
duration: 3000
|
||||
})
|
||||
} finally {
|
||||
setActionState('savePlotsVariables', false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadPlotData()
|
||||
}, [loadPlotData])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Card bg={cardBg}>
|
||||
<CardBody>
|
||||
<Text>Loading plot configurations...</Text>
|
||||
</CardBody>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<VStack spacing={4} align="stretch">
|
||||
<Flex align="center">
|
||||
<Heading size="lg">📈 Plot Manager</Heading>
|
||||
<Spacer />
|
||||
<Button size="sm" variant="outline" onClick={loadPlotData}>
|
||||
🔄 Refresh
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
{/* 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>
|
||||
{getPlotDefinitions().length === 0 ? (
|
||||
<Text color="gray.500" textAlign="center" py={8}>
|
||||
No plot sessions configured. Create plot definitions below to get started.
|
||||
</Text>
|
||||
) : (
|
||||
<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>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* RJSF Configuration Forms */}
|
||||
<VStack spacing={4} align="stretch">
|
||||
{/* Plot Definitions - Collapsible */}
|
||||
<CollapsiblePlotForm
|
||||
data={plotsConfig}
|
||||
schema={plotsSchemaData?.schema}
|
||||
uiSchema={plotsSchemaData?.uiSchema}
|
||||
onSave={savePlotsConfig}
|
||||
title="Plot Definitions"
|
||||
icon="📋"
|
||||
getItemLabel={(item) => `${item.name || item.id} (${item.time_window || 60}s)`}
|
||||
/>
|
||||
|
||||
{/* Plot Variables Configuration */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Heading size="md">⚙️ Plot Variables Configuration</Heading>
|
||||
<Text fontSize="sm" color="gray.500" mt={1}>
|
||||
Select a plot session, then configure its variables and visual settings
|
||||
</Text>
|
||||
</CardHeader>
|
||||
</Text>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
{/* Step 1: Plot Selector (Combo) */}
|
||||
<VStack spacing={4} align="stretch">
|
||||
<Box>
|
||||
<Text fontSize="sm" fontWeight="bold" mb={2}>
|
||||
🎯 Select Plot Session
|
||||
</Text>
|
||||
<Select
|
||||
value={selectedPlotId}
|
||||
onChange={(e) => setSelectedPlotId(e.target.value)}
|
||||
placeholder="Choose a plot session to configure..."
|
||||
size="md"
|
||||
>
|
||||
{availablePlots.map(plot => (
|
||||
<option key={plot.id} value={plot.id}>
|
||||
📈 {plot.name} ({plot.id})
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
{availablePlots.length === 0 && (
|
||||
<Text fontSize="sm" color="orange.500" mt={2}>
|
||||
⚠️ No plot sessions available. Configure plot definitions first in the "Plot Definitions" tab.
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Variables Configuration Form */}
|
||||
{selectedPlotId && (
|
||||
<Box>
|
||||
<Divider mb={4} />
|
||||
<Text fontSize="sm" fontWeight="bold" mb={2}>
|
||||
⚙️ Configure Variables for Plot "{selectedPlotId}"
|
||||
</Text>
|
||||
|
||||
{/* Simplified schema for selected plot variables */}
|
||||
{(() => {
|
||||
const selectedPlotVars = getSelectedPlotVariables()
|
||||
|
||||
// Schema for this plot's variables
|
||||
const singlePlotSchema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
variables: {
|
||||
type: "array",
|
||||
title: "Variables",
|
||||
description: `Variables to display in plot ${selectedPlotId}`,
|
||||
items: {
|
||||
type: "object",
|
||||
title: "Plot Variable",
|
||||
properties: {
|
||||
variable_name: {
|
||||
type: "string",
|
||||
title: "Variable Name",
|
||||
description: "Select variable from datasets with search and metadata"
|
||||
},
|
||||
label: {
|
||||
type: "string",
|
||||
title: "Display Label",
|
||||
description: "Label shown in the plot legend"
|
||||
},
|
||||
color: {
|
||||
type: "string",
|
||||
title: "Line Color",
|
||||
default: "#3182CE"
|
||||
},
|
||||
line_width: {
|
||||
type: "number",
|
||||
title: "Line Width",
|
||||
default: 2,
|
||||
minimum: 1,
|
||||
maximum: 10
|
||||
},
|
||||
y_axis: {
|
||||
type: "string",
|
||||
title: "Y-Axis",
|
||||
enum: ["left", "right"],
|
||||
default: "left"
|
||||
}
|
||||
},
|
||||
required: ["variable_name", "label"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const singlePlotUiSchema = {
|
||||
variables: {
|
||||
items: {
|
||||
"ui:layout": [[
|
||||
{ "name": "variable_name", "width": 4 },
|
||||
{ "name": "label", "width": 2 },
|
||||
{ "name": "color", "width": 2 },
|
||||
{ "name": "line_width", "width": 2 },
|
||||
{ "name": "y_axis", "width": 2 }
|
||||
]],
|
||||
variable_name: {
|
||||
"ui:widget": "variableSelector",
|
||||
"ui:placeholder": "Search and select variable from datasets...",
|
||||
"ui:help": "🔍 Search variables from configured datasets with live values and metadata"
|
||||
},
|
||||
label: {
|
||||
"ui:placeholder": "Chart legend label..."
|
||||
},
|
||||
color: {
|
||||
"ui:widget": "color"
|
||||
},
|
||||
line_width: {
|
||||
"ui:widget": "updown"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Form
|
||||
schema={singlePlotSchema}
|
||||
uiSchema={singlePlotUiSchema}
|
||||
formData={selectedPlotVars}
|
||||
validator={validator}
|
||||
widgets={allWidgets}
|
||||
templates={{ ObjectFieldTemplate: LayoutObjectFieldTemplate }}
|
||||
onSubmit={({ formData }) => {
|
||||
updateSelectedPlotVariables(formData)
|
||||
// Create updated config and save it
|
||||
const updatedVariables = plotsVariablesConfig.variables?.map(v =>
|
||||
v.plot_id === selectedPlotId
|
||||
? { ...v, ...formData }
|
||||
: v
|
||||
) || []
|
||||
|
||||
// If plot not found, add new entry
|
||||
if (!plotsVariablesConfig.variables?.find(v => v.plot_id === selectedPlotId)) {
|
||||
updatedVariables.push({
|
||||
plot_id: selectedPlotId,
|
||||
...formData
|
||||
})
|
||||
}
|
||||
|
||||
const updatedConfig = { ...plotsVariablesConfig, variables: updatedVariables }
|
||||
savePlotsVariablesConfig(updatedConfig)
|
||||
}}
|
||||
onChange={({ formData }) => updateSelectedPlotVariables(formData)}
|
||||
>
|
||||
<HStack spacing={2} mt={4}>
|
||||
<Button
|
||||
type="submit"
|
||||
colorScheme="blue"
|
||||
isLoading={actionLoading.savePlotsVariables}
|
||||
loadingText="Saving..."
|
||||
>
|
||||
💾 Save Variables for {selectedPlotId}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={loadPlotData}>
|
||||
🔄 Reset
|
||||
</Button>
|
||||
</HStack>
|
||||
</Form>
|
||||
)
|
||||
})()}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{!selectedPlotId && availablePlots.length > 0 && (
|
||||
<Box textAlign="center" py={8}>
|
||||
<Text color="gray.500">
|
||||
👆 Select a plot session above to configure its variables
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</VStack>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</VStack>
|
||||
)
|
||||
}
|
|
@ -207,7 +207,7 @@ function CollapsiblePlotChart({ plotDefinition, plotVariables, onConfigUpdate, o
|
|||
<Box>
|
||||
<Heading size="sm">📈 {plotDefinition.name || plotDefinition.id}</Heading>
|
||||
<Text fontSize="xs" color="gray.500">
|
||||
{plotDefinition.time_window}s window • {plotVariables?.variables?.length || 0} variables
|
||||
{plotDefinition.time_window}s window • {plotVariables?.length || 0} variables
|
||||
</Text>
|
||||
</Box>
|
||||
<HStack>
|
||||
|
@ -271,7 +271,11 @@ export default function PlotManager() {
|
|||
if (!selectedPlotId && plotsData?.plots?.length > 0) {
|
||||
setSelectedPlotId(plotsData.plots[0].id)
|
||||
}
|
||||
setPlotsSchemaData(plotsSchemaResponse)
|
||||
|
||||
console.log('✅ Plot data loaded:', {
|
||||
plots: plotsData?.plots?.length || 0,
|
||||
plotVariables: plotVariablesData?.variables?.length || 0
|
||||
})
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: '❌ Failed to load plot configurations',
|
||||
|
@ -282,7 +286,7 @@ export default function PlotManager() {
|
|||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [toast])
|
||||
}, [selectedPlotId, toast])
|
||||
|
||||
const savePlotsConfig = async (formData) => {
|
||||
try {
|
||||
|
@ -335,6 +339,17 @@ export default function PlotManager() {
|
|||
return ['DAR', 'Fast', 'Slow'] // TODO: Get from actual dataset definitions
|
||||
}
|
||||
|
||||
// Helper to get plot variables for any plot ID
|
||||
const getPlotVariables = useCallback((plotId) => {
|
||||
if (!plotId || !plotVariablesConfig?.variables) return []
|
||||
|
||||
const plotVars = plotVariablesConfig.variables.find(
|
||||
item => item.plot_id === plotId
|
||||
)
|
||||
// Return the variables array directly, not the wrapper object
|
||||
return plotVars?.variables || []
|
||||
}, [plotVariablesConfig])
|
||||
|
||||
// Helper functions for Type 3 form pattern (Plot Variables)
|
||||
const getSelectedPlotVariables = () => {
|
||||
if (!selectedPlotId || !plotVariablesConfig?.variables) return { variables: [] }
|
||||
|
@ -420,8 +435,8 @@ export default function PlotManager() {
|
|||
<CollapsiblePlotChart
|
||||
key={plotDef.id}
|
||||
plotDefinition={plotDef}
|
||||
plotVariables={{ variables: [] }} // Simplified for now
|
||||
onConfigUpdate={() => {}}
|
||||
plotVariables={getPlotVariables(plotDef.id)}
|
||||
onConfigUpdate={loadPlotData}
|
||||
onRemove={() => {}}
|
||||
/>
|
||||
))}
|
||||
|
|
|
@ -154,7 +154,7 @@ export default function PlotRealtimeSession({
|
|||
}, [plotDefinition, plotVariables])
|
||||
|
||||
// Control plot session (start, pause, stop, clear)
|
||||
const handleControlClick = async (action) => {
|
||||
const handleControlClick = useCallback(async (action) => {
|
||||
// Send command to backend first
|
||||
try {
|
||||
// For 'start' action, create the plot session first if it doesn't exist
|
||||
|
@ -228,7 +228,7 @@ export default function PlotRealtimeSession({
|
|||
// Revert local state on error
|
||||
await refreshSessionStatus()
|
||||
}
|
||||
}
|
||||
}, [plotDefinition, plotVariables, localConfig, refreshSessionStatus, toast])
|
||||
|
||||
// Apply configuration changes
|
||||
const applyConfigChanges = async () => {
|
||||
|
@ -278,6 +278,11 @@ export default function PlotRealtimeSession({
|
|||
// Refresh plot configuration and recreate chart
|
||||
const refreshPlotConfiguration = useCallback(async () => {
|
||||
setIsRefreshing(true)
|
||||
|
||||
// Remember the current active state before refresh
|
||||
const wasActive = session.is_active
|
||||
const wasPaused = session.is_paused
|
||||
|
||||
try {
|
||||
console.log(`🔄 Refreshing configuration for plot ${plotDefinition.id}...`)
|
||||
|
||||
|
@ -289,6 +294,31 @@ export default function PlotRealtimeSession({
|
|||
// Also refresh session status
|
||||
await refreshSessionStatus()
|
||||
|
||||
// If the plot was active before refresh, try to restart it
|
||||
if (wasActive && !wasPaused) {
|
||||
console.log(`🔄 Plot was active before refresh, attempting to restart...`)
|
||||
try {
|
||||
// Wait a bit to ensure the session status has been updated
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
|
||||
// Try to restart the plot session
|
||||
await handleControlClick('start')
|
||||
|
||||
console.log(`✅ Plot ${plotDefinition.id} restarted after refresh`)
|
||||
} catch (restartError) {
|
||||
console.warn(`⚠️ Could not restart plot ${plotDefinition.id} after refresh:`, restartError)
|
||||
toast({
|
||||
title: '⚠️ Plot was stopped during refresh',
|
||||
description: 'Please click Start to resume plotting',
|
||||
status: 'warning',
|
||||
duration: 4000
|
||||
})
|
||||
}
|
||||
} else if (wasActive && wasPaused) {
|
||||
console.log(`🔄 Plot was paused before refresh, maintaining paused state`)
|
||||
// The session should maintain its paused state
|
||||
}
|
||||
|
||||
toast({
|
||||
title: '🔄 Configuration refreshed',
|
||||
description: 'Plot configuration and variables have been updated',
|
||||
|
@ -308,7 +338,7 @@ export default function PlotRealtimeSession({
|
|||
} finally {
|
||||
setIsRefreshing(false)
|
||||
}
|
||||
}, [plotDefinition.id, refreshSessionStatus, toast])
|
||||
}, [plotDefinition.id, refreshSessionStatus, handleControlClick, session.is_active, session.is_paused, toast])
|
||||
|
||||
// Auto-refresh session status
|
||||
useEffect(() => {
|
||||
|
|
|
@ -1,876 +0,0 @@
|
|||
import React, { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import {
|
||||
Box,
|
||||
Container,
|
||||
VStack,
|
||||
Heading,
|
||||
Card,
|
||||
CardBody,
|
||||
CardHeader,
|
||||
Button,
|
||||
Text,
|
||||
Flex,
|
||||
Spacer,
|
||||
HStack,
|
||||
useColorModeValue,
|
||||
useToast,
|
||||
Alert,
|
||||
AlertIcon,
|
||||
Spinner,
|
||||
Badge,
|
||||
SimpleGrid,
|
||||
Stat,
|
||||
StatLabel,
|
||||
StatNumber,
|
||||
StatHelpText,
|
||||
Divider,
|
||||
Table,
|
||||
Thead,
|
||||
Tbody,
|
||||
Tr,
|
||||
Th,
|
||||
Td,
|
||||
TableContainer,
|
||||
Code,
|
||||
Select
|
||||
} from '@chakra-ui/react'
|
||||
import Form from '@rjsf/chakra-ui'
|
||||
import validator from '@rjsf/validator-ajv8'
|
||||
import PlotManager from '../components/PlotManager'
|
||||
import allWidgets from '../components/widgets/AllWidgets'
|
||||
import LayoutObjectFieldTemplate from '../components/rjsf/LayoutObjectFieldTemplate'
|
||||
import { VariableProvider, useVariableContext } from '../contexts/VariableContext'
|
||||
import * as api from '../services/api'
|
||||
|
||||
// StatusBar Component - Real-time PLC status with action buttons
|
||||
function StatusBar({ status, onRefresh }) {
|
||||
const plcConnected = !!status?.plc_connected
|
||||
const streaming = !!status?.streaming
|
||||
const csvRecording = !!status?.csv_recording
|
||||
const [actionLoading, setActionLoading] = useState({})
|
||||
const toast = useToast()
|
||||
|
||||
const setLoading = (action, loading) => {
|
||||
setActionLoading(prev => ({ ...prev, [action]: loading }))
|
||||
}
|
||||
|
||||
const handleConnectPlc = async () => {
|
||||
setLoading('connect', true)
|
||||
try {
|
||||
const result = await api.connectPlc()
|
||||
toast({
|
||||
title: '🔗 PLC Connection',
|
||||
description: result.message || 'Connection initiated',
|
||||
status: 'info',
|
||||
duration: 2000
|
||||
})
|
||||
setTimeout(() => onRefresh?.(), 1000)
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: '❌ Failed to connect PLC',
|
||||
description: error.message,
|
||||
status: 'error',
|
||||
duration: 3000
|
||||
})
|
||||
} finally {
|
||||
setLoading('connect', false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDisconnectPlc = async () => {
|
||||
setLoading('disconnect', true)
|
||||
try {
|
||||
const result = await api.disconnectPlc()
|
||||
toast({
|
||||
title: '❌ PLC Disconnection',
|
||||
description: result.message || 'Disconnection initiated',
|
||||
status: 'info',
|
||||
duration: 2000
|
||||
})
|
||||
setTimeout(() => onRefresh?.(), 1000)
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: '❌ Failed to disconnect PLC',
|
||||
description: error.message,
|
||||
status: 'error',
|
||||
duration: 3000
|
||||
})
|
||||
} finally {
|
||||
setLoading('disconnect', false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleStartStreaming = async () => {
|
||||
setLoading('startStream', true)
|
||||
try {
|
||||
const result = await api.startUdpStreaming()
|
||||
toast({
|
||||
title: '📡 UDP Streaming started',
|
||||
description: result.message || 'Streaming initiated',
|
||||
status: 'success',
|
||||
duration: 2000
|
||||
})
|
||||
setTimeout(() => onRefresh?.(), 1000)
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: '❌ Failed to start streaming',
|
||||
description: error.message,
|
||||
status: 'error',
|
||||
duration: 3000
|
||||
})
|
||||
} finally {
|
||||
setLoading('startStream', false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleStopStreaming = async () => {
|
||||
setLoading('stopStream', true)
|
||||
try {
|
||||
const result = await api.stopUdpStreaming()
|
||||
toast({
|
||||
title: '⏹️ UDP Streaming stopped',
|
||||
description: result.message || 'Streaming stopped',
|
||||
status: 'info',
|
||||
duration: 2000
|
||||
})
|
||||
setTimeout(() => onRefresh?.(), 1000)
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: '❌ Failed to stop streaming',
|
||||
description: error.message,
|
||||
status: 'error',
|
||||
duration: 3000
|
||||
})
|
||||
} finally {
|
||||
setLoading('stopStream', false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<SimpleGrid columns={{ base: 1, md: 3 }} spacing={4} mb={6}>
|
||||
<Card>
|
||||
<CardBody>
|
||||
<Stat>
|
||||
<StatLabel>🔌 PLC Connection</StatLabel>
|
||||
<StatNumber fontSize="lg" color={plcConnected ? 'green.500' : 'red.500'}>
|
||||
{plcConnected ? 'Connected' : 'Disconnected'}
|
||||
</StatNumber>
|
||||
{status?.plc_reconnection?.enabled && (
|
||||
<StatHelpText>
|
||||
🔄 Auto-reconnection: {status?.plc_reconnection?.active ? 'reconnecting…' : 'enabled'}
|
||||
</StatHelpText>
|
||||
)}
|
||||
<Box mt={2}>
|
||||
{plcConnected ? (
|
||||
<Button
|
||||
size="sm"
|
||||
colorScheme="red"
|
||||
variant="outline"
|
||||
onClick={handleDisconnectPlc}
|
||||
isLoading={actionLoading.disconnect}
|
||||
loadingText="Disconnecting..."
|
||||
>
|
||||
❌ Disconnect
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
size="sm"
|
||||
colorScheme="blue"
|
||||
variant="outline"
|
||||
onClick={handleConnectPlc}
|
||||
isLoading={actionLoading.connect}
|
||||
loadingText="Connecting..."
|
||||
>
|
||||
🔗 Connect
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</Stat>
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardBody>
|
||||
<Stat>
|
||||
<StatLabel>📡 UDP Streaming</StatLabel>
|
||||
<StatNumber fontSize="lg" color={streaming ? 'green.500' : 'gray.500'}>
|
||||
{streaming ? 'Active' : 'Inactive'}
|
||||
</StatNumber>
|
||||
<Box mt={2}>
|
||||
{streaming ? (
|
||||
<Button
|
||||
size="sm"
|
||||
colorScheme="red"
|
||||
variant="outline"
|
||||
onClick={handleStopStreaming}
|
||||
isLoading={actionLoading.stopStream}
|
||||
loadingText="Stopping..."
|
||||
>
|
||||
⏹️ Stop
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
size="sm"
|
||||
colorScheme="blue"
|
||||
variant="outline"
|
||||
onClick={handleStartStreaming}
|
||||
isLoading={actionLoading.startStream}
|
||||
loadingText="Starting..."
|
||||
>
|
||||
▶️ Start
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</Stat>
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardBody>
|
||||
<Stat>
|
||||
<StatLabel>💾 CSV Recording</StatLabel>
|
||||
<StatNumber fontSize="lg" color={csvRecording ? 'green.500' : 'gray.500'}>
|
||||
{csvRecording ? 'Recording' : 'Inactive'}
|
||||
</StatNumber>
|
||||
{status?.disk_space_info && (
|
||||
<StatHelpText>
|
||||
💽 {status.disk_space_info.free_space} free<br/>
|
||||
⏱️ ~{status.disk_space_info.recording_time_left}
|
||||
</StatHelpText>
|
||||
)}
|
||||
</Stat>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</SimpleGrid>
|
||||
)
|
||||
}
|
||||
|
||||
// PLC Configuration Panel - Fixed to PLC & UDP settings only
|
||||
function ConfigurationPanel({ schemaData, formData, onFormChange, onSave, saving, message }) {
|
||||
const cardBg = useColorModeValue('white', 'gray.700')
|
||||
const borderColor = useColorModeValue('gray.200', 'gray.600')
|
||||
|
||||
if (!schemaData?.schema || !formData) {
|
||||
return (
|
||||
<Card bg={cardBg} borderColor={borderColor}>
|
||||
<CardBody>
|
||||
<Text>Loading PLC configuration...</Text>
|
||||
</CardBody>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card bg={cardBg} borderColor={borderColor}>
|
||||
<CardHeader>
|
||||
<Box>
|
||||
<Heading size="md">🔧 PLC & UDP Configuration</Heading>
|
||||
<Text fontSize="sm" color="gray.500" mt={1}>
|
||||
Configure PLC connection settings and UDP streaming parameters
|
||||
</Text>
|
||||
</Box>
|
||||
{message && (
|
||||
<Alert status="success" mt={2}>
|
||||
<AlertIcon />
|
||||
{message}
|
||||
</Alert>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Form
|
||||
schema={schemaData.schema}
|
||||
uiSchema={schemaData.uiSchema}
|
||||
formData={formData}
|
||||
validator={validator}
|
||||
widgets={allWidgets}
|
||||
templates={{ ObjectFieldTemplate: LayoutObjectFieldTemplate }}
|
||||
onChange={({ formData }) => onFormChange(formData)}
|
||||
onSubmit={({ formData }) => onSave(formData)}
|
||||
>
|
||||
<HStack spacing={2} mt={4}>
|
||||
<Button
|
||||
type="submit"
|
||||
colorScheme="blue"
|
||||
isLoading={saving}
|
||||
loadingText="Saving..."
|
||||
>
|
||||
💾 Save Configuration
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => window.location.reload()}>
|
||||
🔄 Reset
|
||||
</Button>
|
||||
</HStack>
|
||||
</Form>
|
||||
</CardBody>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
// Dataset Manager - Type 3 Form Pattern implementation
|
||||
function DatasetManager() {
|
||||
const { triggerVariableRefresh } = useVariableContext()
|
||||
const [datasetsConfig, setDatasetsConfig] = useState(null)
|
||||
const [variablesConfig, setVariablesConfig] = useState(null)
|
||||
const [datasetsSchemaData, setDatasetsSchemaData] = useState(null)
|
||||
const [variablesSchemaData, setVariablesSchemaData] = useState(null)
|
||||
const [selectedDatasetId, setSelectedDatasetId] = useState('')
|
||||
const [loading, setLoading] = useState(true)
|
||||
const toast = useToast()
|
||||
|
||||
const loadDatasetData = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const [datasetsData, variablesData, datasetsSchemaResponse, variablesSchemaResponse] = await Promise.all([
|
||||
api.readConfig('dataset-definitions'),
|
||||
api.readConfig('dataset-variables'),
|
||||
api.getSchema('dataset-definitions'),
|
||||
api.getSchema('dataset-variables')
|
||||
])
|
||||
|
||||
setDatasetsConfig(datasetsData)
|
||||
setVariablesConfig(variablesData)
|
||||
setDatasetsSchemaData(datasetsSchemaResponse)
|
||||
setVariablesSchemaData(variablesSchemaResponse)
|
||||
|
||||
// Auto-select first dataset if none selected
|
||||
if (!selectedDatasetId && datasetsData?.datasets?.length > 0) {
|
||||
setSelectedDatasetId(datasetsData.datasets[0].id)
|
||||
}
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: '❌ Failed to load dataset data',
|
||||
description: error.message,
|
||||
status: 'error',
|
||||
duration: 3000
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const saveDatasets = async (formData) => {
|
||||
try {
|
||||
await api.writeConfig('dataset-definitions', formData)
|
||||
toast({
|
||||
title: '✅ Dataset definitions saved',
|
||||
status: 'success',
|
||||
duration: 2000
|
||||
})
|
||||
setDatasetsConfig(formData)
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: '❌ Failed to save datasets',
|
||||
description: error.message,
|
||||
status: 'error',
|
||||
duration: 3000
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const saveVariables = async (formData) => {
|
||||
try {
|
||||
await api.writeConfig('dataset-variables', formData)
|
||||
toast({
|
||||
title: '✅ Dataset variables saved',
|
||||
status: 'success',
|
||||
duration: 2000
|
||||
})
|
||||
setVariablesConfig(formData)
|
||||
// Trigger refresh of all variable selector widgets
|
||||
triggerVariableRefresh()
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: '❌ Failed to save variables',
|
||||
description: error.message,
|
||||
status: 'error',
|
||||
duration: 3000
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Get filtered variables for selected dataset
|
||||
const getSelectedDatasetVariables = () => {
|
||||
if (!variablesConfig?.variables || !selectedDatasetId) {
|
||||
return { variables: [] }
|
||||
}
|
||||
|
||||
const datasetVars = variablesConfig.variables.find(v => v.dataset_id === selectedDatasetId)
|
||||
return datasetVars || { variables: [] }
|
||||
}
|
||||
|
||||
// Update variables for selected dataset
|
||||
const updateSelectedDatasetVariables = (newVariableData) => {
|
||||
if (!variablesConfig?.variables || !selectedDatasetId) return
|
||||
|
||||
const updatedVariables = variablesConfig.variables.map(v =>
|
||||
v.dataset_id === selectedDatasetId
|
||||
? { ...v, ...newVariableData }
|
||||
: v
|
||||
)
|
||||
|
||||
// If dataset not found, add new entry
|
||||
if (!variablesConfig.variables.find(v => v.dataset_id === selectedDatasetId)) {
|
||||
updatedVariables.push({
|
||||
dataset_id: selectedDatasetId,
|
||||
...newVariableData
|
||||
})
|
||||
}
|
||||
|
||||
const updatedConfig = { ...variablesConfig, variables: updatedVariables }
|
||||
setVariablesConfig(updatedConfig)
|
||||
}
|
||||
|
||||
// Available datasets for combo selector
|
||||
const availableDatasets = datasetsConfig?.datasets || []
|
||||
|
||||
useEffect(() => {
|
||||
loadDatasetData()
|
||||
}, [])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Card>
|
||||
<CardBody>
|
||||
<Flex align="center" justify="center" py={8}>
|
||||
<Spinner mr={3} />
|
||||
<Text>Loading dataset configurations...</Text>
|
||||
</Flex>
|
||||
</CardBody>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<VStack spacing={4} align="stretch">
|
||||
<Flex align="center">
|
||||
<Heading size="lg">📊 Dataset Manager</Heading>
|
||||
<Spacer />
|
||||
<Button size="sm" variant="outline" onClick={loadDatasetData}>
|
||||
🔄 Refresh
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
<Tabs variant="enclosed" colorScheme="blue">
|
||||
<TabList>
|
||||
<Tab>📋 Dataset Definitions</Tab>
|
||||
<Tab>⚙️ Dataset Variables</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabPanels>
|
||||
<TabPanel p={0} pt={4}>
|
||||
{datasetsSchemaData?.schema && datasetsConfig && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Heading size="md">Dataset Metadata Configuration</Heading>
|
||||
<Text fontSize="sm" color="gray.500" mt={1}>
|
||||
Configure dataset names, prefixes, sampling intervals and enable/disable datasets
|
||||
</Text>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Form
|
||||
schema={datasetsSchemaData.schema}
|
||||
uiSchema={datasetsSchemaData.uiSchema}
|
||||
formData={datasetsConfig}
|
||||
validator={validator}
|
||||
widgets={allWidgets}
|
||||
templates={{ ObjectFieldTemplate: LayoutObjectFieldTemplate }}
|
||||
onSubmit={({ formData }) => saveDatasets(formData)}
|
||||
onChange={({ formData }) => setDatasetsConfig(formData)}
|
||||
>
|
||||
<HStack spacing={2} mt={4}>
|
||||
<Button type="submit" colorScheme="blue">
|
||||
💾 Save Definitions
|
||||
</Button>
|
||||
<Button variant="outline" onClick={loadDatasetData}>
|
||||
🔄 Reset
|
||||
</Button>
|
||||
</HStack>
|
||||
</Form>
|
||||
</CardBody>
|
||||
</Card>
|
||||
)}
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel p={0} pt={4}>
|
||||
{/* Dataset Variables Configuration with Combo Selector */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Heading size="md">Dataset Variables Configuration</Heading>
|
||||
<Text fontSize="sm" color="gray.500" mt={1}>
|
||||
Select a dataset, then configure its PLC variables and streaming settings
|
||||
</Text>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
{/* Step 1: Dataset Selector (Combo) */}
|
||||
<VStack spacing={4} align="stretch">
|
||||
<Box>
|
||||
<Text fontSize="sm" fontWeight="bold" mb={2}>
|
||||
🎯 Select Dataset
|
||||
</Text>
|
||||
<Select
|
||||
value={selectedDatasetId}
|
||||
onChange={(e) => setSelectedDatasetId(e.target.value)}
|
||||
placeholder="Choose a dataset to configure..."
|
||||
size="md"
|
||||
>
|
||||
{availableDatasets.map(dataset => (
|
||||
<option key={dataset.id} value={dataset.id}>
|
||||
📊 {dataset.name} ({dataset.id})
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
{availableDatasets.length === 0 && (
|
||||
<Text fontSize="sm" color="orange.500" mt={2}>
|
||||
⚠️ No datasets available. Configure datasets first in the "Dataset Definitions" tab.
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Variables Configuration Form */}
|
||||
{selectedDatasetId && (
|
||||
<Box>
|
||||
<Divider mb={4} />
|
||||
<Text fontSize="sm" fontWeight="bold" mb={2}>
|
||||
⚙️ Configure Variables for Dataset "{selectedDatasetId}"
|
||||
</Text>
|
||||
|
||||
{/* Simplified schema for selected dataset variables */}
|
||||
{(() => {
|
||||
const selectedDatasetVars = getSelectedDatasetVariables()
|
||||
|
||||
// Schema for this dataset's variables
|
||||
const singleDatasetSchema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
variables: {
|
||||
type: "array",
|
||||
title: "Variables",
|
||||
description: `PLC variables to record in dataset ${selectedDatasetId}`,
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string", title: "Variable Name" },
|
||||
area: {
|
||||
type: "string",
|
||||
title: "Memory Area",
|
||||
enum: ["db", "mw", "m", "pew", "pe", "paw", "pa", "e", "a", "mb"],
|
||||
default: "db"
|
||||
},
|
||||
db: { type: "integer", title: "DB Number", minimum: 1, maximum: 9999 },
|
||||
offset: { type: "integer", title: "Offset", minimum: 0, maximum: 8191 },
|
||||
bit: { type: "integer", title: "Bit Position", minimum: 0, maximum: 7 },
|
||||
type: {
|
||||
type: "string",
|
||||
title: "Data Type",
|
||||
enum: ["real", "int", "dint", "bool", "word", "byte"],
|
||||
default: "real"
|
||||
},
|
||||
streaming: { type: "boolean", title: "Stream to UDP", default: false }
|
||||
},
|
||||
required: ["name", "area", "offset", "type"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const singleDatasetUiSchema = {
|
||||
variables: {
|
||||
items: {
|
||||
"ui:layout": [[
|
||||
{ "name": "name", "width": 3 },
|
||||
{ "name": "area", "width": 2 },
|
||||
{ "name": "db", "width": 1 },
|
||||
{ "name": "offset", "width": 2 },
|
||||
{ "name": "type", "width": 2 },
|
||||
{ "name": "streaming", "width": 2 }
|
||||
]]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Form
|
||||
schema={singleDatasetSchema}
|
||||
uiSchema={singleDatasetUiSchema}
|
||||
formData={selectedDatasetVars}
|
||||
validator={validator}
|
||||
widgets={allWidgets}
|
||||
templates={{ ObjectFieldTemplate: LayoutObjectFieldTemplate }}
|
||||
onSubmit={({ formData }) => {
|
||||
updateSelectedDatasetVariables(formData)
|
||||
saveVariables(variablesConfig).then(() => {
|
||||
// Additional trigger after successful save
|
||||
triggerVariableRefresh()
|
||||
})
|
||||
}}
|
||||
onChange={({ formData }) => updateSelectedDatasetVariables(formData)}
|
||||
>
|
||||
<HStack spacing={2} mt={4}>
|
||||
<Button type="submit" colorScheme="blue">
|
||||
💾 Save Variables for {selectedDatasetId}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={loadDatasetData}>
|
||||
🔄 Reset
|
||||
</Button>
|
||||
</HStack>
|
||||
</Form>
|
||||
)
|
||||
})()}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{!selectedDatasetId && availableDatasets.length > 0 && (
|
||||
<Box textAlign="center" py={8}>
|
||||
<Text color="gray.500">
|
||||
👆 Select a dataset above to configure its variables
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</VStack>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
||||
// Events Display Component
|
||||
function EventsDisplay({ events, loading, onRefresh }) {
|
||||
const cardBg = useColorModeValue('white', 'gray.700')
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Card bg={cardBg}>
|
||||
<CardBody>
|
||||
<Flex align="center" justify="center" py={4}>
|
||||
<Spinner mr={3} />
|
||||
<Text>Loading events...</Text>
|
||||
</Flex>
|
||||
</CardBody>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card bg={cardBg}>
|
||||
<CardHeader>
|
||||
<Flex align="center">
|
||||
<Heading size="md">📋 Recent Events</Heading>
|
||||
<Spacer />
|
||||
<Button size="sm" variant="outline" onClick={onRefresh}>
|
||||
🔄 Refresh
|
||||
</Button>
|
||||
</Flex>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<TableContainer>
|
||||
<Table size="sm">
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>Time</Th>
|
||||
<Th>Type</Th>
|
||||
<Th>Message</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{events?.map((event, index) => (
|
||||
<Tr key={index}>
|
||||
<Td>
|
||||
<Code fontSize="xs">
|
||||
{new Date(event.timestamp).toLocaleString()}
|
||||
</Code>
|
||||
</Td>
|
||||
<Td>
|
||||
<Badge
|
||||
colorScheme={
|
||||
event.level === 'ERROR' ? 'red' :
|
||||
event.level === 'WARNING' ? 'orange' :
|
||||
event.level === 'INFO' ? 'blue' : 'gray'
|
||||
}
|
||||
>
|
||||
{event.level}
|
||||
</Badge>
|
||||
</Td>
|
||||
<Td>{event.message}</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
{(!events || events.length === 0) && (
|
||||
<Text textAlign="center" py={4} color="gray.500">
|
||||
No events found
|
||||
</Text>
|
||||
)}
|
||||
</CardBody>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
// Main Dashboard Component - PLC S7-31x Streamer & Logger
|
||||
export default function Dashboard() {
|
||||
return (
|
||||
<VariableProvider>
|
||||
<DashboardContent />
|
||||
</VariableProvider>
|
||||
)
|
||||
}
|
||||
|
||||
// Dashboard Content Component (separated to use context)
|
||||
function DashboardContent() {
|
||||
const [status, setStatus] = useState(null)
|
||||
const [statusLoading, setStatusLoading] = useState(true)
|
||||
const [statusError, setStatusError] = useState('')
|
||||
|
||||
const [schemaData, setSchemaData] = useState(null)
|
||||
const [formData, setFormData] = useState(null)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [message, setMessage] = useState('')
|
||||
|
||||
const [events, setEvents] = useState([])
|
||||
const [eventsLoading, setEventsLoading] = useState(false)
|
||||
|
||||
// Load status once
|
||||
const loadStatus = useCallback(async () => {
|
||||
try {
|
||||
setStatusLoading(true)
|
||||
setStatusError('')
|
||||
const statusData = await api.getStatus()
|
||||
setStatus(statusData)
|
||||
} catch (error) {
|
||||
setStatusError(error.message)
|
||||
} finally {
|
||||
setStatusLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Real-time status updates via polling
|
||||
const subscribeSSE = useCallback(() => {
|
||||
// Use polling for real-time updates (every 5 seconds)
|
||||
const interval = setInterval(async () => {
|
||||
try {
|
||||
const statusData = await api.getStatus()
|
||||
setStatus(statusData)
|
||||
setStatusError('')
|
||||
} catch (error) {
|
||||
console.error('Status polling error:', error)
|
||||
}
|
||||
}, 5000)
|
||||
|
||||
return () => {
|
||||
clearInterval(interval)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Load PLC config
|
||||
const loadConfig = useCallback(async () => {
|
||||
try {
|
||||
const [schemaResponse, configData] = await Promise.all([
|
||||
api.getSchema('plc'),
|
||||
api.readConfig('plc')
|
||||
])
|
||||
setSchemaData(schemaResponse)
|
||||
setFormData(configData)
|
||||
setMessage('')
|
||||
} catch (error) {
|
||||
console.error('Failed to load PLC config:', error)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Save config
|
||||
const saveConfig = useCallback(async (data) => {
|
||||
try {
|
||||
setSaving(true)
|
||||
await api.writeConfig('plc', data)
|
||||
setMessage(`✅ PLC configuration saved successfully`)
|
||||
setTimeout(() => setMessage(''), 3000)
|
||||
setFormData(data)
|
||||
} catch (error) {
|
||||
setMessage(`❌ Failed to save: ${error.message}`)
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Load events
|
||||
const loadEvents = useCallback(async () => {
|
||||
try {
|
||||
setEventsLoading(true)
|
||||
const eventsData = await api.getEvents(50)
|
||||
setEvents(eventsData.events || [])
|
||||
} catch (error) {
|
||||
console.error('Failed to load events:', error)
|
||||
} finally {
|
||||
setEventsLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Effects
|
||||
useEffect(() => {
|
||||
loadStatus()
|
||||
loadConfig()
|
||||
loadEvents()
|
||||
|
||||
const cleanup = subscribeSSE()
|
||||
return cleanup
|
||||
}, [loadStatus, loadConfig, loadEvents, subscribeSSE])
|
||||
|
||||
if (statusLoading) {
|
||||
return (
|
||||
<Container maxW="container.xl" py={6}>
|
||||
<Flex align="center" justify="center" minH="200px">
|
||||
<Spinner size="xl" mr={4} />
|
||||
<Text fontSize="lg">Loading dashboard...</Text>
|
||||
</Flex>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Container maxW="container.xl" py={6}>
|
||||
<VStack spacing={6} align="stretch">
|
||||
<Flex align="center" mb={4}>
|
||||
<Heading size="xl">🏭 PLC S7-31x Streamer & Logger</Heading>
|
||||
<Spacer />
|
||||
<Button size="sm" variant="outline" onClick={loadStatus}>
|
||||
🔄 Refresh Status
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
{statusError && (
|
||||
<Alert status="error">
|
||||
<AlertIcon />
|
||||
Failed to load status: {statusError}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<StatusBar status={status} onRefresh={loadStatus} />
|
||||
|
||||
{/* PLC Configuration Section */}
|
||||
<ConfigurationPanel
|
||||
schemaData={schemaData}
|
||||
formData={formData}
|
||||
onFormChange={setFormData}
|
||||
onSave={saveConfig}
|
||||
saving={saving}
|
||||
message={message}
|
||||
/>
|
||||
|
||||
{/* Dataset Management Section */}
|
||||
<DatasetManager />
|
||||
|
||||
{/* Plot Management Section */}
|
||||
<PlotManager />
|
||||
|
||||
{/* Events Section */}
|
||||
<EventsDisplay
|
||||
events={events}
|
||||
loading={eventsLoading}
|
||||
onRefresh={loadEvents}
|
||||
/>
|
||||
</VStack>
|
||||
</Container>
|
||||
)
|
||||
}
|
|
@ -3,11 +3,11 @@
|
|||
"should_connect": true,
|
||||
"should_stream": true,
|
||||
"active_datasets": [
|
||||
"Fast",
|
||||
"DAR",
|
||||
"Test",
|
||||
"DAR"
|
||||
"Fast"
|
||||
]
|
||||
},
|
||||
"auto_recovery_enabled": true,
|
||||
"last_update": "2025-08-14T14:47:13.218027"
|
||||
"last_update": "2025-08-14T15:03:55.394271"
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
import json
|
||||
import jsonschema
|
||||
|
||||
# Cargar esquema y datos para dataset-definitions
|
||||
with open("config/schema/dataset-definitions.schema.json", "r") as f:
|
||||
schema = json.load(f)
|
||||
|
||||
with open("config/data/dataset_definitions.json", "r") as f:
|
||||
data = json.load(f)
|
||||
|
||||
# Validar
|
||||
try:
|
||||
jsonschema.validate(data, schema)
|
||||
print("✅ Dataset definitions validation successful!")
|
||||
except jsonschema.ValidationError as e:
|
||||
print("❌ Dataset definitions validation error:")
|
||||
print(f'Property path: {".".join(str(x) for x in e.absolute_path)}')
|
||||
print(f"Message: {e.message}")
|
||||
print(f"Failed value: {e.instance}")
|
||||
except Exception as e:
|
||||
print(f"❌ Other error: {e}")
|
||||
|
||||
print("\n" + "=" * 50 + "\n")
|
||||
|
||||
# También validar PLC config
|
||||
with open("config/schema/plc.schema.json", "r") as f:
|
||||
plc_schema = json.load(f)
|
||||
|
||||
with open("config/data/plc_config.json", "r") as f:
|
||||
plc_data = json.load(f)
|
||||
|
||||
try:
|
||||
jsonschema.validate(plc_data, plc_schema)
|
||||
print("✅ PLC config validation successful!")
|
||||
except jsonschema.ValidationError as e:
|
||||
print("❌ PLC config validation error:")
|
||||
print(f'Property path: {".".join(str(x) for x in e.absolute_path)}')
|
||||
print(f"Message: {e.message}")
|
||||
print(f"Failed value: {e.instance}")
|
||||
except Exception as e:
|
||||
print(f"❌ Other error: {e}")
|
Loading…
Reference in New Issue