diff --git a/.doc/MemoriaDeEvolucion.md b/.doc/MemoriaDeEvolucion.md index 0ea7042..7fd9a34 100644 --- a/.doc/MemoriaDeEvolucion.md +++ b/.doc/MemoriaDeEvolucion.md @@ -243,3 +243,172 @@ El frontend estaba intentando acceder a esquemas legacy eliminados: - ✅ Dashboard muestra automáticamente todos los esquemas disponibles - ✅ Config editor funciona con nuevos esquemas separados - ✅ No más errores 404 en consola del navegador + +## Mejoras en Formularios RJSF para Dataset Management + +**Solicitud del Usuario:** Mejorar la presentación de los formularios RJSF de dataset definitions y variables, aprovechando RJSF y Chakra UI para mostrar mejor estos datos y permitir edición más intuitiva. + +### Mejoras Implementadas + +#### 1. Nuevo Componente DatasetManager Integrado + +**Archivo:** `frontend/src/components/DatasetManager.jsx` + +**Características principales:** +- ✅ Vista unificada para gestión de datasets definitions y variables +- ✅ Overview visual de todos los datasets con métricas (variables, streaming, status) +- ✅ Interface con tabs para organizar diferentes aspectos de configuración +- ✅ Cards interactivas para selección de datasets +- ✅ Integración completa con APIs existentes +- ✅ Toast notifications para feedback de operaciones + +**Tabs implementados:** +- 📋 **Dataset Definitions**: Formulario RJSF mejorado para metadatos +- ⚙️ **Dataset Variables**: Formulario RJSF para configuración técnica +- 🔧 **Variable Manager**: Interface visual especializada para gestionar variables + +#### 2. Componente Especializado DatasetVariableManager + +**Archivo:** `frontend/src/components/DatasetVariableManager.jsx` + +**Funcionalidades:** +- ✅ Gestión visual de variables por dataset con cards individuales +- ✅ Modal mejorado para agregar/editar variables con validación +- ✅ Toggle fácil para activar/desactivar streaming por variable +- ✅ Información contextual de áreas PLC y tipos de datos +- ✅ Eliminación de variables con confirmación +- ✅ Vista organizada por grid responsive + +**Features del editor de variables:** +- Select con iconos para áreas PLC (🗃️ DB, 📊 MW, 💾 M, etc.) +- Select con descripciones para tipos de datos (REAL 32-bit, INT 16-bit, etc.) +- NumberInput con steppers para offsets y direcciones +- Checkbox mejorado para streaming con estado visual +- Validación en tiempo real de nombres de variables + +#### 3. Widgets Personalizados de Chakra UI + +**Archivo:** `frontend/src/components/rjsf/PlcWidgets.jsx` + +**Widgets especializados creados:** +- ✅ **PlcAreaWidget**: Select con iconos y descripciones para áreas PLC +- ✅ **PlcDataTypeWidget**: Select con badges de tamaño y descripción de tipos +- ✅ **PlcNumberWidget**: NumberInput con validación de rangos PLC +- ✅ **PlcStreamingWidget**: Checkbox con estado visual y explicaciones +- ✅ **PlcVariableNameWidget**: Input con validación y sugerencias de nombres + +**Características de widgets:** +- Iconos contextuales para mejor UX (🗃️📊💾📥📤🔌) +- Tooltips y help text explicativo +- Validación visual en tiempo real +- Color coding por estado (streaming/no streaming, válido/inválido) +- Diseño responsivo con Chakra UI + +#### 4. Widgets RJSF Mejorados + +**Archivo:** `frontend/src/components/rjsf/widgets.jsx` + +**Actualización completa:** +- ❌ Removidos widgets Bootstrap obsoletos +- ✅ Implementados widgets Chakra UI nativos +- ✅ Consistencia visual con tema de la aplicación +- ✅ Manejo mejorado de errores y validación +- ✅ Integración con color modes (light/dark) + +#### 5. UI Schemas Mejorados + +**Archivos actualizados:** +- `config/schema/ui/dataset-definitions.uischema.json` +- `config/schema/ui/dataset-variables.uischema.json` + +**Mejoras implementadas:** +- ✅ Descripciones con emojis y contexto (📊 Configure dataset metadata) +- ✅ Help text específico para cada campo +- ✅ Layouts responsivos con ui:layout +- ✅ Widgets especializados para casos PLC +- ✅ Opciones de enum con labels descriptivos +- ✅ Placeholders informativos +- ✅ Order específico de campos para flujo lógico + +**Ejemplos de mejoras:** +```json +"area": { + "ui:options": { + "enumOptions": [ + {"value": "db", "label": "🗃️ DB (Data Block)"}, + {"value": "mw", "label": "📊 MW (Memory Word)"} + ] + } +} +``` + +#### 6. Nueva Ruta y Navegación + +**Archivo:** `frontend/src/App.jsx` + +**Integración completa:** +- ✅ Nueva ruta `/datasets` para DatasetManager +- ✅ Botón de navegación "📊 Datasets" en navbar +- ✅ Integración seamless con routing existente + +#### 7. Actualización de Formularios Existentes + +**Archivos actualizados:** +- `frontend/src/pages/Config.jsx` +- `frontend/src/pages/Dashboard.jsx` + +**Cambios:** +- ✅ Integración de widgets personalizados en todos los formularios RJSF +- ✅ Consistencia visual mejorada +- ✅ Mejor UX en configuración PLC y datasets + +### Beneficios de las Mejoras + +#### Experiencia de Usuario Mejorada +- **Interfaz Visual**: Cards interactivas vs formularios planos +- **Feedback Inmediato**: Toast notifications y validación en tiempo real +- **Contexto Claro**: Iconos, colores y descripciones específicas para PLC +- **Flujo Intuitivo**: Navegación organizada en tabs y overview + +#### Productividad Aumentada +- **Gestión Centralizada**: Todo el dataset management en una vista +- **Operaciones Rápidas**: Toggle streaming, edición modal, eliminación directa +- **Información Contextual**: Métricas de variables, estado de streaming visible +- **Validación Proactiva**: Errores detectados antes de envío + +#### Mantenibilidad Mejorada +- **Widgets Reutilizables**: Componentes PLC específicos para consistencia +- **Código Limpio**: Separación clara entre lógica y presentación +- **Tema Unificado**: Chakra UI consistente en toda la aplicación +- **Extensibilidad**: Fácil agregar nuevos widgets PLC o mejoras + +#### Características Técnicas Específicas PLC +- **Validación PLC**: Rangos correctos para offsets, DBs, bits +- **Tipos de Datos**: Información visual de tamaños y características +- **Áreas de Memoria**: Iconografía clara para diferentes áreas S7 +- **Streaming Control**: Gestión visual del estado UDP streaming + +### Archivos Creados + +**Nuevos Componentes:** +- `frontend/src/components/DatasetManager.jsx` +- `frontend/src/components/DatasetVariableManager.jsx` +- `frontend/src/components/rjsf/PlcWidgets.jsx` + +**Archivos Actualizados:** +- `frontend/src/components/rjsf/widgets.jsx` (refactorización completa) +- `config/schema/ui/dataset-definitions.uischema.json` (mejoras UX) +- `config/schema/ui/dataset-variables.uischema.json` (mejoras UX) +- `frontend/src/App.jsx` (nueva ruta y navegación) +- `frontend/src/pages/Config.jsx` (integración widgets) +- `frontend/src/pages/Dashboard.jsx` (integración widgets) + +### Resultado Final + +- ✅ **DatasetManager disponible en `/datasets`** con interface visual completa +- ✅ **Formularios RJSF mejorados** con widgets PLC específicos +- ✅ **UX significativamente mejorada** para configuración de datasets y variables +- ✅ **Consistencia visual** con Chakra UI en toda la aplicación +- ✅ **Widgets reutilizables** para futuras expansiones PLC +- ✅ **Compatibilidad total** con APIs y funcionalidad existente +- ✅ **Cero errores de linting** en todos los nuevos componentes \ No newline at end of file diff --git a/application_events.json b/application_events.json index de1cc8d..4555e5e 100644 --- a/application_events.json +++ b/application_events.json @@ -9364,8 +9364,15 @@ "event_type": "application_started", "message": "Application initialization completed successfully", "details": {} + }, + { + "timestamp": "2025-08-12T19:08:01.450637", + "level": "info", + "event_type": "application_started", + "message": "Application initialization completed successfully", + "details": {} } ], - "last_updated": "2025-08-12T18:27:58.167381", - "total_entries": 877 + "last_updated": "2025-08-12T19:08:01.450637", + "total_entries": 878 } \ No newline at end of file diff --git a/config/schema/ui/dataset-definitions.uischema.json b/config/schema/ui/dataset-definitions.uischema.json index 9bebd52..136800d 100644 --- a/config/schema/ui/dataset-definitions.uischema.json +++ b/config/schema/ui/dataset-definitions.uischema.json @@ -1,19 +1,68 @@ { "datasets": { - "ui:description": "Define dataset metadata (name, prefix, intervals, etc.)", - "items": { + "ui:description": "📊 Configure dataset metadata: names, CSV file prefixes, sampling intervals, and activation status", + "ui:options": { + "addable": true, + "orderable": true, + "removable": true + }, + "additionalProperties": { + "ui:order": [ + "name", + "prefix", + "enabled", + "sampling_interval", + "created" + ], "name": { - "ui:placeholder": "Temperature Sensors" + "ui:placeholder": "e.g., Temperature Sensors, Production Line A", + "ui:help": "Human-readable name for this dataset" }, "prefix": { - "ui:placeholder": "temp" - }, - "sampling_interval": { - "ui:widget": "updown" + "ui:placeholder": "e.g., temp, line_a, sensors", + "ui:help": "Short prefix for CSV filenames (alphanumeric, underscore, dash only)" }, "enabled": { - "ui:widget": "checkbox" + "ui:widget": "checkbox", + "ui:help": "When enabled, this dataset will be actively sampled and recorded" + }, + "sampling_interval": { + "ui:widget": "updown", + "ui:placeholder": "Leave empty to use global interval", + "ui:help": "Custom sampling interval in seconds (0.01-10s). Leave empty to use the global PLC sampling interval." + }, + "created": { + "ui:widget": "text", + "ui:readonly": true, + "ui:help": "Timestamp when this dataset was created" } } - } + }, + "active_datasets": { + "ui:widget": "checkboxes", + "ui:description": "✅ Select which datasets are currently active for sampling", + "ui:help": "Only active datasets will be sampled from the PLC and written to CSV files" + }, + "current_dataset_id": { + "ui:widget": "select", + "ui:description": "🎯 Currently selected dataset for variable editing", + "ui:help": "This determines which dataset is shown by default in the interface" + }, + "version": { + "ui:widget": "text", + "ui:readonly": true, + "ui:help": "Configuration schema version" + }, + "last_update": { + "ui:widget": "text", + "ui:readonly": true, + "ui:help": "Timestamp of last configuration update" + }, + "ui:order": [ + "datasets", + "active_datasets", + "current_dataset_id", + "version", + "last_update" + ] } \ No newline at end of file diff --git a/config/schema/ui/dataset-variables.uischema.json b/config/schema/ui/dataset-variables.uischema.json index f9e1a3a..478f35b 100644 --- a/config/schema/ui/dataset-variables.uischema.json +++ b/config/schema/ui/dataset-variables.uischema.json @@ -1,30 +1,198 @@ { "dataset_variables": { - "ui:description": "Variables assigned to each dataset", - "items": { + "ui:description": "⚙️ Configure PLC variables for each dataset - specify memory areas, data types, and streaming options", + "ui:options": { + "addable": true, + "orderable": false, + "removable": true + }, + "additionalProperties": { + "ui:order": [ + "variables", + "streaming_variables" + ], "variables": { - "ui:description": "Variables configuration for this dataset", - "items": { + "ui:description": "🔧 PLC Variable Definitions", + "ui:help": "Define PLC memory locations, data types, and properties for each variable", + "ui:options": { + "addable": true, + "orderable": true, + "removable": true + }, + "additionalProperties": { + "ui:order": [ + "area", + "db", + "offset", + "bit", + "type", + "streaming" + ], + "ui:layout": [ + [ + { + "name": "area", + "width": 3 + }, + { + "name": "db", + "width": 2 + }, + { + "name": "offset", + "width": 3 + }, + { + "name": "bit", + "width": 2 + }, + { + "name": "type", + "width": 2 + } + ], + [ + { + "name": "streaming", + "width": 12 + } + ] + ], "area": { - "ui:widget": "select" + "ui:widget": "select", + "ui:help": "PLC memory area (DB=DataBlock, MW=MemoryWord, etc.)", + "ui:options": { + "enumOptions": [ + { + "value": "db", + "label": "🗃️ DB (Data Block)" + }, + { + "value": "mw", + "label": "📊 MW (Memory Word)" + }, + { + "value": "m", + "label": "💾 M (Memory)" + }, + { + "value": "pew", + "label": "📥 PEW (Process Input Word)" + }, + { + "value": "pe", + "label": "📥 PE (Process Input)" + }, + { + "value": "paw", + "label": "📤 PAW (Process Output Word)" + }, + { + "value": "pa", + "label": "📤 PA (Process Output)" + }, + { + "value": "e", + "label": "🔌 E (Input)" + }, + { + "value": "a", + "label": "🔌 A (Output)" + }, + { + "value": "mb", + "label": "💾 MB (Memory Byte)" + } + ] + } }, "db": { - "ui:widget": "updown" + "ui:widget": "updown", + "ui:help": "Data Block number (required for DB area)", + "ui:placeholder": "1011" }, "offset": { - "ui:widget": "updown" + "ui:widget": "updown", + "ui:help": "Byte offset within the memory area" }, "bit": { - "ui:widget": "updown" + "ui:widget": "updown", + "ui:help": "Bit position (0-7) for bit-addressable areas" }, "type": { - "ui:widget": "select" + "ui:widget": "select", + "ui:help": "PLC data type", + "ui:options": { + "enumOptions": [ + { + "value": "real", + "label": "🔢 REAL (32-bit float)" + }, + { + "value": "int", + "label": "🔢 INT (16-bit signed)" + }, + { + "value": "bool", + "label": "✅ BOOL (1-bit boolean)" + }, + { + "value": "dint", + "label": "🔢 DINT (32-bit signed)" + }, + { + "value": "word", + "label": "🔢 WORD (16-bit unsigned)" + }, + { + "value": "byte", + "label": "🔢 BYTE (8-bit unsigned)" + }, + { + "value": "uint", + "label": "🔢 UINT (16-bit unsigned)" + }, + { + "value": "udint", + "label": "🔢 UDINT (32-bit unsigned)" + }, + { + "value": "sint", + "label": "🔢 SINT (8-bit signed)" + }, + { + "value": "usint", + "label": "🔢 USINT (8-bit unsigned)" + } + ] + } }, "streaming": { - "ui:widget": "checkbox" + "ui:widget": "checkbox", + "ui:help": "📡 Enable real-time streaming to PlotJuggler for visualization" } } + }, + "streaming_variables": { + "ui:widget": "checkboxes", + "ui:description": "📡 Streaming Variables", + "ui:help": "Variables that are streamed in real-time to PlotJuggler. This list is automatically updated when you enable/disable streaming on individual variables above." } } - } + }, + "version": { + "ui:widget": "text", + "ui:readonly": true, + "ui:help": "Configuration schema version" + }, + "last_update": { + "ui:widget": "text", + "ui:readonly": true, + "ui:help": "Timestamp of last configuration update" + }, + "ui:order": [ + "dataset_variables", + "version", + "last_update" + ] } \ No newline at end of file diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 92d879a..cfd1b5c 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -8,6 +8,7 @@ import ConfigPage from './pages/Config.jsx' import PlotsPage from './pages/Plots.jsx' import PLCConfigModal from './components/PLCConfigModal.jsx' import DashboardPage from './pages/Dashboard.jsx' +import DatasetManager from './components/DatasetManager.jsx' function Home() { return ( @@ -86,6 +87,7 @@ function NavBar() { + @@ -102,19 +104,14 @@ function App() { return ( - - - } /> } /> + } /> } /> } /> } /> - setShowPLCModal(false)} /> ) } diff --git a/frontend/src/components/DatasetManager.jsx b/frontend/src/components/DatasetManager.jsx new file mode 100644 index 0000000..cac4c70 --- /dev/null +++ b/frontend/src/components/DatasetManager.jsx @@ -0,0 +1,287 @@ +import React, { useState, useEffect } from 'react' +import { + Box, Container, Heading, HStack, VStack, Button, Tabs, TabList, TabPanels, Tab, TabPanel, + Card, CardBody, CardHeader, Grid, GridItem, Text, Badge, Alert, AlertIcon, + useColorModeValue, Flex, Spacer, IconButton, useToast +} from '@chakra-ui/react' +import Form from '@rjsf/chakra-ui' +import validator from '@rjsf/validator-ajv8' +import { getSchema, readConfig, writeConfig } from '../services/api.js' +import LayoutObjectFieldTemplate from './rjsf/LayoutObjectFieldTemplate.jsx' +import DatasetVariableManager from './DatasetVariableManager.jsx' + +export default function DatasetManager() { + const [datasetsSchema, setDatasetsSchema] = useState(null) + const [datasetsUiSchema, setDatasetsUiSchema] = useState(null) + const [datasetsData, setDatasetsData] = useState(null) + + const [variablesSchema, setVariablesSchema] = useState(null) + const [variablesUiSchema, setVariablesUiSchema] = useState(null) + const [variablesData, setVariablesData] = useState(null) + + const [loading, setLoading] = useState(true) + const [selectedDatasetId, setSelectedDatasetId] = useState(null) + + const toast = useToast() + const cardBg = useColorModeValue('white', 'gray.700') + const borderColor = useColorModeValue('gray.200', 'gray.600') + + useEffect(() => { + loadData() + }, []) + + const loadData = async () => { + setLoading(true) + try { + const [ + datasetsSchemaResp, datasetsDataResp, + variablesSchemaResp, variablesDataResp + ] = await Promise.all([ + getSchema('dataset-definitions'), + readConfig('dataset-definitions'), + getSchema('dataset-variables'), + readConfig('dataset-variables') + ]) + + setDatasetsSchema(datasetsSchemaResp.schema) + setDatasetsUiSchema(datasetsSchemaResp.ui_schema) + setDatasetsData(datasetsDataResp.data) + + setVariablesSchema(variablesSchemaResp.schema) + setVariablesUiSchema(variablesSchemaResp.ui_schema) + setVariablesData(variablesDataResp.data) + + // Set first dataset as selected if none selected + if (datasetsDataResp.data?.datasets && !selectedDatasetId) { + const firstDataset = Object.keys(datasetsDataResp.data.datasets)[0] + setSelectedDatasetId(firstDataset) + } + } catch (error) { + toast({ + title: 'Error loading data', + description: error.message, + status: 'error', + duration: 5000, + isClosable: true + }) + } finally { + setLoading(false) + } + } + + const handleDatasetsSave = async ({ formData }) => { + try { + await writeConfig('dataset-definitions', formData) + setDatasetsData(formData) + toast({ + title: 'Dataset definitions saved', + status: 'success', + duration: 3000, + isClosable: true + }) + } catch (error) { + toast({ + title: 'Error saving datasets', + description: error.message, + status: 'error', + duration: 5000, + isClosable: true + }) + } + } + + const handleVariablesSave = async ({ formData }) => { + try { + await writeConfig('dataset-variables', formData) + setVariablesData(formData) + toast({ + title: 'Dataset variables saved', + status: 'success', + duration: 3000, + isClosable: true + }) + } catch (error) { + toast({ + title: 'Error saving variables', + description: error.message, + status: 'error', + duration: 5000, + isClosable: true + }) + } + } + + if (loading) { + return ( + + Loading dataset manager... + + ) + } + + return ( + + + + 📊 Dataset Manager + + + + + + + + + 📋 Dataset Definitions + ⚙️ Dataset Variables + 🔧 Variable Manager + + + + + + + Dataset Metadata Configuration + + Configure dataset names, prefixes, sampling intervals and enable/disable datasets + + + + {datasetsSchema && ( +
setDatasetsData(formData)} + uiSchema={datasetsUiSchema} + templates={{ ObjectFieldTemplate: LayoutObjectFieldTemplate }} + > + + + + +
+ )} +
+
+
+ + + + + Dataset Variables Configuration + + Raw JSON configuration for variables assigned to each dataset + + + + {variablesSchema && ( +
setVariablesData(formData)} + uiSchema={variablesUiSchema} + templates={{ ObjectFieldTemplate: LayoutObjectFieldTemplate }} + > + + + + +
+ )} +
+
+
+ + + { + const updatedData = { + ...variablesData, + dataset_variables: newVariables + } + handleVariablesSave({ formData: updatedData }) + }} + /> + +
+
+
+
+ ) +} + +function DatasetOverview({ datasets, variables, selectedDatasetId, onSelectDataset }) { + const cardBg = useColorModeValue('white', 'gray.700') + const selectedBg = useColorModeValue('blue.50', 'blue.900') + const borderColor = useColorModeValue('gray.200', 'gray.600') + + return ( + + 📊 Datasets Overview + + {Object.entries(datasets).map(([id, dataset]) => { + const varCount = variables[id]?.variables ? Object.keys(variables[id].variables).length : 0 + const streamingCount = variables[id]?.streaming_variables?.length || 0 + const isSelected = selectedDatasetId === id + + return ( + onSelectDataset(id)} + _hover={{ borderColor: 'blue.300' }} + > + + + + {dataset.name} + + {dataset.enabled ? 'Active' : 'Inactive'} + + + + + ID: {id} • Prefix: {dataset.prefix} + + + + + 🔧 {varCount} variables + + + 📡 {streamingCount} streaming + + + + {dataset.sampling_interval && ( + + ⏱️ {dataset.sampling_interval}s interval + + )} + + + + ) + })} + + + ) +} diff --git a/frontend/src/components/DatasetVariableManager.jsx b/frontend/src/components/DatasetVariableManager.jsx new file mode 100644 index 0000000..074a9c0 --- /dev/null +++ b/frontend/src/components/DatasetVariableManager.jsx @@ -0,0 +1,426 @@ +import React, { useState } from 'react' +import { + Box, VStack, HStack, Card, CardBody, CardHeader, Heading, Text, Button, + Grid, GridItem, Badge, Select, FormControl, FormLabel, Input, NumberInput, + NumberInputField, NumberInputStepper, NumberIncrementStepper, NumberDecrementStepper, + Checkbox, IconButton, useColorModeValue, Flex, Spacer, useToast, + Modal, ModalOverlay, ModalContent, ModalHeader, ModalBody, ModalFooter, + ModalCloseButton, useDisclosure, Alert, AlertIcon +} from '@chakra-ui/react' + +const PLC_AREAS = [ + { value: 'db', label: 'DB (Data Block)' }, + { value: 'mw', label: 'MW (Memory Word)' }, + { value: 'm', label: 'M (Memory)' }, + { value: 'pew', label: 'PEW (Process Input Word)' }, + { value: 'pe', label: 'PE (Process Input)' }, + { value: 'paw', label: 'PAW (Process Output Word)' }, + { value: 'pa', label: 'PA (Process Output)' }, + { value: 'e', label: 'E (Input)' }, + { value: 'a', label: 'A (Output)' }, + { value: 'mb', label: 'MB (Memory Byte)' } +] + +const DATA_TYPES = [ + { value: 'real', label: 'REAL (32-bit float)' }, + { value: 'int', label: 'INT (16-bit signed)' }, + { value: 'bool', label: 'BOOL (1-bit)' }, + { value: 'dint', label: 'DINT (32-bit signed)' }, + { value: 'word', label: 'WORD (16-bit unsigned)' }, + { value: 'byte', label: 'BYTE (8-bit unsigned)' }, + { value: 'uint', label: 'UINT (16-bit unsigned)' }, + { value: 'udint', label: 'UDINT (32-bit unsigned)' }, + { value: 'sint', label: 'SINT (8-bit signed)' }, + { value: 'usint', label: 'USINT (8-bit unsigned)' } +] + +export default function DatasetVariableManager({ + datasets, + variables, + selectedDatasetId, + onSelectDataset, + onVariablesUpdate +}) { + const { isOpen, onOpen, onClose } = useDisclosure() + const [editingVariable, setEditingVariable] = useState(null) + const toast = useToast() + + const cardBg = useColorModeValue('white', 'gray.700') + const borderColor = useColorModeValue('gray.200', 'gray.600') + + const selectedDataset = selectedDatasetId ? datasets[selectedDatasetId] : null + const datasetVariables = selectedDatasetId ? variables[selectedDatasetId]?.variables || {} : {} + const streamingVariables = selectedDatasetId ? variables[selectedDatasetId]?.streaming_variables || [] : [] + + const handleAddVariable = () => { + setEditingVariable({ + name: '', + area: 'db', + db: 1, + offset: 0, + type: 'real', + streaming: false + }) + onOpen() + } + + const handleEditVariable = (varName) => { + const variable = datasetVariables[varName] + setEditingVariable({ + name: varName, + ...variable + }) + onOpen() + } + + const handleSaveVariable = (variableData) => { + if (!selectedDatasetId) return + + const newVariables = { ...variables } + + // Initialize dataset if it doesn't exist + if (!newVariables[selectedDatasetId]) { + newVariables[selectedDatasetId] = { + variables: {}, + streaming_variables: [] + } + } + + const oldName = editingVariable?.name + const newName = variableData.name + + // Remove old variable if name changed + if (oldName && oldName !== newName && newVariables[selectedDatasetId].variables[oldName]) { + delete newVariables[selectedDatasetId].variables[oldName] + // Update streaming_variables array + newVariables[selectedDatasetId].streaming_variables = + newVariables[selectedDatasetId].streaming_variables.filter(v => v !== oldName) + } + + // Add/update variable + const { name, ...varConfig } = variableData + newVariables[selectedDatasetId].variables[name] = varConfig + + // Update streaming_variables array + const currentStreamingVars = newVariables[selectedDatasetId].streaming_variables.filter(v => v !== name) + if (varConfig.streaming) { + currentStreamingVars.push(name) + } + newVariables[selectedDatasetId].streaming_variables = currentStreamingVars + + onVariablesUpdate(newVariables) + onClose() + setEditingVariable(null) + + toast({ + title: 'Variable saved', + status: 'success', + duration: 2000, + isClosable: true + }) + } + + const handleDeleteVariable = (varName) => { + if (!selectedDatasetId) return + + const newVariables = { ...variables } + delete newVariables[selectedDatasetId].variables[varName] + newVariables[selectedDatasetId].streaming_variables = + newVariables[selectedDatasetId].streaming_variables.filter(v => v !== varName) + + onVariablesUpdate(newVariables) + + toast({ + title: 'Variable deleted', + status: 'info', + duration: 2000, + isClosable: true + }) + } + + const toggleVariableStreaming = (varName) => { + if (!selectedDatasetId) return + + const newVariables = { ...variables } + const variable = newVariables[selectedDatasetId].variables[varName] + variable.streaming = !variable.streaming + + const currentStreamingVars = newVariables[selectedDatasetId].streaming_variables.filter(v => v !== varName) + if (variable.streaming) { + currentStreamingVars.push(varName) + } + newVariables[selectedDatasetId].streaming_variables = currentStreamingVars + + onVariablesUpdate(newVariables) + } + + if (!selectedDataset) { + return ( + + + Select a dataset from the overview above to manage its variables. + + ) + } + + return ( + + + + + + Variables for "{selectedDataset.name}" + + ID: {selectedDatasetId} • {Object.keys(datasetVariables).length} variables • {streamingVariables.length} streaming + + + + + + + + {Object.keys(datasetVariables).length === 0 ? ( + + No variables configured for this dataset. + Click "Add Variable" to get started. + + ) : ( + + {Object.entries(datasetVariables).map(([varName, variable]) => ( + handleEditVariable(varName)} + onDelete={() => handleDeleteVariable(varName)} + onToggleStreaming={() => toggleVariableStreaming(varName)} + /> + ))} + + )} + + + + { + onClose() + setEditingVariable(null) + }} + variable={editingVariable} + onSave={handleSaveVariable} + /> + + ) +} + +function VariableCard({ name, variable, isStreaming, onEdit, onDelete, onToggleStreaming }) { + const cardBg = useColorModeValue('gray.50', 'gray.600') + const borderColor = useColorModeValue('gray.200', 'gray.500') + + const getAreaLabel = (area) => PLC_AREAS.find(a => a.value === area)?.label || area.toUpperCase() + const getTypeLabel = (type) => DATA_TYPES.find(t => t.value === type)?.label || type.toUpperCase() + + return ( + + + + + {name} + + + + + + + + Area: {getAreaLabel(variable.area)} + Type: {getTypeLabel(variable.type)} + + {variable.area === 'db' && ( + DB: {variable.db} + )} + Offset: {variable.offset} + + {variable.bit !== undefined && ( + Bit: {variable.bit} + )} + + + + + {isStreaming ? '📡 Streaming' : '📴 Not streaming'} + + + + + + + ) +} + +function VariableEditModal({ isOpen, onClose, variable, onSave }) { + const [formData, setFormData] = useState(variable || {}) + + React.useEffect(() => { + setFormData(variable || {}) + }, [variable]) + + const handleSubmit = (e) => { + e.preventDefault() + if (!formData.name || !formData.area || !formData.type) { + return + } + onSave(formData) + } + + const isEditing = variable?.name + + return ( + + + + + {isEditing ? `Edit Variable: ${variable.name}` : 'Add New Variable'} + + +
+ + + + Variable Name + setFormData({ ...formData, name: e.target.value })} + placeholder="e.g., Temperature_Tank_1" + /> + + + + + Memory Area + + + + + Data Type + + + + + + {formData.area === 'db' && ( + + DB Number + setFormData({ ...formData, db: num })} + min={1} max={9999} + > + + + + + + + + )} + + + Offset + setFormData({ ...formData, offset: num })} + min={0} max={8191} + > + + + + + + + + + {['e', 'a', 'mb'].includes(formData.area) && ( + + Bit Position + setFormData({ ...formData, bit: num })} + min={0} max={7} + > + + + + + + + + )} + + + + setFormData({ ...formData, streaming: e.target.checked })} + > + Enable streaming to PlotJuggler + + + + + + + + + +
+
+
+ ) +} diff --git a/frontend/src/components/rjsf/PlcWidgets.jsx b/frontend/src/components/rjsf/PlcWidgets.jsx new file mode 100644 index 0000000..0f3649b --- /dev/null +++ b/frontend/src/components/rjsf/PlcWidgets.jsx @@ -0,0 +1,290 @@ +import React from 'react' +import { + FormControl, FormLabel, FormHelperText, Select, NumberInput, NumberInputField, + NumberInputStepper, NumberIncrementStepper, NumberDecrementStepper, Checkbox, Input, + HStack, VStack, Badge, Text, Box, Icon, useColorModeValue +} from '@chakra-ui/react' + +// Widget for PLC Memory Area selection with visual indicators +export function PlcAreaWidget(props) { + const { id, options, value, required, disabled, readonly, label, onChange, onBlur, onFocus, rawErrors = [] } = props + + const borderColor = useColorModeValue('gray.300', 'gray.600') + const focusBorderColor = useColorModeValue('blue.500', 'blue.300') + + const areaIcons = { + 'db': '🗃️', + 'mw': '📊', + 'm': '💾', + 'pew': '📥', + 'pe': '📥', + 'paw': '📤', + 'pa': '📤', + 'e': '🔌', + 'a': '🔌', + 'mb': '💾' + } + + const areaDescriptions = { + 'db': 'Data Block - Structured data storage', + 'mw': 'Memory Word - 16-bit memory words', + 'm': 'Memory - Bit-addressable memory', + 'pew': 'Process Input Word - 16-bit input words', + 'pe': 'Process Input - Bit-addressable inputs', + 'paw': 'Process Output Word - 16-bit output words', + 'pa': 'Process Output - Bit-addressable outputs', + 'e': 'Input - Digital inputs', + 'a': 'Output - Digital outputs', + 'mb': 'Memory Byte - 8-bit memory bytes' + } + + return ( + 0}> + {label && {label}} + + {value && areaDescriptions[value] && ( + + {areaDescriptions[value]} + + )} + {rawErrors.length > 0 && ( + {rawErrors[0]} + )} + + ) +} + +// Widget for PLC Data Type selection with size information +export function PlcDataTypeWidget(props) { + const { id, options, value, required, disabled, readonly, label, onChange, onBlur, onFocus, rawErrors = [] } = props + + const borderColor = useColorModeValue('gray.300', 'gray.600') + const focusBorderColor = useColorModeValue('blue.500', 'blue.300') + + const typeSizes = { + 'real': '32-bit', + 'int': '16-bit', + 'bool': '1-bit', + 'dint': '32-bit', + 'word': '16-bit', + 'byte': '8-bit', + 'uint': '16-bit', + 'udint': '32-bit', + 'sint': '8-bit', + 'usint': '8-bit' + } + + const typeColors = { + 'real': 'blue', + 'int': 'green', + 'bool': 'purple', + 'dint': 'green', + 'word': 'orange', + 'byte': 'orange', + 'uint': 'green', + 'udint': 'green', + 'sint': 'green', + 'usint': 'green' + } + + return ( + 0}> + {label && {label}} + + {value && typeSizes[value] && ( + + + {typeSizes[value]} + + + {value === 'real' ? 'Floating point number' : + value === 'bool' ? 'Boolean true/false' : + value.includes('int') ? 'Integer number' : + 'Binary data'} + + + )} + {rawErrors.length > 0 && ( + {rawErrors[0]} + )} + + ) +} + +// Enhanced number input widget with PLC-specific validations +export function PlcNumberWidget(props) { + const { id, value, required, disabled, readonly, label, onChange, onBlur, onFocus, rawErrors = [], schema } = props + + const min = schema.minimum + const max = schema.maximum + const step = schema.multipleOf || 1 + + return ( + 0}> + {label && {label}} + onChange(isNaN(num) ? undefined : num)} + onBlur={onBlur && (() => onBlur(id, value))} + onFocus={onFocus && (() => onFocus(id, value))} + min={min} + max={max} + step={step} + precision={schema.type === 'integer' ? 0 : 2} + > + + + + + + + {(min !== undefined || max !== undefined) && ( + + Valid range: {min ?? '-∞'} to {max ?? '∞'} + + )} + {rawErrors.length > 0 && ( + {rawErrors[0]} + )} + + ) +} + +// Enhanced checkbox widget for streaming configuration +export function PlcStreamingWidget(props) { + const { id, value, required, disabled, readonly, label, onChange, onBlur, onFocus, rawErrors = [] } = props + + const streamingBg = useColorModeValue('green.50', 'green.900') + const inactiveBg = useColorModeValue('gray.50', 'gray.700') + + return ( + 0}> + + + + + + {value ? '📡' : '📴'} Real-time Streaming + + + {value ? 'Enabled' : 'Disabled'} + + + + {value + ? 'This variable will be streamed to PlotJuggler for real-time visualization' + : 'Enable to stream this variable to PlotJuggler in real-time' + } + + + onChange(e.target.checked)} + onBlur={onBlur && (() => onBlur(id, value))} + onFocus={onFocus && (() => onFocus(id, value))} + colorScheme="green" + size="lg" + /> + + + {rawErrors.length > 0 && ( + {rawErrors[0]} + )} + + ) +} + +// Variable name input with validation and suggestions +export function PlcVariableNameWidget(props) { + const { id, value, required, disabled, readonly, label, onChange, onBlur, onFocus, rawErrors = [] } = props + + const isValid = value && /^[a-zA-Z][a-zA-Z0-9_]*$/.test(value) + const borderColor = useColorModeValue('gray.300', 'gray.600') + const errorBorderColor = useColorModeValue('red.300', 'red.500') + + const suggestions = [ + 'Temperature_Tank_1', + 'Pressure_Line_A', + 'Motor_Speed_RPM', + 'Valve_Position', + 'Flow_Rate_LPM', + 'Level_Percentage' + ] + + return ( + 0}> + {label && {label}} + onChange(e.target.value === '' ? undefined : e.target.value)} + onBlur={onBlur && ((e) => onBlur(id, e.target.value))} + onFocus={onFocus && ((e) => onFocus(id, e.target.value))} + placeholder="e.g., Temperature_Tank_1" + borderColor={rawErrors.length > 0 ? errorBorderColor : borderColor} + _focus={{ borderColor: rawErrors.length > 0 ? errorBorderColor : 'blue.500' }} + /> + + {!value ? ( + <>Use descriptive names like: {suggestions.slice(0, 2).join(', ')} + ) : !isValid ? ( + + ⚠️ Variable names should start with a letter and contain only letters, numbers, and underscores + + ) : ( + ✅ Valid variable name + )} + + {rawErrors.length > 0 && ( + {rawErrors[0]} + )} + + ) +} + +// Widget map to register custom widgets +export const plcWidgets = { + PlcAreaWidget, + PlcDataTypeWidget, + PlcNumberWidget, + PlcStreamingWidget, + PlcVariableNameWidget +} diff --git a/frontend/src/components/rjsf/widgets.jsx b/frontend/src/components/rjsf/widgets.jsx index e4b3b3c..ae2ba1a 100644 --- a/frontend/src/components/rjsf/widgets.jsx +++ b/frontend/src/components/rjsf/widgets.jsx @@ -1,96 +1,146 @@ -import React from 'react'; -// Deprecated: Bootstrap widgets were used before migrating to Chakra UI theme -// Legacy Bootstrap widgets no longer used after migrating to Chakra UI +import React from 'react' +import { + FormControl, FormLabel, FormHelperText, Input, Textarea, Select, Checkbox, + NumberInput, NumberInputField, NumberInputStepper, NumberIncrementStepper, NumberDecrementStepper, + useColorModeValue +} from '@chakra-ui/react' +import { + PlcAreaWidget, + PlcDataTypeWidget, + PlcNumberWidget, + PlcStreamingWidget, + PlcVariableNameWidget +} from './PlcWidgets.jsx' -export const TextWidget = ({ id, placeholder, required, readonly, disabled, label, value, onChange, onBlur, onFocus, autofocus, options, schema }) => ( - - {schema?.title && {schema.title}} - onChange(event.target.value)} - onBlur={() => onBlur && onBlur(id, value)} - onFocus={() => onFocus && onFocus(id, value)} - /> - -); +// Enhanced Chakra UI widgets for RJSF forms -export const UpDownWidget = ({ id, placeholder, required, readonly, disabled, label, value, onChange, onBlur, onFocus, autofocus, options, schema }) => ( - - {schema?.title && {schema.title}} - onChange(event.target.value === '' ? undefined : Number(event.target.value))} - onBlur={() => onBlur && onBlur(id, value)} - onFocus={() => onFocus && onFocus(id, value)} - /> - -); +export const TextWidget = ({ id, placeholder, required, readonly, disabled, label, value, onChange, onBlur, onFocus, rawErrors = [] }) => { + const borderColor = useColorModeValue('gray.300', 'gray.600') + const focusBorderColor = useColorModeValue('blue.500', 'blue.300') -export const TextareaWidget = ({ id, placeholder, required, readonly, disabled, label, value, onChange, onBlur, onFocus, autofocus, options, schema }) => ( - - {schema?.title && {schema.title}} - onChange(event.target.value)} - onBlur={() => onBlur && onBlur(id, value)} - onFocus={() => onFocus && onFocus(id, value)} - /> - -); - -export const SelectWidget = ({ id, required, readonly, disabled, label, value, onChange, options, schema }) => { - const enumOptions = options?.enumOptions || []; return ( - - {schema?.title && {schema.title}} - 0}> + {label && {label}} + onChange(event.target.value)} + placeholder={placeholder} + value={value || ''} + onChange={(e) => onChange(e.target.value === '' ? undefined : e.target.value)} + onBlur={onBlur && ((e) => onBlur(id, e.target.value))} + onFocus={onFocus && ((e) => onFocus(id, e.target.value))} + borderColor={borderColor} + _focus={{ borderColor: focusBorderColor }} + /> + {rawErrors.length > 0 && ( + {rawErrors[0]} + )} + + ) +} + +export const UpDownWidget = ({ id, placeholder, required, readonly, disabled, label, value, onChange, onBlur, onFocus, rawErrors = [], schema }) => { + const min = schema?.minimum + const max = schema?.maximum + const step = schema?.multipleOf || 1 + + return ( + 0}> + {label && {label}} + onChange(isNaN(num) ? undefined : num)} + onBlur={onBlur && (() => onBlur(id, value))} + onFocus={onFocus && (() => onFocus(id, value))} + min={min} + max={max} + step={step} + precision={schema?.type === 'integer' ? 0 : 2} > - {enumOptions.map((opt) => ( - + {rawErrors.length > 0 && ( + {rawErrors[0]} + )} + + ) +} + +export const TextareaWidget = ({ id, placeholder, required, readonly, disabled, label, value, onChange, onBlur, onFocus, rawErrors = [], options }) => { + const borderColor = useColorModeValue('gray.300', 'gray.600') + const focusBorderColor = useColorModeValue('blue.500', 'blue.300') + + return ( + 0}> + {label && {label}} +