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:
Miguel 2025-08-14 15:04:47 +02:00
parent f5db758698
commit a9396ec309
9 changed files with 144 additions and 1785 deletions

View File

@ -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: ✅

View File

@ -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.

View File

@ -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
}

View File

@ -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>
)
}

View File

@ -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={() => {}}
/>
))}

View File

@ -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(() => {

View File

@ -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>
)
}

View File

@ -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"
}

View File

@ -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}")