diff --git a/.doc/MemoriaDeEvolucion.md b/.doc/MemoriaDeEvolucion.md index 7ecb28f..0b376b1 100644 --- a/.doc/MemoriaDeEvolucion.md +++ b/.doc/MemoriaDeEvolucion.md @@ -1,17 +1,80 @@ -Solicitud (resumen): "En el Dashboard, quiero que la configuración del PLC esté arriba de los datasets (mencionado en @App.jsx)." +# Memoria de Evolución del Proyecto -Cambio aplicado: -- Se ajustó el orden de renderizado de secciones en `frontend/src/pages/Dashboard.jsx` para priorizar `plc` por encima de `dataset-definitions` y `dataset-variables`. -- Implementado con `useMemo` que ordena los IDs usando `preferredOrder = ['plc', 'dataset-definitions', 'dataset-variables']`. +## Sistema de Tablas Editables para Datasets y Plots (10/08/2025) -Motivación/decisión: -- Priorizar la configuración del PLC en la vista Dashboard para que quede por encima de los datasets. -- Comentarios y variables del código en inglés. +### Solicitud del Usuario +El usuario solicitó cambiar la presentación de los datasets de un formato de formulario a un formato de tabla para poder leer/escribir campos directamente. Quería: +- Tablas con botones de eliminar para cada fila +- Control reutilizable que tome schema + data + uiSchema y genere tabla con botones +- Sistema maestro-detalle: tabla de datasets -> selector -> tabla de variables del dataset +- Mismo patrón para plots y sus variables +- Todo integrado en Dashboard.jsx -Impacto: -- La sección de configuración del PLC se muestra primero en el Dashboard. +### Implementación Realizada -Archivos afectados: -- `frontend/src/pages/Dashboard.jsx` +**1. Componente EditableTable.jsx** +- Componente reutilizable que convierte schemas JSON en tablas editables +- Maneja tanto arrays como objetos con keys +- Modales para agregar/editar items +- Soporte para widgets: text, select, checkbox, number/updown +- Botones de eliminar por fila -Fecha: 2025-08-12 \ No newline at end of file +**2. DatasetTableManager.jsx** +- Gestiona datasets y variables en sistema maestro-detalle +- Tabla de dataset definitions con CRUD completo +- Selector de dataset que carga tabla de variables correspondiente +- Variables como objetos con keys (name, area, db, offset, type, streaming) +- Integración con APIs de configuración + +**3. PlotTableManager.jsx** +- Similar a datasets pero para plots +- Plot definitions con propiedades como name, time_window, y_min/max, triggers +- Variables como array simple de strings +- Componente especializado PlotVariablesTable para arrays de strings + +**4. Modificación Dashboard.jsx** +- Reemplazó secciones dataset-definitions/variables con DatasetTableManager +- Reemplazó secciones plot-definitions/variables con PlotTableManager +- Mantiene PLC config como formulario RJSF original +- Organización clara por tipo de gestión + +### Decisiones Técnicas +- EditableTable maneja conversión automática entre arrays y objetos con keys +- Variables de datasets son objetos complejos, variables de plots son strings simples +- Guardado automático al hacer cambios con feedback visual +- Modales para evitar edición inline compleja +- Reutilización del sistema de schemas existente + +### Corrección: Sistema de Formularios de Una Sola Fila (10/08/2025) + +**Problema detectado**: Usuario reportó "No schema properties defined for this table" y solicitó formularios de una sola fila basados en schemas-ui. + +**Causa**: Acceso incorrecto a propiedades del schema y enfoque muy complejo de tabla tradicional. + +**Solución implementada**: + +**1. FormTable.jsx - Componente corregido** +- Usa schemas RJSF directamente con additionalProperties +- Muestra cada objeto como un formulario completo en una tarjeta +- Modo readonly por defecto, modo edit al hacer clic en editar +- Integración completa con LayoutObjectFieldTemplate y widgets + +**2. DatasetFormManager.jsx y PlotFormManager.jsx** +- Acceso correcto a schemas: `schema.properties.datasets` +- Logs de consola para debugging de carga de schemas +- Guardado que preserva estructura completa del config +- Sistema maestro-detalle funcional + +**3. Presentación mejorada** +- Cada dataset/plot/variable se muestra como un formulario completo +- Botones de editar/eliminar por tarjeta +- Formularios que respetan completamente los UI schemas +- Generación automática de IDs para nuevos elementos + +### Beneficios +- Formularios de una sola fila como solicitó el usuario +- Usa completamente los schemas-ui existentes +- CRUD completo desde interfaz gráfica +- Debugging mejorado con logs de consola +- Mantiene integridad completa con schemas JSON existentes +- Patrón reutilizable para futuras entidades diff --git a/application_events.json b/application_events.json index 4555e5e..e1f2bb9 100644 --- a/application_events.json +++ b/application_events.json @@ -9371,8 +9371,22 @@ "event_type": "application_started", "message": "Application initialization completed successfully", "details": {} + }, + { + "timestamp": "2025-08-12T19:46:18.227132", + "level": "info", + "event_type": "application_started", + "message": "Application initialization completed successfully", + "details": {} + }, + { + "timestamp": "2025-08-12T20:49:00.212504", + "level": "info", + "event_type": "application_started", + "message": "Application initialization completed successfully", + "details": {} } ], - "last_updated": "2025-08-12T19:08:01.450637", - "total_entries": 878 + "last_updated": "2025-08-12T20:49:00.212504", + "total_entries": 880 } \ No newline at end of file diff --git a/config/schema/dataset-definitions.schema.json b/config/schema/dataset-definitions.schema.json index afacbdb..3520137 100644 --- a/config/schema/dataset-definitions.schema.json +++ b/config/schema/dataset-definitions.schema.json @@ -1,99 +1,99 @@ { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "dataset-definitions.schema.json", - "title": "Dataset Definitions", - "description": "Schema for dataset definitions (metadata only, no variables)", - "type": "object", - "additionalProperties": false, - "properties": { - "datasets": { - "type": "object", - "title": "Dataset Definitions", - "additionalProperties": { - "type": "object", - "properties": { - "name": { - "type": "string", - "title": "Dataset Name", - "description": "Human-readable name of the dataset", - "minLength": 1, - "maxLength": 60 - }, - "prefix": { - "type": "string", - "title": "CSV Prefix", - "description": "Prefix for CSV files", - "pattern": "^[a-zA-Z0-9_-]+$", - "minLength": 1, - "maxLength": 20 - }, - "sampling_interval": { - "type": [ - "number", - "null" - ], - "title": "Sampling interval (s)", - "description": "Leave empty to use the global interval", - "minimum": 0.01, - "maximum": 10 - }, - "enabled": { - "type": "boolean", - "title": "Dataset Enabled", - "default": false, - "enum": [ - true, - false - ], - "options": { - "enum_titles": [ - "Activate", - "Deactivate" - ] - } - }, - "created": { - "type": [ - "string", - "null" - ], - "title": "Created" - } - }, - "required": [ - "name", - "prefix" - ] - } - }, - "active_datasets": { - "type": "array", - "title": "Active Datasets", - "items": { - "type": "string" - }, - "default": [] - }, - "current_dataset_id": { - "type": [ - "string", - "null" - ], - "title": "Current Dataset Id" - }, - "version": { - "type": "string", - "title": "Version" - }, - "last_update": { - "type": [ - "string", - "null" - ], - "title": "Last Update" - } + "$id": "dataset-definitions.schema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "description": "Schema for dataset definitions (metadata only, no variables)", + "properties": { + "active_datasets": { + "default": [], + "items": { + "type": "string" + }, + "title": "Active Datasets", + "type": "array" }, - "required": [ - "datasets" - ] + "current_dataset_id": { + "title": "Current Dataset Id", + "type": [ + "string", + "null" + ] + }, + "datasets": { + "additionalProperties": { + "properties": { + "created": { + "title": "Created", + "type": [ + "string", + "null" + ] + }, + "enabled": { + "default": false, + "enum": [ + true, + false + ], + "options": { + "enum_titles": [ + "Activate", + "Deactivate" + ] + }, + "title": "Dataset Enabled", + "type": "boolean" + }, + "name": { + "description": "Human-readable name of the dataset", + "maxLength": 60, + "minLength": 1, + "title": "Dataset Name", + "type": "string" + }, + "prefix": { + "description": "Prefix for CSV files", + "maxLength": 20, + "minLength": 1, + "pattern": "^[a-zA-Z0-9_-]+$", + "title": "CSV Prefix", + "type": "string" + }, + "sampling_interval": { + "description": "Leave empty to use the global interval", + "maximum": 10, + "minimum": 0.01, + "title": "Sampling interval (s)", + "type": [ + "number", + "null" + ] + } + }, + "required": [ + "name", + "prefix" + ], + "type": "object" + }, + "title": "Dataset Definitions", + "type": "object" + }, + "last_update": { + "title": "Last Update", + "type": [ + "string", + "null" + ] + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "datasets" + ], + "title": "Dataset Definitions", + "type": "object" } \ 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 136800d..b5819ee 100644 --- a/config/schema/ui/dataset-definitions.uischema.json +++ b/config/schema/ui/dataset-definitions.uischema.json @@ -1,68 +1,93 @@ { - "datasets": { - "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": "e.g., Temperature Sensors, Production Line A", - "ui:help": "Human-readable name for this dataset" - }, - "prefix": { - "ui:placeholder": "e.g., temp, line_a, sensors", - "ui:help": "Short prefix for CSV filenames (alphanumeric, underscore, dash only)" - }, - "enabled": { - "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", + "active_datasets": { + "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", + "ui:widget": "checkboxes", + "ui:column": 3 + }, + "current_dataset_id": { + "ui:description": "🎯 Currently selected dataset for variable editing", + "ui:help": "This determines which dataset is shown by default in the interface", + "ui:widget": "select", + "ui:column": 3 + }, + "datasets": { + "additionalProperties": { + "created": { + "ui:help": "Timestamp when this dataset was created", "ui:readonly": true, - "ui:help": "Configuration schema version" + "ui:widget": "text" + }, + "enabled": { + "ui:help": "When enabled, this dataset will be actively sampled and recorded", + "ui:widget": "checkbox" + }, + "name": { + "ui:help": "Human-readable name for this dataset", + "ui:placeholder": "e.g., Temperature Sensors, Production Line A" + }, + "prefix": { + "ui:help": "Short prefix for CSV filenames (alphanumeric, underscore, dash only)", + "ui:placeholder": "e.g., temp, line_a, sensors" + }, + "sampling_interval": { + "ui:help": "Custom sampling interval in seconds (0.01-10s). Leave empty to use the global PLC sampling interval.", + "ui:placeholder": "Leave empty to use global interval", + "ui:widget": "updown" + }, + "ui:order": [ + "name", + "prefix", + "enabled", + "sampling_interval", + "created" + ] }, - "last_update": { - "ui:widget": "text", - "ui:readonly": true, - "ui:help": "Timestamp of last configuration update" + "ui:description": "📊 Configure dataset metadata: names, CSV file prefixes, sampling intervals, and activation status", + "ui:options": { + "addable": true, + "orderable": true, + "removable": true }, - "ui:order": [ - "datasets", - "active_datasets", - "current_dataset_id", - "version", - "last_update" + "ui:column": 3 + }, + "last_update": { + "ui:help": "Timestamp of last configuration update", + "ui:readonly": true, + "ui:widget": "text", + "ui:column": 3 + }, + "ui:order": [ + "datasets", + "active_datasets", + "current_dataset_id", + "version", + "last_update" + ], + "version": { + "ui:help": "Configuration schema version", + "ui:readonly": true, + "ui:widget": "text", + "ui:column": 3 + }, + "ui:layout": [ + [ + { + "name": "active_datasets", + "width": 3 + }, + { + "name": "current_dataset_id", + "width": 3 + }, + { + "name": "last_update", + "width": 3 + }, + { + "name": "version", + "width": 3 + } ] + ] } \ No newline at end of file diff --git a/frontend/src/components/DatasetFormManager.jsx b/frontend/src/components/DatasetFormManager.jsx new file mode 100644 index 0000000..72df130 --- /dev/null +++ b/frontend/src/components/DatasetFormManager.jsx @@ -0,0 +1,231 @@ +import React, { useState, useEffect, useMemo } from 'react' +import { + Box, + VStack, + HStack, + Text, + Select, + Card, + CardBody, + CardHeader, + Heading, + Alert, + AlertIcon, + useColorModeValue, + Divider +} from '@chakra-ui/react' +import FormTable from './FormTable.jsx' +import { getSchema, readConfig, writeConfig } from '../services/api.js' + +/** + * DatasetFormManager - Gestiona datasets y variables usando FormTable + */ +export default function DatasetFormManager() { + const [datasets, setDatasets] = useState({}) + const [datasetVariables, setDatasetVariables] = useState({}) + const [selectedDatasetId, setSelectedDatasetId] = useState('') + + const [datasetSchema, setDatasetSchema] = useState(null) + const [datasetUiSchema, setDatasetUiSchema] = useState({}) + const [variableSchema, setVariableSchema] = useState(null) + const [variableUiSchema, setVariableUiSchema] = useState({}) + + const [loading, setLoading] = useState(true) + const [message, setMessage] = useState('') + + const muted = useColorModeValue('gray.600', 'gray.300') + + useEffect(() => { + loadData() + }, []) + + const loadData = async () => { + setLoading(true) + try { + // Cargar schemas + const [datasetSchemaResp, variableSchemaResp] = await Promise.all([ + getSchema('dataset-definitions'), + getSchema('dataset-variables') + ]) + + console.log('Dataset schema response:', datasetSchemaResp) + console.log('Variable schema response:', variableSchemaResp) + + // Cargar datos + const [datasetDataResp, variableDataResp] = await Promise.all([ + readConfig('dataset-definitions'), + readConfig('dataset-variables') + ]) + + console.log('Dataset data response:', datasetDataResp) + console.log('Variable data response:', variableDataResp) + + // Extraer schemas correctamente + setDatasetSchema(datasetSchemaResp.schema?.properties?.datasets) + setDatasetUiSchema(datasetSchemaResp.ui_schema?.datasets || {}) + + setVariableSchema(variableSchemaResp.schema?.properties?.dataset_variables?.additionalProperties?.properties?.variables) + setVariableUiSchema(variableSchemaResp.ui_schema?.dataset_variables?.additionalProperties?.variables || {}) + + setDatasets(datasetDataResp.data?.datasets || {}) + setDatasetVariables(variableDataResp.data?.dataset_variables || {}) + + // Seleccionar primer dataset + const datasetIds = Object.keys(datasetDataResp.data?.datasets || {}) + if (datasetIds.length > 0 && !selectedDatasetId) { + setSelectedDatasetId(datasetIds[0]) + } + + } catch (error) { + console.error('Error loading data:', error) + setMessage(`Error loading data: ${error.message}`) + } finally { + setLoading(false) + } + } + + const saveDatasets = async (newDatasets) => { + try { + const currentConfig = await readConfig('dataset-definitions') + const saveData = { + ...currentConfig.data, + datasets: newDatasets, + last_update: new Date().toISOString() + } + + await writeConfig('dataset-definitions', saveData) + setDatasets(newDatasets) + setMessage('Datasets saved successfully') + setTimeout(() => setMessage(''), 3000) + } catch (error) { + console.error('Error saving datasets:', error) + setMessage(`Error saving datasets: ${error.message}`) + } + } + + const saveDatasetVariables = async (newVariables) => { + try { + const currentConfig = await readConfig('dataset-variables') + const updatedDatasetVariables = { + ...datasetVariables, + [selectedDatasetId]: { + variables: newVariables, + streaming_variables: Object.keys(newVariables).filter(key => newVariables[key]?.streaming) + } + } + + const saveData = { + ...currentConfig.data, + dataset_variables: updatedDatasetVariables, + last_update: new Date().toISOString() + } + + await writeConfig('dataset-variables', saveData) + setDatasetVariables(updatedDatasetVariables) + setMessage('Dataset variables saved successfully') + setTimeout(() => setMessage(''), 3000) + } catch (error) { + console.error('Error saving variables:', error) + setMessage(`Error saving variables: ${error.message}`) + } + } + + const currentDatasetVariables = useMemo(() => { + return selectedDatasetId && datasetVariables[selectedDatasetId] + ? datasetVariables[selectedDatasetId].variables || {} + : {} + }, [selectedDatasetId, datasetVariables]) + + const datasetOptions = Object.entries(datasets).map(([id, dataset]) => ({ + value: id, + label: `${dataset.name || id} (${id})` + })) + + if (loading) { + return Loading datasets... + } + + return ( + + {message && ( + + + {message} + + )} + + {/* Datasets */} + + + 📊 Dataset Definitions + + + {datasetSchema ? ( + + ) : ( + + + Dataset schema not available + + )} + + + + + + {/* Variables del Dataset */} + + + + 🔧 Dataset Variables + + Dataset: + + + + + + {!selectedDatasetId ? ( + + + Select a dataset to manage its variables + + ) : variableSchema ? ( + + ) : ( + + + Variable schema not available + + )} + + + + ) +} diff --git a/frontend/src/components/DatasetTableManager.jsx b/frontend/src/components/DatasetTableManager.jsx new file mode 100644 index 0000000..3f938fa --- /dev/null +++ b/frontend/src/components/DatasetTableManager.jsx @@ -0,0 +1,264 @@ +import React, { useState, useEffect, useMemo } from 'react' +import { + Box, + VStack, + HStack, + Text, + Select, + Button, + Card, + CardBody, + CardHeader, + Heading, + Alert, + AlertIcon, + useColorModeValue, + Divider +} from '@chakra-ui/react' +import EditableTable from './EditableTable.jsx' +import { getSchema, readConfig, writeConfig } from '../services/api.js' + +/** + * DatasetTableManager - Componente para gestionar datasets y sus variables + * Muestra tabla de datasets y tabla de variables del dataset seleccionado + */ +export default function DatasetTableManager() { + const [datasets, setDatasets] = useState({}) + const [datasetVariables, setDatasetVariables] = useState({}) + const [selectedDatasetId, setSelectedDatasetId] = useState('') + + const [datasetSchema, setDatasetSchema] = useState(null) + const [datasetUiSchema, setDatasetUiSchema] = useState({}) + const [variableSchema, setVariableSchema] = useState(null) + const [variableUiSchema, setVariableUiSchema] = useState({}) + + const [loading, setLoading] = useState(true) + const [saving, setSaving] = useState(false) + const [message, setMessage] = useState('') + + const muted = useColorModeValue('gray.600', 'gray.300') + + // Cargar schemas y datos al montar + useEffect(() => { + loadData() + }, []) + + const loadData = async () => { + setLoading(true) + try { + // Cargar schemas + const [datasetSchemaResp, variableSchemaResp] = await Promise.all([ + getSchema('dataset-definitions'), + getSchema('dataset-variables') + ]) + + // Cargar datos de configuración + const [datasetDataResp, variableDataResp] = await Promise.all([ + readConfig('dataset-definitions'), + readConfig('dataset-variables') + ]) + + setDatasetSchema(datasetSchemaResp.schema?.properties?.datasets) + setDatasetUiSchema(datasetSchemaResp.ui_schema?.datasets || {}) + + setVariableSchema(variableSchemaResp.schema?.properties?.dataset_variables?.additionalProperties?.properties?.variables) + setVariableUiSchema(variableSchemaResp.ui_schema?.dataset_variables?.additionalProperties?.variables || {}) + + setDatasets(datasetDataResp.data?.datasets || {}) + setDatasetVariables(datasetDataResp.data?.dataset_variables || {}) + + // Seleccionar el primer dataset si existe + const datasetIds = Object.keys(datasetDataResp.data?.datasets || {}) + if (datasetIds.length > 0 && !selectedDatasetId) { + setSelectedDatasetId(datasetIds[0]) + } + + } catch (error) { + setMessage(`Error loading data: ${error.message}`) + } finally { + setLoading(false) + } + } + + const saveDatasets = async (newDatasets) => { + setSaving(true) + setMessage('') + try { + // Construir el objeto completo para guardar + const saveData = { + datasets: newDatasets, + // Mantener otros campos existentes + active_datasets: [], // Esto se puede gestionar por separado + current_dataset_id: selectedDatasetId, + version: "1.0", + last_update: new Date().toISOString() + } + + await writeConfig('dataset-definitions', saveData) + setDatasets(newDatasets) + setMessage('Datasets saved successfully') + } catch (error) { + setMessage(`Error saving datasets: ${error.message}`) + } finally { + setSaving(false) + } + } + + const saveDatasetVariables = async (newVariables) => { + setSaving(true) + setMessage('') + try { + const updatedDatasetVariables = { + ...datasetVariables, + [selectedDatasetId]: { + variables: newVariables, + streaming_variables: [] // Esto se puede calcular automáticamente + } + } + + const saveData = { + dataset_variables: updatedDatasetVariables, + version: "1.0", + last_update: new Date().toISOString() + } + + await writeConfig('dataset-variables', saveData) + setDatasetVariables(updatedDatasetVariables) + setMessage('Dataset variables saved successfully') + } catch (error) { + setMessage(`Error saving variables: ${error.message}`) + } finally { + setSaving(false) + } + } + + // Convertir datos de datasets para el componente EditableTable + const datasetsForTable = useMemo(() => { + return Object.entries(datasets).map(([id, data]) => ({ + id, + ...data + })) + }, [datasets]) + + // Convertir variables del dataset seleccionado para el componente EditableTable + const variablesForTable = useMemo(() => { + if (!selectedDatasetId || !datasetVariables[selectedDatasetId]) { + return [] + } + + const variables = datasetVariables[selectedDatasetId].variables || {} + return Object.entries(variables).map(([name, data]) => ({ + name, + ...data + })) + }, [selectedDatasetId, datasetVariables]) + + const datasetOptions = Object.entries(datasets).map(([id, dataset]) => ({ + value: id, + label: `${dataset.name} (${id})` + })) + + if (loading) { + return Loading datasets... + } + + return ( + + {message && ( + + + {message} + + )} + + {/* Tabla de Datasets */} + + + 📊 Datasets + + + {datasetSchema ? ( + { + // Convertir de array a objeto con keys + const newDatasets = {} + newData.forEach(item => { + const { id, ...rest } = item + newDatasets[id] = rest + }) + saveDatasets(newDatasets) + }} + title="Dataset Definitions" + keyField="id" + /> + ) : ( + + + No dataset schema available + + )} + + + + + + {/* Selector de Dataset y Tabla de Variables */} + + + + 🔧 Dataset Variables + + Dataset: + + + + + + {!selectedDatasetId ? ( + + + Select a dataset to manage its variables + + ) : variableSchema ? ( + { + // Convertir de array a objeto con keys + const newVariables = {} + newData.forEach(item => { + const { name, ...rest } = item + newVariables[name] = rest + }) + saveDatasetVariables(newVariables) + }} + title={`Variables for dataset: ${datasets[selectedDatasetId]?.name || selectedDatasetId}`} + keyField="name" + /> + ) : ( + + + No variable schema available + + )} + + + + ) +} diff --git a/frontend/src/components/EditableTable.jsx b/frontend/src/components/EditableTable.jsx new file mode 100644 index 0000000..730cd1d --- /dev/null +++ b/frontend/src/components/EditableTable.jsx @@ -0,0 +1,355 @@ +import React, { useState, useMemo } from 'react' +import { + Box, + Table, + Thead, + Tbody, + Tr, + Th, + Td, + Button, + Input, + Select, + Checkbox, + IconButton, + HStack, + VStack, + Text, + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalBody, + ModalFooter, + ModalCloseButton, + useDisclosure, + Alert, + AlertIcon, + NumberInput, + NumberInputField, + NumberInputStepper, + NumberIncrementStepper, + NumberDecrementStepper, + useColorModeValue +} from '@chakra-ui/react' +import { AddIcon, DeleteIcon, EditIcon } from '@chakra-ui/icons' + +/** + * EditableTable - Componente reutilizable para editar arrays de objetos en forma de tabla + * + * @param {Object} schema - JSON Schema que define la estructura de los objetos + * @param {Object} uiSchema - UI Schema con widgets y configuraciones + * @param {Array} data - Array de objetos a editar + * @param {Function} onChange - Callback que se ejecuta al cambiar los datos: (newData) => void + * @param {string} title - Título de la tabla + * @param {string} keyField - Campo que actúa como clave única (ej: 'id', 'name') + */ +export default function EditableTable({ + schema, + uiSchema = {}, + data = [], + onChange, + title = "Data", + keyField = "id", + allowAdd = true, + allowEdit = true, + allowDelete = true +}) { + const [editingKey, setEditingKey] = useState(null) + const [editingData, setEditingData] = useState({}) + const [newItem, setNewItem] = useState({}) + const { isOpen: isAddOpen, onOpen: onAddOpen, onClose: onAddClose } = useDisclosure() + const { isOpen: isEditOpen, onOpen: onEditOpen, onClose: onEditClose } = useDisclosure() + + const muted = useColorModeValue('gray.600', 'gray.300') + const borderColor = useColorModeValue('gray.200', 'gray.600') + + // Extraer propiedades del schema + const properties = useMemo(() => { + if (!schema?.properties) return {} + return schema.properties + }, [schema]) + + const propertyNames = useMemo(() => { + return Object.keys(properties) + }, [properties]) + + // Convertir array de objetos a formato objeto con keys + const dataAsObject = useMemo(() => { + if (Array.isArray(data)) { + const result = {} + data.forEach((item, index) => { + const key = item[keyField] || `item_${index}` + result[key] = item + }) + return result + } + return data || {} + }, [data, keyField]) + + const dataKeys = Object.keys(dataAsObject) + + const handleDelete = (key) => { + const newDataObj = { ...dataAsObject } + delete newDataObj[key] + + // Convertir de vuelta a array si es necesario + const newData = Array.isArray(data) + ? Object.values(newDataObj) + : newDataObj + + onChange(newData) + } + + const handleAdd = () => { + if (!newItem[keyField]) { + alert(`Please provide a ${keyField}`) + return + } + + const newDataObj = { ...dataAsObject } + newDataObj[newItem[keyField]] = { ...newItem } + + // Convertir de vuelta a array si es necesario + const newData = Array.isArray(data) + ? Object.values(newDataObj) + : newDataObj + + onChange(newData) + setNewItem({}) + onAddClose() + } + + const handleEdit = () => { + if (!editingKey) return + + const newDataObj = { ...dataAsObject } + newDataObj[editingKey] = { ...editingData } + + // Convertir de vuelta a array si es necesario + const newData = Array.isArray(data) + ? Object.values(newDataObj) + : newDataObj + + onChange(newData) + setEditingKey(null) + setEditingData({}) + onEditClose() + } + + const openEdit = (key) => { + setEditingKey(key) + setEditingData({ ...dataAsObject[key] }) + onEditOpen() + } + + const renderInput = (propertyName, value, setValue, itemData = {}) => { + const property = properties[propertyName] + const uiConfig = uiSchema[propertyName] || {} + const widget = uiConfig['ui:widget'] || 'text' + + const commonProps = { + size: 'sm', + value: value || '', + onChange: (e) => setValue({ ...itemData, [propertyName]: e.target.value }) + } + + if (property?.enum && widget === 'select') { + return ( + + ) + } + + if (property?.type === 'boolean' || widget === 'checkbox') { + return ( + setValue({ ...itemData, [propertyName]: e.target.checked })} + /> + ) + } + + if (property?.type === 'number' || property?.type === 'integer' || widget === 'updown') { + return ( + setValue({ ...itemData, [propertyName]: parseFloat(valueString) || null })} + min={property?.minimum} + max={property?.maximum} + step={property?.type === 'integer' ? 1 : 0.01} + > + + + + + + + ) + } + + return + } + + const renderValue = (propertyName, value) => { + const property = properties[propertyName] + + if (property?.type === 'boolean') { + return + } + + if (value === null || value === undefined) { + return - + } + + return {String(value)} + } + + const getColumnTitle = (propertyName) => { + const property = properties[propertyName] + return property?.title || propertyName + } + + if (propertyNames.length === 0) { + return ( + + + No schema properties defined for this table + + ) + } + + return ( + + + {title} + {allowAdd && ( + + )} + + + + + + + {propertyNames.map(prop => ( + + ))} + {(allowEdit || allowDelete) && } + + + + {dataKeys.length === 0 ? ( + + + + ) : ( + dataKeys.map(key => ( + + {propertyNames.map(prop => ( + + ))} + {(allowEdit || allowDelete) && ( + + )} + + )) + )} + +
{getColumnTitle(prop)}Actions
+ No items +
+ {renderValue(prop, dataAsObject[key][prop])} + + + {allowEdit && ( + } + size="xs" + variant="outline" + onClick={() => openEdit(key)} + /> + )} + {allowDelete && ( + } + size="xs" + variant="outline" + colorScheme="red" + onClick={() => handleDelete(key)} + /> + )} + +
+
+ + {/* Add Modal */} + + + + Add New Item + + + + {propertyNames.map(prop => ( + + + {getColumnTitle(prop)} + {schema.required?.includes(prop) && *} + + {renderInput(prop, newItem[prop], setNewItem, newItem)} + {uiSchema[prop]?.['ui:help'] && ( + + {uiSchema[prop]['ui:help']} + + )} + + ))} + + + + + + + + + + {/* Edit Modal */} + + + + Edit Item: {editingKey} + + + + {propertyNames.map(prop => ( + + + {getColumnTitle(prop)} + {schema.required?.includes(prop) && *} + + {renderInput(prop, editingData[prop], setEditingData, editingData)} + {uiSchema[prop]?.['ui:help'] && ( + + {uiSchema[prop]['ui:help']} + + )} + + ))} + + + + + + + + +
+ ) +} diff --git a/frontend/src/components/FormTable.jsx b/frontend/src/components/FormTable.jsx new file mode 100644 index 0000000..9857c85 --- /dev/null +++ b/frontend/src/components/FormTable.jsx @@ -0,0 +1,234 @@ +import React, { useState, useEffect } from 'react' +import { + Box, + VStack, + HStack, + Text, + Button, + Card, + CardBody, + CardHeader, + Heading, + Alert, + AlertIcon, + useColorModeValue, + Divider, + IconButton, + Flex, + Badge +} from '@chakra-ui/react' +import { AddIcon, DeleteIcon, EditIcon } from '@chakra-ui/icons' +import Form from '@rjsf/chakra-ui' +import validator from '@rjsf/validator-ajv8' +import LayoutObjectFieldTemplate from './rjsf/LayoutObjectFieldTemplate.jsx' +import { widgets } from './rjsf/widgets.jsx' + +/** + * FormTable - Muestra objetos como filas de formularios usando schemas RJSF + */ +export default function FormTable({ + schema, + uiSchema = {}, + data = {}, + onChange, + title = "Data", + keyField = "id", + allowAdd = true, + allowDelete = true +}) { + const [editingKey, setEditingKey] = useState(null) + const [addingNew, setAddingNew] = useState(false) + const [newKey, setNewKey] = useState('') + + const muted = useColorModeValue('gray.600', 'gray.300') + const borderColor = useColorModeValue('gray.200', 'gray.600') + + if (!schema || !schema.additionalProperties) { + return ( + + + Schema not available for {title} + + ) + } + + const itemSchema = schema.additionalProperties + const itemUiSchema = uiSchema.additionalProperties || {} + const dataKeys = Object.keys(data) + + const handleAdd = (formData) => { + if (!newKey) { + alert('Please provide a key/ID') + return + } + + const newData = { + ...data, + [newKey]: formData + } + + onChange(newData) + setAddingNew(false) + setNewKey('') + } + + const handleEdit = (key, formData) => { + const newData = { + ...data, + [key]: formData + } + + onChange(newData) + setEditingKey(null) + } + + const handleDelete = (key) => { + if (confirm(`¿Eliminar "${key}"?`)) { + const newData = { ...data } + delete newData[key] + onChange(newData) + } + } + + const generateNewKey = () => { + const baseName = keyField === 'id' ? 'item' : 'new' + let counter = 1 + let newKey = `${baseName}_${counter}` + + while (data[newKey]) { + counter++ + newKey = `${baseName}_${counter}` + } + + return newKey + } + + const startAdd = () => { + setNewKey(generateNewKey()) + setAddingNew(true) + } + + return ( + + + {title} + {allowAdd && ( + + )} + + + {dataKeys.length === 0 && !addingNew && ( + + No items found + + )} + + {/* Formulario para agregar nuevo item */} + {addingNew && ( + + + + + ➕ Adding: {newKey} + + + + + +
handleAdd(formData)} + templates={{ ObjectFieldTemplate: LayoutObjectFieldTemplate }} + widgets={widgets} + > + + + + +
+
+
+ )} + + {/* Formularios para items existentes */} + {dataKeys.map(key => ( + + + + + {key} + {editingKey === key && ( + Editing + )} + + + {editingKey === key ? ( + + ) : ( + <> + } + size="xs" + variant="outline" + onClick={() => setEditingKey(key)} + /> + {allowDelete && ( + } + size="xs" + variant="outline" + colorScheme="red" + onClick={() => handleDelete(key)} + /> + )} + + )} + + + + +
{ }} + onSubmit={editingKey === key ? ({ formData }) => handleEdit(key, formData) : undefined} + templates={{ ObjectFieldTemplate: LayoutObjectFieldTemplate }} + widgets={widgets} + readonly={editingKey !== key} + > + {editingKey === key && ( + + + + + )} +
+
+
+ ))} +
+ ) +} diff --git a/frontend/src/components/PlotFormManager.jsx b/frontend/src/components/PlotFormManager.jsx new file mode 100644 index 0000000..a25cf03 --- /dev/null +++ b/frontend/src/components/PlotFormManager.jsx @@ -0,0 +1,399 @@ +import React, { useState, useEffect, useMemo } from 'react' +import { + Box, + VStack, + HStack, + Text, + Select, + Card, + CardBody, + CardHeader, + Heading, + Alert, + AlertIcon, + useColorModeValue, + Divider, + Button, + Input, + IconButton, + Table, + Thead, + Tbody, + Tr, + Th, + Td, + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalBody, + ModalFooter, + ModalCloseButton, + useDisclosure +} from '@chakra-ui/react' +import { AddIcon, DeleteIcon, EditIcon } from '@chakra-ui/icons' +import FormTable from './FormTable.jsx' +import { getSchema, readConfig, writeConfig } from '../services/api.js' + +/** + * PlotVariablesManager - Componente para gestionar array de strings (variables de plot) + */ +function PlotVariablesManager({ variables = [], onChange, title = "Variables" }) { + const [newVariable, setNewVariable] = useState('') + const [editingIndex, setEditingIndex] = useState(null) + const [editingValue, setEditingValue] = useState('') + + const { isOpen: isAddOpen, onOpen: onAddOpen, onClose: onAddClose } = useDisclosure() + const { isOpen: isEditOpen, onOpen: onEditOpen, onClose: onEditClose } = useDisclosure() + + const muted = useColorModeValue('gray.600', 'gray.300') + const borderColor = useColorModeValue('gray.200', 'gray.600') + + const handleAdd = () => { + if (!newVariable.trim()) return + + const newVariables = [...variables, newVariable.trim()] + onChange(newVariables) + setNewVariable('') + onAddClose() + } + + const handleEdit = () => { + if (editingIndex === null || !editingValue.trim()) return + + const newVariables = [...variables] + newVariables[editingIndex] = editingValue.trim() + onChange(newVariables) + setEditingIndex(null) + setEditingValue('') + onEditClose() + } + + const handleDelete = (index) => { + if (confirm('¿Eliminar esta variable?')) { + const newVariables = variables.filter((_, i) => i !== index) + onChange(newVariables) + } + } + + const openEdit = (index) => { + setEditingIndex(index) + setEditingValue(variables[index]) + onEditOpen() + } + + return ( + + + {title} + + + + + + + + + + + + + {variables.length === 0 ? ( + + + + ) : ( + variables.map((variable, index) => ( + + + + + )) + )} + +
Variable NameActions
+ No variables +
{variable} + + } + size="xs" + variant="outline" + onClick={() => openEdit(index)} + /> + } + size="xs" + variant="outline" + colorScheme="red" + onClick={() => handleDelete(index)} + /> + +
+
+ + {/* Add Modal */} + + + + Add Variable + + + + + + Variable Name * + + setNewVariable(e.target.value)} + placeholder="e.g., UR29_Brix" + /> + + Enter the name of the variable to plot + + + + + + + + + + + + {/* Edit Modal */} + + + + Edit Variable + + + + + + Variable Name * + + setEditingValue(e.target.value)} + placeholder="e.g., UR29_Brix" + /> + + Enter the name of the variable to plot + + + + + + + + + + +
+ ) +} + +/** + * PlotFormManager - Gestiona plots y variables usando FormTable + */ +export default function PlotFormManager() { + const [plots, setPlots] = useState({}) + const [plotVariables, setPlotVariables] = useState({}) + const [selectedPlotId, setSelectedPlotId] = useState('') + + const [plotSchema, setPlotSchema] = useState(null) + const [plotUiSchema, setPlotUiSchema] = useState({}) + + const [loading, setLoading] = useState(true) + const [message, setMessage] = useState('') + + const muted = useColorModeValue('gray.600', 'gray.300') + + useEffect(() => { + loadData() + }, []) + + const loadData = async () => { + setLoading(true) + try { + // Cargar schemas + const [plotSchemaResp, plotVariableSchemaResp] = await Promise.all([ + getSchema('plot-definitions'), + getSchema('plot-variables') + ]) + + console.log('Plot schema response:', plotSchemaResp) + console.log('Plot variable schema response:', plotVariableSchemaResp) + + // Cargar datos + const [plotDataResp, plotVariableDataResp] = await Promise.all([ + readConfig('plot-definitions'), + readConfig('plot-variables') + ]) + + console.log('Plot data response:', plotDataResp) + console.log('Plot variable data response:', plotVariableDataResp) + + // Extraer schemas + setPlotSchema(plotSchemaResp.schema?.properties?.plots) + setPlotUiSchema(plotSchemaResp.ui_schema?.plots || {}) + + setPlots(plotDataResp.data?.plots || {}) + setPlotVariables(plotVariableDataResp.data?.plot_variables || {}) + + // Seleccionar primer plot + const plotIds = Object.keys(plotDataResp.data?.plots || {}) + if (plotIds.length > 0 && !selectedPlotId) { + setSelectedPlotId(plotIds[0]) + } + + } catch (error) { + console.error('Error loading plot data:', error) + setMessage(`Error loading data: ${error.message}`) + } finally { + setLoading(false) + } + } + + const savePlots = async (newPlots) => { + try { + const currentConfig = await readConfig('plot-definitions') + const saveData = { + ...currentConfig.data, + plots: newPlots, + last_saved: new Date().toISOString() + } + + await writeConfig('plot-definitions', saveData) + setPlots(newPlots) + setMessage('Plots saved successfully') + setTimeout(() => setMessage(''), 3000) + } catch (error) { + console.error('Error saving plots:', error) + setMessage(`Error saving plots: ${error.message}`) + } + } + + const savePlotVariables = async (newVariables) => { + try { + const currentConfig = await readConfig('plot-variables') + const updatedPlotVariables = { + ...plotVariables, + [selectedPlotId]: { + variables: newVariables + } + } + + const saveData = { + ...currentConfig.data, + plot_variables: updatedPlotVariables, + last_update: new Date().toISOString() + } + + await writeConfig('plot-variables', saveData) + setPlotVariables(updatedPlotVariables) + setMessage('Plot variables saved successfully') + setTimeout(() => setMessage(''), 3000) + } catch (error) { + console.error('Error saving plot variables:', error) + setMessage(`Error saving variables: ${error.message}`) + } + } + + const currentPlotVariables = useMemo(() => { + return selectedPlotId && plotVariables[selectedPlotId] + ? plotVariables[selectedPlotId].variables || [] + : [] + }, [selectedPlotId, plotVariables]) + + const plotOptions = Object.entries(plots).map(([id, plot]) => ({ + value: id, + label: `${plot.name || id} (${id})` + })) + + if (loading) { + return Loading plots... + } + + return ( + + {message && ( + + + {message} + + )} + + {/* Plots */} + + + 📈 Plot Definitions + + + {plotSchema ? ( + + ) : ( + + + Plot schema not available + + )} + + + + + + {/* Variables del Plot */} + + + + 🔧 Plot Variables + + Plot: + + + + + + {!selectedPlotId ? ( + + + Select a plot to manage its variables + + ) : ( + + )} + + + + ) +} diff --git a/frontend/src/components/PlotTableManager.jsx b/frontend/src/components/PlotTableManager.jsx new file mode 100644 index 0000000..73a9a53 --- /dev/null +++ b/frontend/src/components/PlotTableManager.jsx @@ -0,0 +1,414 @@ +import React, { useState, useEffect, useMemo } from 'react' +import { + Box, + VStack, + HStack, + Text, + Select, + Button, + Card, + CardBody, + CardHeader, + Heading, + Alert, + AlertIcon, + useColorModeValue, + Divider, + Table, + Thead, + Tbody, + Tr, + Th, + Td, + IconButton, + Input, + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalBody, + ModalFooter, + ModalCloseButton, + useDisclosure +} from '@chakra-ui/react' +import { AddIcon, DeleteIcon, EditIcon } from '@chakra-ui/icons' +import EditableTable from './EditableTable.jsx' +import { getSchema, readConfig, writeConfig } from '../services/api.js' + +/** + * PlotVariablesTable - Componente especializado para editar variables de plots (array de strings) + */ +function PlotVariablesTable({ variables = [], onChange, title = "Variables" }) { + const [newVariable, setNewVariable] = useState('') + const { isOpen, onOpen, onClose } = useDisclosure() + const [editingIndex, setEditingIndex] = useState(null) + const [editingValue, setEditingValue] = useState('') + const { isOpen: isEditOpen, onOpen: onEditOpen, onClose: onEditClose } = useDisclosure() + + const muted = useColorModeValue('gray.600', 'gray.300') + const borderColor = useColorModeValue('gray.200', 'gray.600') + + const handleAdd = () => { + if (!newVariable.trim()) return + + const newVariables = [...variables, newVariable.trim()] + onChange(newVariables) + setNewVariable('') + onClose() + } + + const handleEdit = () => { + if (editingIndex === null || !editingValue.trim()) return + + const newVariables = [...variables] + newVariables[editingIndex] = editingValue.trim() + onChange(newVariables) + setEditingIndex(null) + setEditingValue('') + onEditClose() + } + + const handleDelete = (index) => { + const newVariables = variables.filter((_, i) => i !== index) + onChange(newVariables) + } + + const openEdit = (index) => { + setEditingIndex(index) + setEditingValue(variables[index]) + onEditOpen() + } + + return ( + + + {title} + + + + + + + + + + + + + {variables.length === 0 ? ( + + + + ) : ( + variables.map((variable, index) => ( + + + + + )) + )} + +
Variable NameActions
+ No variables +
{variable} + + } + size="xs" + variant="outline" + onClick={() => openEdit(index)} + /> + } + size="xs" + variant="outline" + colorScheme="red" + onClick={() => handleDelete(index)} + /> + +
+
+ + {/* Add Modal */} + + + + Add Variable + + + + + + Variable Name * + + setNewVariable(e.target.value)} + placeholder="e.g., UR29_Brix" + /> + + Enter the name of the variable to plot + + + + + + + + + + + + {/* Edit Modal */} + + + + Edit Variable + + + + + + Variable Name * + + setEditingValue(e.target.value)} + placeholder="e.g., UR29_Brix" + /> + + Enter the name of the variable to plot + + + + + + + + + + +
+ ) +} + +/** + * PlotTableManager - Componente para gestionar plots y sus variables + * Muestra tabla de plots y tabla de variables del plot seleccionado + */ +export default function PlotTableManager() { + const [plots, setPlots] = useState({}) + const [plotVariables, setPlotVariables] = useState({}) + const [selectedPlotId, setSelectedPlotId] = useState('') + + const [plotSchema, setPlotSchema] = useState(null) + const [plotUiSchema, setPlotUiSchema] = useState({}) + + const [loading, setLoading] = useState(true) + const [saving, setSaving] = useState(false) + const [message, setMessage] = useState('') + + const muted = useColorModeValue('gray.600', 'gray.300') + + // Cargar schemas y datos al montar + useEffect(() => { + loadData() + }, []) + + const loadData = async () => { + setLoading(true) + try { + // Cargar schemas + const [plotSchemaResp, plotVariableSchemaResp] = await Promise.all([ + getSchema('plot-definitions'), + getSchema('plot-variables') + ]) + + // Cargar datos de configuración + const [plotDataResp, plotVariableDataResp] = await Promise.all([ + readConfig('plot-definitions'), + readConfig('plot-variables') + ]) + + setPlotSchema(plotSchemaResp.schema?.properties?.plots) + setPlotUiSchema(plotSchemaResp.ui_schema?.plots || {}) + + setPlots(plotDataResp.data?.plots || {}) + setPlotVariables(plotVariableDataResp.data?.plot_variables || {}) + + // Seleccionar el primer plot si existe + const plotIds = Object.keys(plotDataResp.data?.plots || {}) + if (plotIds.length > 0 && !selectedPlotId) { + setSelectedPlotId(plotIds[0]) + } + + } catch (error) { + setMessage(`Error loading data: ${error.message}`) + } finally { + setLoading(false) + } + } + + const savePlots = async (newPlots) => { + setSaving(true) + setMessage('') + try { + // Construir el objeto completo para guardar + const saveData = { + plots: newPlots, + session_counter: 0, // Esto se puede gestionar por separado + last_saved: new Date().toISOString(), + version: "1.0" + } + + await writeConfig('plot-definitions', saveData) + setPlots(newPlots) + setMessage('Plots saved successfully') + } catch (error) { + setMessage(`Error saving plots: ${error.message}`) + } finally { + setSaving(false) + } + } + + const savePlotVariables = async (newVariables) => { + setSaving(true) + setMessage('') + try { + const updatedPlotVariables = { + ...plotVariables, + [selectedPlotId]: { + variables: newVariables + } + } + + const saveData = { + plot_variables: updatedPlotVariables, + version: "1.0", + last_update: new Date().toISOString() + } + + await writeConfig('plot-variables', saveData) + setPlotVariables(updatedPlotVariables) + setMessage('Plot variables saved successfully') + } catch (error) { + setMessage(`Error saving variables: ${error.message}`) + } finally { + setSaving(false) + } + } + + // Convertir datos de plots para el componente EditableTable + const plotsForTable = useMemo(() => { + return Object.entries(plots).map(([id, data]) => ({ + id, + ...data + })) + }, [plots]) + + // Variables del plot seleccionado + const variablesForTable = useMemo(() => { + if (!selectedPlotId || !plotVariables[selectedPlotId]) { + return [] + } + + return plotVariables[selectedPlotId].variables || [] + }, [selectedPlotId, plotVariables]) + + const plotOptions = Object.entries(plots).map(([id, plot]) => ({ + value: id, + label: `${plot.name} (${id})` + })) + + if (loading) { + return Loading plots... + } + + return ( + + {message && ( + + + {message} + + )} + + {/* Tabla de Plots */} + + + 📈 Plots + + + {plotSchema ? ( + { + // Convertir de array a objeto con keys + const newPlots = {} + newData.forEach(item => { + const { id, ...rest } = item + newPlots[id] = rest + }) + savePlots(newPlots) + }} + title="Plot Definitions" + keyField="id" + /> + ) : ( + + + No plot schema available + + )} + + + + + + {/* Selector de Plot y Tabla de Variables */} + + + + 🔧 Plot Variables + + Plot: + + + + + + {!selectedPlotId ? ( + + + Select a plot to manage its variables + + ) : ( + + )} + + + + ) +} diff --git a/frontend/src/pages/Dashboard.jsx b/frontend/src/pages/Dashboard.jsx index 727dcda..1644e23 100644 --- a/frontend/src/pages/Dashboard.jsx +++ b/frontend/src/pages/Dashboard.jsx @@ -5,6 +5,8 @@ import Form from '@rjsf/chakra-ui' import validator from '@rjsf/validator-ajv8' import LayoutObjectFieldTemplate from '../components/rjsf/LayoutObjectFieldTemplate.jsx' import { widgets } from '../components/rjsf/widgets.jsx' +import DatasetFormManager from '../components/DatasetFormManager.jsx' +import PlotFormManager from '../components/PlotFormManager.jsx' import { getStatus, getEvents, @@ -225,7 +227,47 @@ export default function DashboardPage() { {statusError && {statusError}} {status && } - {available.map((sectionId) => ( + {/* Sección PLC Config */} + {available.includes('plc') && ( + + + + 🧩 PLC Configuration + + + + + + + + )} + + {/* Sección Datasets con Tablas */} + {(available.includes('dataset-definitions') || available.includes('dataset-variables')) && ( + + + + 📊 Dataset Management + + + + + )} + + {/* Sección Plots con Tablas */} + {(available.includes('plot-definitions') || available.includes('plot-variables')) && ( + + + + 📈 Plot Management + + + + + )} + + {/* Otras secciones que no son datasets ni plots */} + {available.filter(id => !['dataset-definitions', 'dataset-variables', 'plot-definitions', 'plot-variables', 'plc'].includes(id)).map((sectionId) => (