From 3417056b0667b285e5a373ddc972467f16701b1d Mon Sep 17 00:00:00 2001 From: Miguel Date: Fri, 15 Aug 2025 22:55:03 +0200 Subject: [PATCH] Refactor: Remove PlotRealtimeViewer and PlotTableManager components - Deleted PlotRealtimeViewer.jsx and PlotTableManager.jsx components to streamline the codebase. - Removed associated imports and references in Dashboard.jsx. - Cleaned up unused TabCoordinationDemo component. - Updated main.py to serve SIDEL.png from the public folder. - Removed obsolete test scripts related to configuration reload, endpoint testing, header validation, and symbol loading. --- MULTI_BROWSER_FIX_SUMMARY.md | 64 - MULTI_BROWSER_SUPPORT.md | 75 -- OFFLINE_USAGE.md | 107 -- application_events.json | 11 +- .../src/components/DatasetCompleteManager.jsx | 294 ----- .../src/components/DatasetFormManager.jsx | 237 ---- .../src/components/DatasetTableManager.jsx | 264 ---- .../src/components/DatasetVariableManager.jsx | 426 ------ .../src/components/DatasetVariablesRJSF.jsx | 195 --- frontend/src/components/EditableTable.jsx | 355 ----- frontend/src/components/FormTable.jsx | 254 ---- .../src/components/PlotCompleteManager.jsx | 264 ---- frontend/src/components/PlotFormManager.jsx | 402 ------ frontend/src/components/PlotManager.jsx | 1161 ++++++++++------- frontend/src/components/PlotManagerSimple.jsx | 884 ------------- .../src/components/PlotRealtimeViewer.jsx | 284 ---- frontend/src/components/PlotTableManager.jsx | 414 ------ .../src/components/TabCoordinationDemo.jsx | 62 - frontend/src/pages/Dashboard.jsx | 2 +- main.py | 10 + test_config_reload.py | 129 -- test_endpoint.py | 23 - test_header_validation.py | 105 -- test_request.json | Bin 126 -> 0 bytes test_symbol_loader.py | 67 - 25 files changed, 741 insertions(+), 5348 deletions(-) delete mode 100644 MULTI_BROWSER_FIX_SUMMARY.md delete mode 100644 MULTI_BROWSER_SUPPORT.md delete mode 100644 OFFLINE_USAGE.md delete mode 100644 frontend/src/components/DatasetCompleteManager.jsx delete mode 100644 frontend/src/components/DatasetFormManager.jsx delete mode 100644 frontend/src/components/DatasetTableManager.jsx delete mode 100644 frontend/src/components/DatasetVariableManager.jsx delete mode 100644 frontend/src/components/DatasetVariablesRJSF.jsx delete mode 100644 frontend/src/components/EditableTable.jsx delete mode 100644 frontend/src/components/FormTable.jsx delete mode 100644 frontend/src/components/PlotCompleteManager.jsx delete mode 100644 frontend/src/components/PlotFormManager.jsx delete mode 100644 frontend/src/components/PlotManagerSimple.jsx delete mode 100644 frontend/src/components/PlotRealtimeViewer.jsx delete mode 100644 frontend/src/components/PlotTableManager.jsx delete mode 100644 frontend/src/components/TabCoordinationDemo.jsx delete mode 100644 test_config_reload.py delete mode 100644 test_endpoint.py delete mode 100644 test_header_validation.py delete mode 100644 test_request.json delete mode 100644 test_symbol_loader.py diff --git a/MULTI_BROWSER_FIX_SUMMARY.md b/MULTI_BROWSER_FIX_SUMMARY.md deleted file mode 100644 index 326dee3..0000000 --- a/MULTI_BROWSER_FIX_SUMMARY.md +++ /dev/null @@ -1,64 +0,0 @@ -# Multi-Browser Plot Fix - Resumen de Cambios - -## Problema Original -Cuando se abría el mismo plot en múltiples pestañas del mismo navegador, ambas instancias se bloqueaban y dejaban de funcionar. Funcionaba bien en navegadores diferentes pero no en pestañas del mismo navegador. - -## Análisis del Problema -El problema era que aunque el backend generaba session IDs únicos, el frontend seguía consultando con el `plot_id` original, causando que la segunda pestaña encontrara la sesión de la primera pestaña y asumiera que era suya. - -## Solución Implementada - -### 1. Backend Changes (core/plot_manager.py) -- **Session IDs únicos mejorados**: Ahora incluyen `browser_tab_id` en el formato: `{plot_id}_{browser_tab_id}_{timestamp}_{counter}` -- **Búsqueda por plot_id**: El método `get_session_config` ahora puede buscar por session_id único O por plot_id (backward compatibility) -- **Limpieza automática**: Sesiones inactivas se limpian automáticamente cada 5 minutos - -### 2. Frontend Changes (PlotRealtimeSession.jsx) -- **Browser Tab ID único**: Cada instancia genera un ID único: `tab_{timestamp}_{random}` -- **Session ID management**: Se mantiene el `actualSessionId` devuelto por el backend -- **No auto-refresh inicial**: Se eliminó el auto-refresh al cargar para evitar conflictos -- **Identificador visual**: Se muestra el Tab ID y Session ID para debugging - -### 3. API Changes (main.py) -- **Soporte para browser_tab_id**: El endpoint de creación acepta `browser_tab_id` -- **Nuevos endpoints**: - - `/api/plots/sessions/{plot_id}` - Lista sesiones de un plot - - `/api/plots/cleanup` - Limpieza manual de sesiones - -## Flujo de Trabajo Actualizado - -### Primera Pestaña: -1. Genera `browserTabId` único -2. Crea sesión con `browser_tab_id` incluido -3. Recibe `actualSessionId` único del backend -4. Usa `actualSessionId` para todas las operaciones - -### Segunda Pestaña: -1. Genera su propio `browserTabId` único -2. No busca sesiones existentes al inicio -3. Crea su propia sesión independiente -4. Recibe su propio `actualSessionId` único -5. Opera completamente independiente de la primera pestaña - -## Beneficios -- ✅ Múltiples pestañas del mismo navegador funcionan independientemente -- ✅ Múltiples navegadores siguen funcionando -- ✅ No hay conflictos de estado entre instancias -- ✅ Limpieza automática previene acumulación de sesiones -- ✅ Debugging mejorado con IDs visibles -- ✅ Backward compatibility mantenida - -## Testing -Para probar: -1. Abrir plot en primera pestaña -2. Hacer clic en "Start" -3. Abrir segunda pestaña del mismo navegador -4. Abrir el mismo plot -5. Hacer clic en "Start" en segunda pestaña -6. Ambos plots deben funcionar independientemente - -## Archivos Modificados -- `core/plot_manager.py` - Session management mejorado -- `frontend/src/components/PlotRealtimeSession.jsx` - Tab ID y session management -- `main.py` - API endpoints actualizados -- `MULTI_BROWSER_SUPPORT.md` - Documentación diff --git a/MULTI_BROWSER_SUPPORT.md b/MULTI_BROWSER_SUPPORT.md deleted file mode 100644 index aa2f564..0000000 --- a/MULTI_BROWSER_SUPPORT.md +++ /dev/null @@ -1,75 +0,0 @@ -# Multi-Browser Plot Support - -## Problema Resuelto -Anteriormente no era posible tener el mismo plot activo en múltiples navegadores simultáneamente. Cuando se intentaba esto, ambas instancias se bloqueaban y dejaban de funcionar. - -## Solución Implementada - -### 1. Session IDs Únicos -- **Antes**: El sistema usaba `plotDefinition.id` directamente como `session_id` -- **Ahora**: Se generan session IDs únicos con formato: `{plot_id}_{timestamp}_{counter}` - -### 2. Gestión de Sesiones Múltiples -- Cada navegador/pestaña puede crear su propia sesión independiente -- Las sesiones se distinguen por timestamps únicos -- Se incluye limpieza automática de sesiones inactivas - -### 3. API Mejorada - -#### Nuevo Parámetro en POST /api/plots -```json -{ - "id": "plot_1", - "name": "Mi Plot", - "variables": ["var1", "var2"], - "allow_multiple": true // Permite múltiples sesiones (default: true) -} -``` - -#### Nuevos Endpoints -- `GET /api/plots/sessions/{plot_id}` - Lista todas las sesiones de un plot -- `POST /api/plots/cleanup` - Limpieza manual de sesiones inactivas - -### 4. Frontend Actualizado -- El componente `PlotRealtimeSession` ahora maneja session IDs dinámicos -- Se muestra información del session ID real cuando es diferente del plot ID -- Mejor gestión de estado para sesiones independientes - -### 5. Limpieza Automática -- Las sesiones inactivas se limpian automáticamente cada 5 minutos -- Sesiones mayores a 1 hora sin actividad son eliminadas por defecto -- Evita acumulación de sesiones obsoletas - -## Uso - -### Para Permitir Múltiples Instancias (Comportamiento por Defecto) -```javascript -await api.createPlot({ - id: "plot_1", - name: "Mi Plot", - variables: ["var1", "var2"], - allow_multiple: true // Permite múltiples navegadores -}) -``` - -### Para Reutilizar Sesión Existente -```javascript -await api.createPlot({ - id: "plot_1", - name: "Mi Plot", - variables: ["var1", "var2"], - allow_multiple: false // Reutiliza sesión activa si existe -}) -``` - -## Beneficios -1. **Múltiples Navegadores**: Cada usuario puede tener su propia instancia del mismo plot -2. **Sin Conflictos**: Las sesiones no se interfieren entre sí -3. **Limpieza Automática**: No hay acumulación de sesiones obsoletas -4. **Compatibilidad**: Mantiene compatibilidad con código existente -5. **Flexibilidad**: Se puede elegir entre múltiples sesiones o reutilización - -## Consideraciones -- Cada sesión adicional consume memoria -- La limpieza automática evita acumulación excesiva -- El frontend mantiene referencia al session ID real para comunicación correcta con el backend diff --git a/OFFLINE_USAGE.md b/OFFLINE_USAGE.md deleted file mode 100644 index 2fcac56..0000000 --- a/OFFLINE_USAGE.md +++ /dev/null @@ -1,107 +0,0 @@ -# Offline Usage Configuration - -## Overview -The application has been configured to work completely offline without any CDN dependencies. - -## Changes Made - -### 1. Chart.js Libraries Migration -**Before (CDN Dependencies):** -- Chart.js loaded from `https://cdn.jsdelivr.net/npm/chart.js@3.9.1` -- Luxon from `https://cdn.jsdelivr.net/npm/luxon@2` -- Chart.js adapter from `https://cdn.jsdelivr.net/npm/chartjs-adapter-luxon@1.3.1` -- Zoom plugin from `https://unpkg.com/chartjs-plugin-zoom@1.2.1` -- Streaming plugin from `https://cdn.jsdelivr.net/npm/chartjs-plugin-streaming@2.0.0` - -**After (NPM Dependencies):** -All Chart.js libraries are now installed as npm packages and bundled with the application: -```json -"chart.js": "^3.9.1", -"chartjs-adapter-luxon": "^1.3.1", -"chartjs-plugin-streaming": "^2.0.0", -"chartjs-plugin-zoom": "^1.2.1", -"luxon": "^2.5.2" -``` - -### 2. Chart.js Setup Module -Created `frontend/src/utils/chartSetup.js` that: -- Imports all Chart.js components as ES modules -- Registers all required plugins (zoom, streaming, time scales) -- Makes Chart.js available globally for existing components -- Provides console confirmation of successful setup - -### 3. Application Entry Point -Modified `frontend/src/main.jsx` to import the Chart.js setup before rendering the application. - -### 4. Updated HTML Template -Removed all CDN script tags from `frontend/index.html`. - -## Verification - -### Build Verification -The application builds successfully without any external dependencies: -```bash -cd frontend -npm run build -``` - -### Development Server -The development server runs without internet connection: -```bash -cd frontend -npm run dev -``` - -### Runtime Verification -- No network requests to external CDNs -- All Chart.js functionality preserved (zooming, streaming, real-time plots) -- Completely self-contained in bundled JavaScript - -## Backend Offline Compliance - -The Python backend already uses only local dependencies: -- Flask for web server -- python-snap7 for PLC communication -- Local file-based configuration -- No external API calls or services - -## Deployment for Offline Use - -### Frontend Production Build -```bash -cd frontend -npm run build -``` -The `dist/` folder contains all necessary files with no external dependencies. - -### Complete Offline Package -The entire application (backend + frontend) can be deployed on systems without internet access: - -1. **Python Requirements**: Install from `requirements.txt` -2. **Frontend**: Use built files from `dist/` folder -3. **PLC Communication**: Requires `snap7.dll` in system PATH -4. **Configuration**: All JSON-based, stored locally - -## Chart.js Feature Compatibility - -All existing Chart.js features remain functional: -- ✅ Real-time streaming plots -- ✅ Zoom and pan functionality -- ✅ Time-based X-axis with Luxon adapter -- ✅ Multiple dataset support -- ✅ Dynamic color assignment -- ✅ Plot session management -- ✅ CSV data export integration - -## Technical Notes - -### Global vs ES Module Access -The setup maintains backward compatibility by making Chart.js available both ways: -- **Global**: `window.Chart` (for existing components) -- **ES Module**: `import ChartJS from './utils/chartSetup.js'` (for new components) - -### Bundle Size Impact -The Chart.js libraries add approximately ~400KB to the bundle (gzipped), which is acceptable for offline industrial applications. - -### Browser Compatibility -All dependencies support modern browsers without requiring polyfills for the target deployment environment. diff --git a/application_events.json b/application_events.json index 16e5527..267c60d 100644 --- a/application_events.json +++ b/application_events.json @@ -8145,8 +8145,15 @@ "event_type": "application_started", "message": "Application initialization completed successfully", "details": {} + }, + { + "timestamp": "2025-08-15T22:47:53.048381", + "level": "info", + "event_type": "application_started", + "message": "Application initialization completed successfully", + "details": {} } ], - "last_updated": "2025-08-15T21:25:51.900870", - "total_entries": 676 + "last_updated": "2025-08-15T22:47:53.048381", + "total_entries": 677 } \ No newline at end of file diff --git a/frontend/src/components/DatasetCompleteManager.jsx b/frontend/src/components/DatasetCompleteManager.jsx deleted file mode 100644 index 11e4bf7..0000000 --- a/frontend/src/components/DatasetCompleteManager.jsx +++ /dev/null @@ -1,294 +0,0 @@ -import React, { useState, useEffect, useMemo } from 'react' -import { - Box, - VStack, - HStack, - Text, - Select, - Card, - CardBody, - CardHeader, - Heading, - Alert, - AlertIcon, - useColorModeValue, - Divider, - Button -} from '@chakra-ui/react' -// No necesitamos Form completo, solo FormTable -import FormTable from './FormTable.jsx' -import { getSchema, readConfig, writeConfig, activateDataset, deactivateDataset } from '../services/api.js' -import { useCoordinatedSSE } from '../hooks/useCoordinatedConnection' - -/** - * DatasetCompleteManager - Gestiona datasets y variables de forma simplificada - * Incluye: tabla de datasets individuales + variables (sin campos estáticos de configuración) - */ -export default function DatasetCompleteManager({ status }) { - const [fullData, setFullData] = 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') - const [liveValues, setLiveValues] = useState({}) - - // Usar SSE coordinado para valores en vivo del dataset seleccionado - const plcConnected = !!status?.plc_connected - const sseUrl = selectedDatasetId && plcConnected ? - `/api/stream/variables?dataset_id=${encodeURIComponent(selectedDatasetId)}&interval=1.0` : - null - - const { data: liveData } = useCoordinatedSSE( - `dataset_variables_${selectedDatasetId}`, - sseUrl, - [selectedDatasetId, plcConnected] - ) - - // Procesar datos SSE recibidos - useEffect(() => { - if (!liveData) { - setLiveValues({}) - return - } - - if (liveData?.type === 'values' && liveData.values) { - setLiveValues(liveData.values || {}) - } else if (liveData?.type === 'no_cache' || liveData?.type === 'dataset_inactive' || liveData?.type === 'plc_disconnected') { - setLiveValues({}) - } - }, [liveData]) - - useEffect(() => { - loadData() - }, []) - - const loadData = async () => { - setLoading(true) - try { - // Cargar schemas completos - const [datasetSchemaResp, variableSchemaResp] = await Promise.all([ - getSchema('dataset-definitions'), - getSchema('dataset-variables') - ]) - - // Cargar datos - const [datasetDataResp, variableDataResp] = await Promise.all([ - readConfig('dataset-definitions'), - readConfig('dataset-variables') - ]) - - // Usar schemas completos - setDatasetSchema(datasetSchemaResp.schema) - setDatasetUiSchema(datasetSchemaResp.ui_schema || {}) - - // Schema para variables individuales - const variableSchemaPath = variableSchemaResp.schema?.properties?.dataset_variables?.additionalProperties?.properties?.variables?.additionalProperties - const variableUiSchemaPath = variableSchemaResp.ui_schema?.dataset_variables?.additionalProperties?.variables?.additionalProperties - - // FormTable requiere un schema con additionalProperties en la raíz - setVariableSchema(variableSchemaPath ? { additionalProperties: variableSchemaPath } : null) - setVariableUiSchema(variableUiSchemaPath || {}) - - setFullData(datasetDataResp.data || {}) - setDatasetVariables(variableDataResp.data?.dataset_variables || {}) - - // Seleccionar dataset actual - const currentId = datasetDataResp.data?.current_dataset_id - const datasetIds = Object.keys(datasetDataResp.data?.datasets || {}) - if (currentId && datasetIds.includes(currentId)) { - setSelectedDatasetId(currentId) - } else if (datasetIds.length > 0) { - setSelectedDatasetId(datasetIds[0]) - } - - } catch (error) { - console.error('Error loading complete data:', error) - setMessage(`Error loading data: ${error.message}`) - } finally { - setLoading(false) - } - } - - const saveFullData = async (newData) => { - setSaving(true) - try { - await writeConfig('dataset-definitions', newData) - setFullData(newData) - setMessage('Datasets saved successfully') - setTimeout(() => setMessage(''), 3000) - } catch (error) { - console.error('Error saving data:', error) - setMessage(`Error saving datasets: ${error.message}`) - } finally { - setSaving(false) - } - } - - const saveDatasets = async (newDatasets) => { - try { - // Detectar cambios en "enabled" para llamar a endpoints de activación, evitando estados inconsistentes - const prev = fullData.datasets || {} - const changedIds = [] - for (const [id, cfg] of Object.entries(newDatasets)) { - const before = prev[id]?.enabled === true - const after = cfg?.enabled === true - if (before !== after) changedIds.push({ id, after }) - } - - // Persistir datasets primero - const newFullData = { datasets: newDatasets } - await saveFullData(newFullData) - - // Aplicar activación/desactivación en backend para arrancar/parar hilos - await Promise.allSettled(changedIds.map(({ id, after }) => after ? activateDataset(id) : deactivateDataset(id))) - - // Refrescar selección y datos locales tras cambios - setFullData(newFullData) - if (selectedDatasetId && !newDatasets[selectedDatasetId]) { - const ids = Object.keys(newDatasets) - setSelectedDatasetId(ids[0] || '') - } - setMessage('Datasets saved and activation applied') - setTimeout(() => setMessage(''), 2000) - } catch (error) { - console.error('Error saving datasets:', error) - setMessage(`Error saving datasets: ${error.message}`) - } - } - - const saveDatasetVariables = async (newVariables) => { - try { - const updatedDatasetVariables = { - ...datasetVariables, - [selectedDatasetId]: { - variables: newVariables, - streaming_variables: Object.keys(newVariables).filter(key => newVariables[key]?.streaming) - } - } - - const saveData = { - dataset_variables: updatedDatasetVariables - } - - 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(fullData.datasets || {}).map(([id, dataset]) => ({ - value: id, - label: `${dataset.name || id} (${id})` - })) - - if (loading) { - return Loading dataset configuration... - } - - return ( - - {message && ( - - - {message} - - )} - - {/* Tabla de Datasets */} - - - 📊 Dataset Management - - Manage your datasets: create, edit and configure - - - - {datasetSchema?.properties?.datasets ? ( - - ) : ( - - - Dataset schema for individual datasets 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/DatasetFormManager.jsx b/frontend/src/components/DatasetFormManager.jsx deleted file mode 100644 index 81710a0..0000000 --- a/frontend/src/components/DatasetFormManager.jsx +++ /dev/null @@ -1,237 +0,0 @@ -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 || {}) - - console.log('Dataset full schema:', datasetSchemaResp.schema?.properties?.datasets) - console.log('Dataset full uiSchema:', datasetSchemaResp.ui_schema?.datasets) - - setVariableSchema(variableSchemaResp.schema?.properties?.dataset_variables?.additionalProperties?.properties?.variables) - setVariableUiSchema(variableSchemaResp.ui_schema?.dataset_variables?.additionalProperties?.variables || {}) - - console.log('Variable full schema:', variableSchemaResp.schema?.properties?.dataset_variables?.additionalProperties?.properties?.variables) - console.log('Variable full uiSchema:', 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 deleted file mode 100644 index 3f938fa..0000000 --- a/frontend/src/components/DatasetTableManager.jsx +++ /dev/null @@ -1,264 +0,0 @@ -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/DatasetVariableManager.jsx b/frontend/src/components/DatasetVariableManager.jsx deleted file mode 100644 index 074a9c0..0000000 --- a/frontend/src/components/DatasetVariableManager.jsx +++ /dev/null @@ -1,426 +0,0 @@ -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/DatasetVariablesRJSF.jsx b/frontend/src/components/DatasetVariablesRJSF.jsx deleted file mode 100644 index 9ceac98..0000000 --- a/frontend/src/components/DatasetVariablesRJSF.jsx +++ /dev/null @@ -1,195 +0,0 @@ -import React, { useState, useEffect } from 'react' -import { - Box, - VStack, - HStack, - Card, - CardBody, - CardHeader, - Heading, - Text, - Select, - Alert, - AlertIcon, - useColorModeValue, - Spinner -} from '@chakra-ui/react' -import Form from '@rjsf/chakra-ui' -import validator from '@rjsf/validator-ajv8' -import LayoutObjectFieldTemplate from './rjsf/LayoutObjectFieldTemplate.jsx' -import { allWidgets } from './widgets/AllWidgets.jsx' - -/** - * DatasetVariablesRJSF - Maneja variables de dataset usando RJSF Type 3 pattern - */ -export default function DatasetVariablesRJSF({ - datasets = {}, - selectedDatasetId, - onSelectDataset, - onVariablesUpdate -}) { - const [schema, setSchema] = useState(null) - const [uiSchema, setUiSchema] = useState(null) - const [variables, setVariables] = useState({ variables: [] }) - const [loading, setLoading] = useState(true) - const [saving, setSaving] = useState(false) - const [message, setMessage] = useState('') - - const muted = useColorModeValue('gray.600', 'gray.300') - - // Load schema and data - useEffect(() => { - loadSchemaAndData() - }, []) - - const loadSchemaAndData = async () => { - setLoading(true) - try { - // Load schema - const schemaResp = await fetch('/api/config/schema/dataset-variables') - const schemaData = await schemaResp.json() - - if (schemaData.success) { - setSchema(schemaData.schema) - setUiSchema(schemaData.ui_schema || {}) - } - - // Load data - const dataResp = await fetch('/api/config/dataset-variables') - const data = await dataResp.json() - - if (data.success) { - setVariables(data.data || { variables: [] }) - } - - } catch (error) { - console.error('Error loading schema/data:', error) - setMessage(`Error loading: ${error.message}`) - } finally { - setLoading(false) - } - } - - const saveVariables = async (formData) => { - setSaving(true) - try { - const response = await fetch('/api/config/dataset-variables', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(formData) - }) - - const result = await response.json() - - if (result.success) { - setVariables(formData) - setMessage('Variables saved successfully') - setTimeout(() => setMessage(''), 3000) - - if (onVariablesUpdate) { - onVariablesUpdate(formData) - } - } else { - throw new Error(result.error || 'Failed to save variables') - } - } catch (error) { - console.error('Error saving variables:', error) - setMessage(`Error saving: ${error.message}`) - } finally { - setSaving(false) - } - } - - // Get available datasets for selector - const datasetOptions = Object.entries(datasets).map(([id, dataset]) => ({ - value: id, - label: `${dataset.name || id} (${id})` - })) - - if (loading) { - return ( - - - - - Loading dataset variables configuration... - - - - ) - } - - if (!schema) { - return ( - - - - - Failed to load schema for dataset variables - - - - ) - } - - return ( - - - - 🔧 Dataset Variables - - Configure PLC variables for each dataset - use symbols or manual configuration - - - {datasetOptions.length > 0 && ( - - Reference Dataset: - - - )} - - - - {message && ( - - - {message} - - )} - -
setVariables(formData)} - onSubmit={({ formData }) => saveVariables(formData)} - templates={{ ObjectFieldTemplate: LayoutObjectFieldTemplate }} - widgets={allWidgets} - showErrorList={false} - disabled={saving} - > - - - -
-
-
- ) -} diff --git a/frontend/src/components/EditableTable.jsx b/frontend/src/components/EditableTable.jsx deleted file mode 100644 index 730cd1d..0000000 --- a/frontend/src/components/EditableTable.jsx +++ /dev/null @@ -1,355 +0,0 @@ -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 deleted file mode 100644 index 5acc097..0000000 --- a/frontend/src/components/FormTable.jsx +++ /dev/null @@ -1,254 +0,0 @@ -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 { allWidgets } from './widgets/AllWidgets.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, - liveValues = null -}) { - const [editingKey, setEditingKey] = useState(null) - const [addingNew, setAddingNew] = useState(false) - const [newKey, setNewKey] = useState('') - const [editingFormData, setEditingFormData] = useState(null) - - 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 - // Usar el uiSchema directamente ya que se pasa el contenido de additionalProperties desde el parent - const itemUiSchema = uiSchema || {} - - - - 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={allWidgets} - showErrorList={false} - > - - - - -
-
-
- )} - - {/* Formularios para items existentes */} - {dataKeys.map(key => ( - - - - - {key} - {data[key] && typeof data[key].enabled === 'boolean' && ( - - {data[key].enabled ? 'Enabled' : 'Disabled'} - - )} - {editingKey === key && ( - Editing - )} - - - {editingKey === key ? ( - - ) : ( - <> - } - size="xs" - variant="outline" - onClick={() => { setEditingFormData({ ...(data[key] || {}) }); setEditingKey(key) }} - /> - {allowDelete && ( - } - size="xs" - variant="outline" - colorScheme="red" - onClick={() => handleDelete(key)} - /> - )} - - )} - - - - - {liveValues && liveValues[key] !== undefined && ( - - - Live value: {String(liveValues[key])} - - - )} -
setEditingFormData(formData) : () => { }} - onSubmit={editingKey === key ? ({ formData }) => handleEdit(key, formData) : undefined} - templates={{ ObjectFieldTemplate: LayoutObjectFieldTemplate }} - widgets={allWidgets} - readonly={editingKey !== key} - showErrorList={false} - > - {editingKey === key && ( - - - - - )} -
-
-
- ))} -
- ) -} diff --git a/frontend/src/components/PlotCompleteManager.jsx b/frontend/src/components/PlotCompleteManager.jsx deleted file mode 100644 index 3e8e81b..0000000 --- a/frontend/src/components/PlotCompleteManager.jsx +++ /dev/null @@ -1,264 +0,0 @@ -import React, { useState, useEffect, useMemo } from 'react' -import { - Box, - VStack, - HStack, - Text, - Select, - Card, - CardBody, - CardHeader, - Heading, - Alert, - AlertIcon, - useColorModeValue, - Divider, - Button -} from '@chakra-ui/react' -// No necesitamos Form completo, solo FormTable -import FormTable from './FormTable.jsx' -import { getSchema, readConfig, writeConfig } from '../services/api.js' - - - -/** - * PlotCompleteManager - Gestiona plots y variables de forma simplificada - * Incluye: tabla de plots individuales + variables (sin campos estáticos de configuración) - */ -export default function PlotCompleteManager() { - const [fullData, setFullData] = useState({}) - const [plotVariables, setPlotVariables] = useState({}) - const [selectedPlotId, setSelectedPlotId] = useState('') - - const [plotSchema, setPlotSchema] = useState(null) - const [plotUiSchema, setPlotUiSchema] = 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') - - useEffect(() => { - loadData() - }, []) - - const loadData = async () => { - setLoading(true) - try { - // Cargar schemas completos - const [plotSchemaResp, variableSchemaResp] = await Promise.all([ - getSchema('plot-definitions'), - getSchema('plot-variables') - ]) - - console.log('Complete plot schema response:', plotSchemaResp) - console.log('Complete variable schema response:', variableSchemaResp) - - // Cargar datos - const [plotDataResp, variableDataResp] = await Promise.all([ - readConfig('plot-definitions'), - readConfig('plot-variables') - ]) - - console.log('Complete plot data response:', plotDataResp) - console.log('Complete variable data response:', variableDataResp) - - // Usar schemas completos - setPlotSchema(plotSchemaResp.schema) - setPlotUiSchema(plotSchemaResp.ui_schema || {}) - - // Debug para schema de variables - console.log('Variable schema structure:', variableSchemaResp.schema) - console.log('Variable schema props:', variableSchemaResp.schema?.properties) - console.log('Plot variables props:', variableSchemaResp.schema?.properties?.plot_variables) - console.log('Additional props:', variableSchemaResp.schema?.properties?.plot_variables?.additionalProperties) - console.log('Variables property:', variableSchemaResp.schema?.properties?.plot_variables?.additionalProperties?.properties?.variables) - console.log('Final variables schema:', variableSchemaResp.schema?.properties?.plot_variables?.additionalProperties?.properties?.variables?.additionalProperties) - - // Debug para UI schema de variables - console.log('Variable ui schema structure:', variableSchemaResp.ui_schema) - console.log('UI schema plot_variables:', variableSchemaResp.ui_schema?.plot_variables) - console.log('UI schema additionalProperties:', variableSchemaResp.ui_schema?.plot_variables?.additionalProperties) - console.log('UI schema variables:', variableSchemaResp.ui_schema?.plot_variables?.additionalProperties?.variables) - console.log('Final ui schema:', variableSchemaResp.ui_schema?.plot_variables?.additionalProperties?.variables?.additionalProperties) - - // Schema para variables individuales (debe contener additionalProperties) - setVariableSchema(variableSchemaResp.schema?.properties?.plot_variables?.additionalProperties?.properties?.variables) - setVariableUiSchema(variableSchemaResp.ui_schema?.plot_variables?.additionalProperties?.variables?.additionalProperties || {}) - - setFullData(plotDataResp.data || {}) - setPlotVariables(variableDataResp.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 complete plot data:', error) - setMessage(`Error loading data: ${error.message}`) - } finally { - setLoading(false) - } - } - - const saveFullData = async (newData) => { - setSaving(true) - try { - await writeConfig('plot-definitions', newData) - setFullData(newData) - setMessage('Plot configuration saved successfully') - setTimeout(() => setMessage(''), 3000) - } catch (error) { - console.error('Error saving full plot data:', error) - setMessage(`Error saving plot configuration: ${error.message}`) - } finally { - setSaving(false) - } - } - - const savePlots = async (newPlots) => { - try { - // Solo enviar plots, mantener campos técnicos automáticamente - const newFullData = { - plots: newPlots, - session_counter: fullData.session_counter || 0, - last_saved: new Date().toISOString(), - version: fullData.version || "1.0" - } - await saveFullData(newFullData) - } catch (error) { - console.error('Error saving plots:', error) - setMessage(`Error saving plots: ${error.message}`) - } - } - - const savePlotVariables = async (newVariables) => { - try { - const updatedPlotVariables = { - ...plotVariables, - [selectedPlotId]: { - variables: newVariables - } - } - - const saveData = { - plot_variables: updatedPlotVariables - } - - 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(fullData.plots || {}).map(([id, plot]) => ({ - value: id, - label: `${plot.name || id} (${id})` - })) - - if (loading) { - return Loading plot configuration... - } - - return ( - - {message && ( - - - {message} - - )} - - {/* Tabla de Plots */} - - - 📈 Plot Management - - Manage your plots: create, edit and configure - - - - {plotSchema?.properties?.plots ? ( - - ) : ( - - - Plot schema for individual plots not available - - )} - - - - - - {/* Variables del Plot */} - - - - 🔧 Plot Variables - - Plot: - - - - - - {!selectedPlotId ? ( - - - Select a plot to manage its variables - - ) : variableSchema ? ( - - ) : ( - - - Variable schema not available - - )} - - - - ) -} diff --git a/frontend/src/components/PlotFormManager.jsx b/frontend/src/components/PlotFormManager.jsx deleted file mode 100644 index d85be5c..0000000 --- a/frontend/src/components/PlotFormManager.jsx +++ /dev/null @@ -1,402 +0,0 @@ -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 || {}) - - console.log('Plot full schema:', plotSchemaResp.schema?.properties?.plots) - console.log('Plot full uiSchema:', 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/PlotManager.jsx b/frontend/src/components/PlotManager.jsx index cba91c1..803db29 100644 --- a/frontend/src/components/PlotManager.jsx +++ b/frontend/src/components/PlotManager.jsx @@ -14,31 +14,27 @@ import { 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 + Spinner, + Select, + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalFooter, + ModalBody, + ModalCloseButton, + Alert, + AlertIcon } from '@chakra-ui/react' +import { ChevronDownIcon, ChevronUpIcon } from '@chakra-ui/icons' import Form from '@rjsf/chakra-ui' import validator from '@rjsf/validator-ajv8' import allWidgets from './widgets/AllWidgets' @@ -47,14 +43,64 @@ 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(true) +// Confirmation Dialog Component for deletion operations +function ConfirmationDialog({ isOpen, onClose, onConfirm, title, message, itemName, confirmButtonText = "Delete" }) { + return ( + + + + + + ⚠️ + {title} + + + + + + + {message} + + {itemName && ( + + + Item to delete: "{itemName}" + + + )} + + + + This action cannot be undone. Make sure you want to proceed. + + + + + + + + + + + ) +} + +// Collapsible Plot Items Form - Each item in the array is individually collapsible +function CollapsiblePlotItemsForm({ data, schema, uiSchema, onSave, title, icon, getItemLabel, isExpanded, onToggleExpansion }) { const [formData, setFormData] = useState(data) + const [expandedItems, setExpandedItems] = useState(new Set()) + const [confirmDelete, setConfirmDelete] = useState({ isOpen: false, index: null, itemName: '' }) useEffect(() => { - setFormData(data) - }, [data]) + // Solo actualizar formData si data realmente cambió en contenido + if (JSON.stringify(data) !== JSON.stringify(formData)) { + setFormData(data) + } + }, [data]) // Removed formData from dependencies to avoid infinite loop if (!schema || !formData) { return ( @@ -69,7 +115,84 @@ function CollapsiblePlotForm({ data, schema, uiSchema, onSave, title, icon, getI ) } - const items = formData[Object.keys(formData)[0]] || [] + const arrayKey = Object.keys(formData)[0] + const items = formData[arrayKey] || [] + + const toggleItemExpansion = (index) => { + const newExpanded = new Set(expandedItems) + if (newExpanded.has(index)) { + newExpanded.delete(index) + } else { + newExpanded.add(index) + } + setExpandedItems(newExpanded) + } + + const updateItem = (index, newItemData) => { + const newItems = [...items] + newItems[index] = newItemData + const newFormData = { ...formData, [arrayKey]: newItems } + setFormData(newFormData) + } + + const addItem = () => { + const newItem = {} + const newItems = [...items, newItem] + const newFormData = { ...formData, [arrayKey]: newItems } + setFormData(newFormData) + setExpandedItems(new Set([...expandedItems, items.length])) + } + + const removeItem = (index) => { + const newItems = items.filter((_, i) => i !== index) + const newFormData = { ...formData, [arrayKey]: newItems } + setFormData(newFormData) + // Update expanded items indices + const newExpanded = new Set() + expandedItems.forEach(i => { + if (i < index) newExpanded.add(i) + else if (i > index) newExpanded.add(i - 1) + }) + setExpandedItems(newExpanded) + // Close confirmation dialog + setConfirmDelete({ isOpen: false, index: null, itemName: '' }) + } + + const handleDeleteClick = (index) => { + const item = items[index] + const itemName = getItemLabel ? getItemLabel(item) : (item.name || item.id || `Item ${index + 1}`) + setConfirmDelete({ + isOpen: true, + index, + itemName + }) + } + + const handleConfirmDelete = () => { + if (confirmDelete.index !== null) { + removeItem(confirmDelete.index) + } + } + + const handleCancelDelete = () => { + setConfirmDelete({ isOpen: false, index: null, itemName: '' }) + } + + const saveChanges = async () => { + try { + await onSave(formData) + // No hacer nada con la expansión aquí - será manejado por el componente padre + } catch (error) { + console.error('Error saving:', error) + } + } + + // Get item schema from the array schema + const itemSchema = schema?.properties?.[arrayKey]?.items || {} + + // Get item UI schema - extract from the nested structure + const arrayUiSchema = uiSchema?.[arrayKey] || {} + const itemUiSchema = arrayUiSchema.items || {} return ( @@ -81,276 +204,428 @@ function CollapsiblePlotForm({ data, schema, uiSchema, onSave, title, icon, getI {items.length} item{items.length !== 1 ? 's' : ''} configured - + + {/* Si se proporciona toggle de expansión externa, agregar botón de colapso */} + {onToggleExpansion && ( + + )} + + + - - {/* Show summary when collapsed */} - {!isOpen && items.length > 0 && ( - - Quick Overview: - - {items.slice(0, 5).map((item, index) => ( - - {getItemLabel ? getItemLabel(item) : (item.name || item.id || `Item ${index + 1}`)} - - ))} - {items.length > 5 && ( - - +{items.length - 5} more... - - )} - - - )} - + {/* Usar Collapse si se proporciona estado de expansión externo */} + {onToggleExpansion ? ( + + + {items.length === 0 ? ( + + + No items configured yet + + + + ) : ( + + {items.map((item, index) => { + const isItemExpanded = expandedItems.has(index) + const itemLabel = getItemLabel ? getItemLabel(item) : (item.name || item.id || `Item ${index + 1}`) + + return ( + + + + + + #{index + 1} + + + + + + + +
updateItem(index, newItemData)} + > +
{/* Prevents form buttons from showing */} +
+
+
+
+ ) + })} +
+ )} +
+
+ ) : ( + + {items.length === 0 ? ( + + + No items configured yet + + + + ) : ( + + {items.map((item, index) => { + const isItemExpanded = expandedItems.has(index) + const itemLabel = getItemLabel ? getItemLabel(item) : (item.name || item.id || `Item ${index + 1}`) + + return ( + + + + + + #{index + 1} + + + + + + + +
updateItem(index, newItemData)} + > +
{/* Prevents form buttons from showing */} +
+
+
+
+ ) + })} +
+ )} +
+ )} + + {/* Confirmation Dialog for Deletion */} + +
+ ) +} + +// Collapsible Plot Component +function CollapsiblePlotChart({ plotDefinition, plotVariables, onConfigUpdate, onReloadConfig, onRemove, isExpanded, onToggleExpansion }) { + + return ( + + + + + 📈 {plotDefinition.name || plotDefinition.id} + + {plotDefinition.time_window}s window • {plotVariables?.length || 0} variables + + + + + + + + + -
onSave(formData)} - onChange={({ formData }) => setFormData(formData)} - > - - - - -
+
) } -// Pure RJSF Plot Manager Component +// Simple 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 [plotVariablesConfig, setPlotVariablesConfig] = useState(null) + const [plotsSchemaData, setPlotsSchemaData] = useState(null) + const [plotVariablesSchemaData, setPlotVariablesSchemaData] = useState(null) const [selectedPlotId, setSelectedPlotId] = useState('') const [loading, setLoading] = useState(true) - const [actionLoading, setActionLoading] = useState({}) - + // Estado para preservar qué plots están expandidos/colapsados + const [expandedPlots, setExpandedPlots] = useState(new Set()) + // Estado para preservar si la configuración de plot definitions está expandida + const [configExpanded, setConfigExpanded] = useState(true) 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 })) - } + // Auto expandir todos los plots cuando se cargue la configuración + useEffect(() => { + if (plotsConfig?.plots) { + const allIds = new Set(plotsConfig.plots.map(plot => plot.id)) + setExpandedPlots(allIds) + } + }, [plotsConfig]) const loadPlotData = useCallback(async () => { + setLoading(true) try { - setLoading(true) - const [ - plotsData, - plotsSchemaResponse, - plotsVariablesSchemaResponse, - plotsConfigData, - plotsVariablesConfigData - ] = await Promise.all([ - api.getPlots(), - api.getSchema('plot-definitions'), - api.getSchema('plot-variables'), + const [plotsData, plotVariablesData, plotsSchemaResponse, plotVariablesSchemaResponse] = await Promise.all([ api.readConfig('plot-definitions'), - api.readConfig('plot-variables') + api.readConfig('plot-variables'), + api.getSchema('plot-definitions'), + api.getSchema('plot-variables') ]) - setPlots(plotsData?.plots || {}) + setPlotsConfig(plotsData) + setPlotVariablesConfig(plotVariablesData) setPlotsSchemaData(plotsSchemaResponse) - setPlotsVariablesSchemaData(plotsVariablesSchemaResponse) - setPlotsConfig(plotsConfigData) - setPlotsVariablesConfig(plotsVariablesConfigData) + setPlotVariablesSchemaData(plotVariablesSchemaResponse) // Auto-select first plot if none selected - if (!selectedPlotId && plotsConfigData?.plots?.length > 0) { - setSelectedPlotId(plotsConfigData.plots[0].id) + if (!selectedPlotId && plotsData?.plots?.length > 0) { + setSelectedPlotId(plotsData.plots[0].id) } + + console.log('✅ Plot data loaded:', { + plots: plotsData?.plots?.length || 0, + plotVariables: plotVariablesData?.variables?.length || 0 + }) } catch (error) { toast({ - title: '❌ Failed to load plot data', + title: '❌ Failed to load plot configurations', description: error.message, status: 'error', - duration: 3000 + duration: 5000 }) } finally { setLoading(false) } - }, [toast]) + }, [selectedPlotId, toast]) + + // Función para actualizar configuración de un plot específico sin recargar todo + const updatePlotConfig = async (plotId, newConfig) => { + try { + // Actualizar solo el plot específico en la configuración local + const updatedPlots = plotsConfig?.plots?.map(plot => + plot.id === plotId ? { ...plot, ...newConfig } : plot + ) || [] + + const updatedConfig = { + ...plotsConfig, + plots: updatedPlots + } + + // Guardar en el backend + await api.writeConfig('plot-definitions', updatedConfig) + + // Actualizar estado local + setPlotsConfig(updatedConfig) + + console.log(`✅ Plot ${plotId} configuration updated locally`) + } catch (error) { + console.error(`❌ Failed to update plot ${plotId} config:`, error) + throw error + } + } + + const savePlotsConfig = async (formData) => { + try { + await api.writeConfig('plot-definitions', formData) + setPlotsConfig(formData) + // Mantener la configuración expandida después de Apply + setConfigExpanded(true) + toast({ + title: '✅ Plot definitions saved', + status: 'success', + duration: 3000 + }) + triggerVariableRefresh() + } catch (error) { + toast({ + title: '❌ Failed to save plot definitions', + description: error.message, + status: 'error', + duration: 5000 + }) + } + } + + const savePlotVariables = async (formData) => { + try { + await api.writeConfig('plot-variables', formData) + setPlotVariablesConfig(formData) + toast({ + title: '✅ Plot variables saved', + status: 'success', + duration: 3000 + }) + } catch (error) { + toast({ + title: '❌ Failed to save plot variables', + description: error.message, + status: 'error', + duration: 5000 + }) + } + } // 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 || [] + // Helper to get dataset IDs for dropdown + const getDatasetIds = () => { + // This would normally come from dataset definitions + // For now, return some placeholder values + return ['DAR', 'Fast', 'Slow'] // TODO: Get from actual dataset definitions } - // Type 3 Pattern Helper Functions - // Get filtered variables for selected plot + // 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]) + + // Functions to handle plot expansion state + const togglePlotExpansion = (plotId) => { + const newExpanded = new Set(expandedPlots) + if (newExpanded.has(plotId)) { + newExpanded.delete(plotId) + } else { + newExpanded.add(plotId) + } + setExpandedPlots(newExpanded) + } + + const isPlotExpanded = (plotId) => { + return expandedPlots.has(plotId) + } + + // Function to handle configuration expansion toggle + const toggleConfigExpansion = () => { + setConfigExpanded(!configExpanded) + } + + // Helper functions for Type 3 form pattern (Plot Variables) const getSelectedPlotVariables = () => { - if (!plotsVariablesConfig?.variables || !selectedPlotId) { - return { variables: [] } + if (!selectedPlotId || !plotVariablesConfig?.variables) return { variables: [] } + + const plotVars = plotVariablesConfig.variables.find( + item => item.plot_id === selectedPlotId + ) + return plotVars || { plot_id: selectedPlotId, variables: [] } + } + + const updateSelectedPlotVariables = (formData) => { + if (!plotVariablesConfig?.variables) { + // Initialize plotVariablesConfig if it doesn't exist + const newConfig = { + variables: [{ plot_id: selectedPlotId, ...formData }] + } + setPlotVariablesConfig(newConfig) + return newConfig } - 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 + const existingIndex = plotVariablesConfig.variables.findIndex( + item => item.plot_id === selectedPlotId ) - - // If plot not found, add new entry - if (!plotsVariablesConfig.variables.find(v => v.plot_id === selectedPlotId)) { - updatedVariables.push({ - plot_id: selectedPlotId, - ...newVariableData - }) + + const updatedVars = [...plotVariablesConfig.variables] + const newVarData = { plot_id: selectedPlotId, ...formData } + + if (existingIndex >= 0) { + updatedVars[existingIndex] = newVarData + } else { + updatedVars.push(newVarData) } - - 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) + + const updatedConfig = { + ...plotVariablesConfig, + variables: updatedVars } + + setPlotVariablesConfig(updatedConfig) + return updatedConfig } useEffect(() => { @@ -359,16 +634,19 @@ export default function PlotManager() { if (loading) { return ( - + - Loading plot configurations... + + + Loading plot configurations... + ) } return ( - + 📈 Plot Manager @@ -377,12 +655,12 @@ export default function PlotManager() { - {/* Active Plot Sessions with Real Chart.js Plots */} - + {/* Real-time Charts Section */} + - 🎛️ Active Plot Sessions + 🔴 Real-time Plot Sessions - Real-time Chart.js plots with streaming data from PLC + Live charts for configured plot sessions - click to expand/collapse @@ -391,14 +669,17 @@ export default function PlotManager() { No plot sessions configured. Create plot definitions below to get started. ) : ( - + {getPlotDefinitions().map((plotDef) => ( - {}} + isExpanded={isPlotExpanded(plotDef.id)} + onToggleExpansion={togglePlotExpansion} /> ))} @@ -408,204 +689,204 @@ export default function PlotManager() { - {/* RJSF Configuration Forms */} - - {/* Plot Definitions - Collapsible */} - `${item.name || item.id} (${item.time_window || 60}s)`} - /> + {/* Plot Definitions - Collapsible */} + `${item.name || item.id} (${item.time_window || 60}s)`} + isExpanded={configExpanded} + onToggleExpansion={toggleConfigExpansion} + /> - {/* Plot Variables Configuration */} - - - ⚙️ Plot Variables Configuration - - Select a plot session, then configure its variables and visual settings - - + {/* Plot Variables Configuration - Type 3 Form Pattern */} + + + ⚙️ Plot Variables Configuration + + Select a plot, then configure which variables are displayed in that plot session + + + + {/* Step 1: Plot Selector (Combo) */} + + + + 🎯 Select Plot Session + + + {getPlotDefinitions().length === 0 && ( + + ⚠️ No plots available. Configure plot definitions first above. - - - {/* Step 1: Plot Selector (Combo) */} - - - - 🎯 Select Plot Session - - - {availablePlots.length === 0 && ( - - ⚠️ No plot sessions available. Configure plot definitions first in the "Plot Definitions" tab. - - )} - + )} + - {/* Variables Configuration Form */} - {selectedPlotId && ( - - - - ⚙️ Configure Variables for Plot "{selectedPlotId}" - - - {/* Simplified schema for selected plot variables */} - {(() => { - const selectedPlotVars = getSelectedPlotVariables() - - // Schema for this plot's variables - const singlePlotSchema = { + {/* Variables Configuration Form */} + {selectedPlotId && ( + + + + ⚙️ Configure Variables for Plot "{selectedPlotId}" + + + {/* Simplified schema for selected plot variables */} + {(() => { + const selectedPlotVars = getSelectedPlotVariables() + + // Schema for this plot's variables - match the real schema + const singlePlotSchema = { + type: "object", + properties: { + variables: { + type: "array", + title: "Variables", + description: `Variables to display in plot ${selectedPlotId}`, + items: { 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"] - } + variable_name: { + type: "string", + title: "Variable Name", + description: "Name of the variable from the dataset" + }, + label: { + type: "string", + title: "Display Label", + description: "Label shown in the plot legend" + }, + color: { + type: "string", + title: "Plot Color", + pattern: "^#[0-9A-Fa-f]{6}$", + default: "#3498db" + }, + 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" + }, + enabled: { + type: "boolean", + title: "Show in Plot", + default: true } } } + } + } + } - 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" - } - } - } + // UI Schema for layout + const singlePlotUiSchema = { + "ui:layout": [[ + { "name": "variables", "width": 12 } + ]], + variables: { + items: { + "ui:layout": [[ + { "name": "variable_name", "width": 4 }, + { "name": "label", "width": 3 }, + { "name": "color", "width": 2 }, + { "name": "line_width", "width": 1 }, + { "name": "y_axis", "width": 1 }, + { "name": "enabled", "width": 1 } + ]], + variable_name: { + "ui:widget": "variableSelector", + "ui:placeholder": "Search and select variable from datasets...", + "ui:help": "🔍 Search and select a variable from the configured datasets (includes live values and metadata)" + }, + label: { + "ui:widget": "text", + "ui:placeholder": "Chart legend label...", + "ui:help": "📊 Label shown in the plot legend for this variable" + }, + color: { + "ui:widget": "color", + "ui:help": "🎨 Select the color for this variable in the plot", + "ui:placeholder": "#3498db" + }, + line_width: { + "ui:widget": "updown", + "ui:help": "📏 Thickness of the line in the plot (1-10)", + "ui:options": { "step": 1, "min": 1, "max": 10 } + }, + y_axis: { + "ui:widget": "select", + "ui:help": "📈 Which Y-axis to use for this variable" + }, + enabled: { + "ui:widget": "checkbox", + "ui:help": "✅ Whether to show this variable in the plot" } + } + } + } - return ( -
{ - 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)} - > - - - - -
- ) - })()} -
- )} + return ( +
{ + const updatedConfig = updateSelectedPlotVariables(formData) + savePlotVariables(updatedConfig).then(() => { + // Additional trigger after successful save + triggerVariableRefresh?.() + }) + }} + onChange={({ formData }) => updateSelectedPlotVariables(formData)} + > + + + + +
+ ) + })()} +
+ )} - {!selectedPlotId && availablePlots.length > 0 && ( - - - 👆 Select a plot session above to configure its variables - - - )} -
-
-
- - - + {!selectedPlotId && getPlotDefinitions().length > 0 && ( + + + 👆 Select a plot above to configure its variables + + + )} +
+
+
) } diff --git a/frontend/src/components/PlotManagerSimple.jsx b/frontend/src/components/PlotManagerSimple.jsx deleted file mode 100644 index 0381ddd..0000000 --- a/frontend/src/components/PlotManagerSimple.jsx +++ /dev/null @@ -1,884 +0,0 @@ -import React, { useState, useEffect, useCallback } from 'react' -import { - Box, - Card, - CardBody, - CardHeader, - Button, - Text, - Grid, - Flex, - Spacer, - HStack, - VStack, - useColorModeValue, - useToast, - Heading, - Badge, - Divider, - Accordion, - AccordionItem, - AccordionButton, - AccordionPanel, - Collapse, - useDisclosure, - Spinner, - Select, - Modal, - ModalOverlay, - ModalContent, - ModalHeader, - ModalFooter, - ModalBody, - ModalCloseButton, - Alert, - AlertIcon -} from '@chakra-ui/react' -import { ChevronDownIcon, ChevronUpIcon } from '@chakra-ui/icons' -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' - -// Confirmation Dialog Component for deletion operations -function ConfirmationDialog({ isOpen, onClose, onConfirm, title, message, itemName, confirmButtonText = "Delete" }) { - return ( - - - - - - ⚠️ - {title} - - - - - - - {message} - - {itemName && ( - - - Item to delete: "{itemName}" - - - )} - - - - This action cannot be undone. Make sure you want to proceed. - - - - - - - - - - - ) -} - -// Collapsible Plot Items Form - Each item in the array is individually collapsible -function CollapsiblePlotItemsForm({ data, schema, uiSchema, onSave, title, icon, getItemLabel, isExpanded, onToggleExpansion }) { - const [formData, setFormData] = useState(data) - const [expandedItems, setExpandedItems] = useState(new Set()) - const [confirmDelete, setConfirmDelete] = useState({ isOpen: false, index: null, itemName: '' }) - - useEffect(() => { - // Solo actualizar formData si data realmente cambió en contenido - if (JSON.stringify(data) !== JSON.stringify(formData)) { - setFormData(data) - } - }, [data]) // Removed formData from dependencies to avoid infinite loop - - if (!schema || !formData) { - return ( - - - - - Loading {title}... - - - - ) - } - - const arrayKey = Object.keys(formData)[0] - const items = formData[arrayKey] || [] - - const toggleItemExpansion = (index) => { - const newExpanded = new Set(expandedItems) - if (newExpanded.has(index)) { - newExpanded.delete(index) - } else { - newExpanded.add(index) - } - setExpandedItems(newExpanded) - } - - const updateItem = (index, newItemData) => { - const newItems = [...items] - newItems[index] = newItemData - const newFormData = { ...formData, [arrayKey]: newItems } - setFormData(newFormData) - } - - const addItem = () => { - const newItem = {} - const newItems = [...items, newItem] - const newFormData = { ...formData, [arrayKey]: newItems } - setFormData(newFormData) - setExpandedItems(new Set([...expandedItems, items.length])) - } - - const removeItem = (index) => { - const newItems = items.filter((_, i) => i !== index) - const newFormData = { ...formData, [arrayKey]: newItems } - setFormData(newFormData) - // Update expanded items indices - const newExpanded = new Set() - expandedItems.forEach(i => { - if (i < index) newExpanded.add(i) - else if (i > index) newExpanded.add(i - 1) - }) - setExpandedItems(newExpanded) - // Close confirmation dialog - setConfirmDelete({ isOpen: false, index: null, itemName: '' }) - } - - const handleDeleteClick = (index) => { - const item = items[index] - const itemName = getItemLabel ? getItemLabel(item) : (item.name || item.id || `Item ${index + 1}`) - setConfirmDelete({ - isOpen: true, - index, - itemName - }) - } - - const handleConfirmDelete = () => { - if (confirmDelete.index !== null) { - removeItem(confirmDelete.index) - } - } - - const handleCancelDelete = () => { - setConfirmDelete({ isOpen: false, index: null, itemName: '' }) - } - - const saveChanges = async () => { - try { - await onSave(formData) - // No hacer nada con la expansión aquí - será manejado por el componente padre - } catch (error) { - console.error('Error saving:', error) - } - } - - // Get item schema from the array schema - const itemSchema = schema?.properties?.[arrayKey]?.items || {} - - // Get item UI schema - extract from the nested structure - const arrayUiSchema = uiSchema?.[arrayKey] || {} - const itemUiSchema = arrayUiSchema.items || {} - - return ( - - - - - {icon} {title} - - {items.length} item{items.length !== 1 ? 's' : ''} configured - - - - {/* Si se proporciona toggle de expansión externa, agregar botón de colapso */} - {onToggleExpansion && ( - - )} - - - - - - - {/* Usar Collapse si se proporciona estado de expansión externo */} - {onToggleExpansion ? ( - - - {items.length === 0 ? ( - - - No items configured yet - - - - ) : ( - - {items.map((item, index) => { - const isItemExpanded = expandedItems.has(index) - const itemLabel = getItemLabel ? getItemLabel(item) : (item.name || item.id || `Item ${index + 1}`) - - return ( - - - - - - #{index + 1} - - - - - - - -
updateItem(index, newItemData)} - > -
{/* Prevents form buttons from showing */} -
-
-
-
- ) - })} -
- )} -
-
- ) : ( - - {items.length === 0 ? ( - - - No items configured yet - - - - ) : ( - - {items.map((item, index) => { - const isItemExpanded = expandedItems.has(index) - const itemLabel = getItemLabel ? getItemLabel(item) : (item.name || item.id || `Item ${index + 1}`) - - return ( - - - - - - #{index + 1} - - - - - - - -
updateItem(index, newItemData)} - > -
{/* Prevents form buttons from showing */} -
-
-
-
- ) - })} -
- )} -
- )} - - {/* Confirmation Dialog for Deletion */} - -
- ) -} - -// Collapsible Plot Component -function CollapsiblePlotChart({ plotDefinition, plotVariables, onConfigUpdate, onReloadConfig, onRemove, isExpanded, onToggleExpansion }) { - - return ( - - - - - 📈 {plotDefinition.name || plotDefinition.id} - - {plotDefinition.time_window}s window • {plotVariables?.length || 0} variables - - - - - - - - - - - - - - - ) -} - -// Simple Plot Manager Component -export default function PlotManager() { - const { triggerVariableRefresh } = useVariableContext() - const [plotsConfig, setPlotsConfig] = useState(null) - const [plotVariablesConfig, setPlotVariablesConfig] = useState(null) - const [plotsSchemaData, setPlotsSchemaData] = useState(null) - const [plotVariablesSchemaData, setPlotVariablesSchemaData] = useState(null) - const [selectedPlotId, setSelectedPlotId] = useState('') - const [loading, setLoading] = useState(true) - // Estado para preservar qué plots están expandidos/colapsados - const [expandedPlots, setExpandedPlots] = useState(new Set()) - // Estado para preservar si la configuración de plot definitions está expandida - const [configExpanded, setConfigExpanded] = useState(false) - const toast = useToast() - - const loadPlotData = useCallback(async () => { - setLoading(true) - try { - const [plotsData, plotVariablesData, plotsSchemaResponse, plotVariablesSchemaResponse] = await Promise.all([ - api.readConfig('plot-definitions'), - api.readConfig('plot-variables'), - api.getSchema('plot-definitions'), - api.getSchema('plot-variables') - ]) - - setPlotsConfig(plotsData) - setPlotVariablesConfig(plotVariablesData) - setPlotsSchemaData(plotsSchemaResponse) - setPlotVariablesSchemaData(plotVariablesSchemaResponse) - - // Auto-select first plot if none selected - if (!selectedPlotId && plotsData?.plots?.length > 0) { - setSelectedPlotId(plotsData.plots[0].id) - } - - console.log('✅ Plot data loaded:', { - plots: plotsData?.plots?.length || 0, - plotVariables: plotVariablesData?.variables?.length || 0 - }) - } catch (error) { - toast({ - title: '❌ Failed to load plot configurations', - description: error.message, - status: 'error', - duration: 5000 - }) - } finally { - setLoading(false) - } - }, [selectedPlotId, toast]) - - // Función para actualizar configuración de un plot específico sin recargar todo - const updatePlotConfig = async (plotId, newConfig) => { - try { - // Actualizar solo el plot específico en la configuración local - const updatedPlots = plotsConfig?.plots?.map(plot => - plot.id === plotId ? { ...plot, ...newConfig } : plot - ) || [] - - const updatedConfig = { - ...plotsConfig, - plots: updatedPlots - } - - // Guardar en el backend - await api.writeConfig('plot-definitions', updatedConfig) - - // Actualizar estado local - setPlotsConfig(updatedConfig) - - console.log(`✅ Plot ${plotId} configuration updated locally`) - } catch (error) { - console.error(`❌ Failed to update plot ${plotId} config:`, error) - throw error - } - } - - const savePlotsConfig = async (formData) => { - try { - await api.writeConfig('plot-definitions', formData) - setPlotsConfig(formData) - // Mantener la configuración expandida después de Apply - setConfigExpanded(true) - toast({ - title: '✅ Plot definitions saved', - status: 'success', - duration: 3000 - }) - triggerVariableRefresh() - } catch (error) { - toast({ - title: '❌ Failed to save plot definitions', - description: error.message, - status: 'error', - duration: 5000 - }) - } - } - - const savePlotVariables = async (formData) => { - try { - await api.writeConfig('plot-variables', formData) - setPlotVariablesConfig(formData) - toast({ - title: '✅ Plot variables saved', - status: 'success', - duration: 3000 - }) - } catch (error) { - toast({ - title: '❌ Failed to save plot variables', - description: error.message, - status: 'error', - duration: 5000 - }) - } - } - - // Helper function to get plot definitions from config - const getPlotDefinitions = () => { - return plotsConfig?.plots || [] - } - - // Helper to get dataset IDs for dropdown - const getDatasetIds = () => { - // This would normally come from dataset definitions - // For now, return some placeholder values - 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]) - - // Functions to handle plot expansion state - const togglePlotExpansion = (plotId) => { - const newExpanded = new Set(expandedPlots) - if (newExpanded.has(plotId)) { - newExpanded.delete(plotId) - } else { - newExpanded.add(plotId) - } - setExpandedPlots(newExpanded) - } - - const isPlotExpanded = (plotId) => { - return expandedPlots.has(plotId) - } - - // Function to handle configuration expansion toggle - const toggleConfigExpansion = () => { - setConfigExpanded(!configExpanded) - } - - // Helper functions for Type 3 form pattern (Plot Variables) - const getSelectedPlotVariables = () => { - if (!selectedPlotId || !plotVariablesConfig?.variables) return { variables: [] } - - const plotVars = plotVariablesConfig.variables.find( - item => item.plot_id === selectedPlotId - ) - return plotVars || { plot_id: selectedPlotId, variables: [] } - } - - const updateSelectedPlotVariables = (formData) => { - if (!plotVariablesConfig?.variables) { - // Initialize plotVariablesConfig if it doesn't exist - const newConfig = { - variables: [{ plot_id: selectedPlotId, ...formData }] - } - setPlotVariablesConfig(newConfig) - return newConfig - } - - const existingIndex = plotVariablesConfig.variables.findIndex( - item => item.plot_id === selectedPlotId - ) - - const updatedVars = [...plotVariablesConfig.variables] - const newVarData = { plot_id: selectedPlotId, ...formData } - - if (existingIndex >= 0) { - updatedVars[existingIndex] = newVarData - } else { - updatedVars.push(newVarData) - } - - const updatedConfig = { - ...plotVariablesConfig, - variables: updatedVars - } - - setPlotVariablesConfig(updatedConfig) - return updatedConfig - } - - useEffect(() => { - loadPlotData() - }, [loadPlotData]) - - if (loading) { - return ( - - - - - Loading plot configurations... - - - - ) - } - - return ( - - - 📈 Plot Manager - - - - - {/* Real-time Charts Section */} - - - 🔴 Real-time Plot Sessions - - Live charts for configured plot sessions - click to expand/collapse - - - - {getPlotDefinitions().length === 0 ? ( - - No plot sessions configured. Create plot definitions below to get started. - - ) : ( - - {getPlotDefinitions().map((plotDef) => ( - {}} - isExpanded={isPlotExpanded(plotDef.id)} - onToggleExpansion={togglePlotExpansion} - /> - ))} - - )} - - - - - - {/* Plot Definitions - Collapsible */} - `${item.name || item.id} (${item.time_window || 60}s)`} - isExpanded={configExpanded} - onToggleExpansion={toggleConfigExpansion} - /> - - {/* Plot Variables Configuration - Type 3 Form Pattern */} - - - ⚙️ Plot Variables Configuration - - Select a plot, then configure which variables are displayed in that plot session - - - - {/* Step 1: Plot Selector (Combo) */} - - - - 🎯 Select Plot Session - - - {getPlotDefinitions().length === 0 && ( - - ⚠️ No plots available. Configure plot definitions first above. - - )} - - - {/* Variables Configuration Form */} - {selectedPlotId && ( - - - - ⚙️ Configure Variables for Plot "{selectedPlotId}" - - - {/* Simplified schema for selected plot variables */} - {(() => { - const selectedPlotVars = getSelectedPlotVariables() - - // Schema for this plot's variables - match the real schema - const singlePlotSchema = { - type: "object", - properties: { - variables: { - type: "array", - title: "Variables", - description: `Variables to display in plot ${selectedPlotId}`, - items: { - type: "object", - properties: { - variable_name: { - type: "string", - title: "Variable Name", - description: "Name of the variable from the dataset" - }, - label: { - type: "string", - title: "Display Label", - description: "Label shown in the plot legend" - }, - color: { - type: "string", - title: "Plot Color", - pattern: "^#[0-9A-Fa-f]{6}$", - default: "#3498db" - }, - 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" - }, - enabled: { - type: "boolean", - title: "Show in Plot", - default: true - } - } - } - } - } - } - - // UI Schema for layout - const singlePlotUiSchema = { - "ui:layout": [[ - { "name": "variables", "width": 12 } - ]], - variables: { - items: { - "ui:layout": [[ - { "name": "variable_name", "width": 4 }, - { "name": "label", "width": 3 }, - { "name": "color", "width": 2 }, - { "name": "line_width", "width": 1 }, - { "name": "y_axis", "width": 1 }, - { "name": "enabled", "width": 1 } - ]], - variable_name: { - "ui:widget": "variableSelector", - "ui:placeholder": "Search and select variable from datasets...", - "ui:help": "🔍 Search and select a variable from the configured datasets (includes live values and metadata)" - }, - label: { - "ui:widget": "text", - "ui:placeholder": "Chart legend label...", - "ui:help": "📊 Label shown in the plot legend for this variable" - }, - color: { - "ui:widget": "color", - "ui:help": "🎨 Select the color for this variable in the plot", - "ui:placeholder": "#3498db" - }, - line_width: { - "ui:widget": "updown", - "ui:help": "📏 Thickness of the line in the plot (1-10)", - "ui:options": { "step": 1, "min": 1, "max": 10 } - }, - y_axis: { - "ui:widget": "select", - "ui:help": "📈 Which Y-axis to use for this variable" - }, - enabled: { - "ui:widget": "checkbox", - "ui:help": "✅ Whether to show this variable in the plot" - } - } - } - } - - return ( -
{ - const updatedConfig = updateSelectedPlotVariables(formData) - savePlotVariables(updatedConfig).then(() => { - // Additional trigger after successful save - triggerVariableRefresh?.() - }) - }} - onChange={({ formData }) => updateSelectedPlotVariables(formData)} - > - - - - -
- ) - })()} -
- )} - - {!selectedPlotId && getPlotDefinitions().length > 0 && ( - - - 👆 Select a plot above to configure its variables - - - )} -
-
-
-
- ) -} diff --git a/frontend/src/components/PlotRealtimeViewer.jsx b/frontend/src/components/PlotRealtimeViewer.jsx deleted file mode 100644 index 04061d3..0000000 --- a/frontend/src/components/PlotRealtimeViewer.jsx +++ /dev/null @@ -1,284 +0,0 @@ -import React, { useEffect, useMemo, useRef, useState } from 'react' -import { - Box, - VStack, - HStack, - Text, - Button, - Card, - CardBody, - CardHeader, - Heading, - useColorModeValue, - Badge, - IconButton, - Divider, - Spacer, - Modal, - ModalOverlay, - ModalContent, - ModalHeader, - ModalCloseButton, - ModalBody, - useDisclosure, -} from '@chakra-ui/react' -import { EditIcon, SettingsIcon, DeleteIcon, ViewIcon } from '@chakra-ui/icons' -import ChartjsPlot from './ChartjsPlot.jsx' -import { useCoordinatedPolling } from '../hooks/useCoordinatedConnection' - -export default function PlotRealtimeViewer() { - const [sessions, setSessions] = useState(new Map()) - const muted = useColorModeValue('gray.600', 'gray.300') - - // Usar polling coordinado para sesiones - const { data: sessionsData, isLeader, isConnected } = useCoordinatedPolling( - 'plot_sessions', - async () => { - const res = await fetch('/api/plots') - return res.json() - }, - 5000 // 5 segundos - ) - - // Procesar datos de sesiones cuando llegan - useEffect(() => { - if (sessionsData && sessionsData.sessions) { - setSessions(prev => { - const next = new Map(prev) - const incomingIds = new Set() - for (const s of sessionsData.sessions) { - incomingIds.add(s.session_id) - const existing = next.get(s.session_id) - if (existing) { - // Mutate existing object to preserve reference - existing.name = s.name - existing.is_active = s.is_active - existing.is_paused = s.is_paused - existing.variables_count = s.variables_count - } else { - next.set(s.session_id, { ...s }) - } - } - // Remove sessions not present anymore - for (const id of Array.from(next.keys())) { - if (!incomingIds.has(id)) next.delete(id) - } - return next - }) - } else { - setSessions(new Map()) - } - }, [sessionsData]) - - const refreshSession = async (sessionId) => { - try { - const res = await fetch(`/api/plots/${sessionId}/config`) - const data = await res.json() - if (data && data.success && data.config) { - setSessions(prev => { - const n = new Map(prev) - const existing = n.get(sessionId) - const varsCount = Array.isArray(data.config.variables) - ? data.config.variables.length - : (data.config.variables ? Object.keys(data.config.variables).length : (existing?.variables_count || 0)) - if (existing) { - existing.name = data.config.name - existing.is_active = data.config.is_active - existing.is_paused = data.config.is_paused - existing.variables_count = varsCount - } else { - n.set(sessionId, { - session_id: sessionId, - name: data.config.name, - is_active: data.config.is_active, - is_paused: data.config.is_paused, - variables_count: varsCount, - }) - } - return n - }) - } - } catch { /* ignore */ } - } - - const controlSession = async (sessionId, action) => { - try { - await fetch(`/api/plots/${sessionId}/control`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ action }), - }) - await refreshSession(sessionId) - } catch { /* ignore */ } - } - - const sessionsList = useMemo(() => Array.from(sessions.values()), [sessions]) - - if (!isConnected && sessionsList.length === 0) { - return Cargando sesiones de plots… - } - - if (sessionsList.length === 0) { - return ( - - - No hay sesiones de plot. Cree o edite plots en la sección superior. - - - ) - } - - return ( - - {sessionsList.map((session) => ( - - ))} - - ) -} - -function PlotRealtimeCard({ session, onControl, onRefresh }) { - const cardBg = useColorModeValue('white', 'gray.700') - const borderColor = useColorModeValue('gray.200', 'gray.600') - const muted = useColorModeValue('gray.600', 'gray.300') - const chartControlsRef = useRef(null) - const { isOpen: isFullscreen, onOpen: openFullscreen, onClose: closeFullscreen } = useDisclosure() - - const handleChartReady = (controls) => { - chartControlsRef.current = controls - } - - const enhancedSession = { - ...session, - onChartReady: handleChartReady, - isFullscreen: isFullscreen, - } - - const handleControlClick = async (action) => { - if (chartControlsRef.current) { - switch (action) { - case 'pause': - chartControlsRef.current.pauseStreaming() - break - case 'start': - case 'resume': - chartControlsRef.current.resumeStreaming() - break - case 'clear': - chartControlsRef.current.clearChart() - break - case 'stop': - chartControlsRef.current.pauseStreaming() - break - } - } - // No esperar a que el backend responda para aplicar efecto local - onControl(session.session_id, action) - } - - return ( - - - onRefresh(session.session_id)} - onFullscreen={openFullscreen} - /> - - - - - - - - - - - - - - {/* Fullscreen Modal */} - - - - - - 📈 {session.name || session.session_id} - Fullscreen Mode - - - Zoom: Drag to select area | Pan: Shift + Drag | Double-click to reset - - - - - - - - - - - - - {chartControlsRef.current && ( - - )} - - - - - - - ) -} - -function FlexHeader({ session, muted, onRefresh, onFullscreen }) { - return ( - - - 📈 {session.name || session.session_id} - - Variables: {session.variables_count || 0} | Status: {session.is_active ? (session.is_paused ? 'Paused' : 'Active') : 'Stopped'} - - - - - - } - size="sm" - variant="outline" - aria-label="Refresh status" - onClick={onRefresh} - /> - - - ) -} - - diff --git a/frontend/src/components/PlotTableManager.jsx b/frontend/src/components/PlotTableManager.jsx deleted file mode 100644 index 73a9a53..0000000 --- a/frontend/src/components/PlotTableManager.jsx +++ /dev/null @@ -1,414 +0,0 @@ -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/components/TabCoordinationDemo.jsx b/frontend/src/components/TabCoordinationDemo.jsx deleted file mode 100644 index 0e5b3b3..0000000 --- a/frontend/src/components/TabCoordinationDemo.jsx +++ /dev/null @@ -1,62 +0,0 @@ -import React from 'react' -import { Box, Text, Badge, VStack, HStack, useColorModeValue } from '@chakra-ui/react' -import { getTabCoordinator } from '../utils/TabCoordinator' - -/** - * TabCoordinationDemo - Componente de demostración para mostrar el estado de coordinación - */ -export default function TabCoordinationDemo() { - const [coordinator, setCoordinator] = React.useState(null) - const [isLeader, setIsLeader] = React.useState(false) - const [tabInfo, setTabInfo] = React.useState({}) - const bgColor = useColorModeValue('gray.50', 'gray.800') - - React.useEffect(() => { - const coord = getTabCoordinator() - setCoordinator(coord) - setIsLeader(coord.getIsLeader()) - setTabInfo({ - tabId: coord.tabId, - isLeader: coord.getIsLeader() - }) - - // Subscribirse a cambios de liderazgo - const unsubscribe = coord.subscribe('demo', ({ type, data }) => { - if (type === 'leadership_change') { - setIsLeader(data.isLeader) - setTabInfo(prev => ({ - ...prev, - isLeader: data.isLeader - })) - } - }) - - return unsubscribe - }, []) - - if (!coordinator) { - return Loading coordinator... - } - - return ( - - - - 🔗 Tab Coordination Status - - {isLeader ? '👑 Leader' : '👥 Follower'} - - - - Tab ID: {tabInfo.tabId} - - - Role: {isLeader ? 'Making real connections to backend' : 'Receiving data from leader tab'} - - - Only the leader tab creates actual HTTP connections. Other tabs receive data via BroadcastChannel. - - - - ) -} diff --git a/frontend/src/pages/Dashboard.jsx b/frontend/src/pages/Dashboard.jsx index 4f17a0a..3d9fc9a 100644 --- a/frontend/src/pages/Dashboard.jsx +++ b/frontend/src/pages/Dashboard.jsx @@ -55,7 +55,7 @@ import { import { ChevronDownIcon, ChevronUpIcon } from '@chakra-ui/icons' import Form from '@rjsf/chakra-ui' import validator from '@rjsf/validator-ajv8' -import PlotManager from '../components/PlotManagerSimple' +import PlotManager from '../components/PlotManager' import allWidgets from '../components/widgets/AllWidgets' import LayoutObjectFieldTemplate from '../components/rjsf/LayoutObjectFieldTemplate' import CsvFileBrowser from '../components/CsvFileBrowser' diff --git a/main.py b/main.py index daf41da..e6a82bd 100644 --- a/main.py +++ b/main.py @@ -115,6 +115,16 @@ def serve_public_record_png(): return Response("record.png not found", status=404, mimetype="text/plain") +@app.route("/SIDEL.png") +def serve_public_sidel_png(): + """Serve /SIDEL.png from the React public folder.""" + public_dir = project_path("frontend", "public") + sidel_file = os.path.join(public_dir, "SIDEL.png") + if os.path.exists(sidel_file): + return send_from_directory(public_dir, "SIDEL.png") + return Response("SIDEL.png not found", status=404, mimetype="text/plain") + + # ============================== # Frontend (React SPA) # ============================== diff --git a/test_config_reload.py b/test_config_reload.py deleted file mode 100644 index 539ab38..0000000 --- a/test_config_reload.py +++ /dev/null @@ -1,129 +0,0 @@ -""" -Test script to validate automatic configuration reloading -""" - -import json -import requests -import time - -# Configuration -BASE_URL = "http://localhost:5000" -TEST_DATASET_ID = "TestReload" - - -def test_config_reload(): - """Test that backend automatically reloads configuration when datasets are updated""" - - print("🧪 Testing automatic configuration reload...") - - try: - # Step 1: Get current dataset definitions - print("📖 Reading current dataset definitions...") - response = requests.get(f"{BASE_URL}/api/config/dataset-definitions") - if not response.ok: - print(f"❌ Failed to read dataset definitions: {response.status_code}") - return False - - current_config = response.json() - datasets = current_config.get("data", {}).get("datasets", []) - print(f"Current datasets: {[d.get('id') for d in datasets]}") - - # Step 2: Add a test dataset - print(f"➕ Adding test dataset: {TEST_DATASET_ID}") - test_dataset = { - "id": TEST_DATASET_ID, - "name": "Test Reload Dataset", - "prefix": "test_reload", - "sampling_interval": 1.0, - "enabled": False, - } - - # Add to datasets list - new_datasets = [ - d for d in datasets if d.get("id") != TEST_DATASET_ID - ] # Remove if exists - new_datasets.append(test_dataset) - - new_config = { - "datasets": new_datasets, - "version": "1.0", - "last_update": f"{time.time()}", - } - - # Save configuration - response = requests.put( - f"{BASE_URL}/api/config/dataset-definitions", - headers={"Content-Type": "application/json"}, - json=new_config, - ) - - if not response.ok: - print(f"❌ Failed to save dataset definitions: {response.status_code}") - return False - - print("✅ Dataset definitions saved") - - # Step 3: Check if backend has reloaded the configuration - print("🔍 Checking if backend reloaded configuration...") - time.sleep(1) # Give backend a moment to reload - - # Get status from backend - response = requests.get(f"{BASE_URL}/api/status") - if not response.ok: - print(f"❌ Failed to get status: {response.status_code}") - return False - - status = response.json() - backend_datasets = status.get("datasets", {}) - - if TEST_DATASET_ID in backend_datasets: - print(f"✅ Backend successfully loaded new dataset: {TEST_DATASET_ID}") - print(f"Dataset details: {backend_datasets[TEST_DATASET_ID]}") - - # Step 4: Clean up - remove test dataset - print("🧹 Cleaning up test dataset...") - cleanup_datasets = [ - d for d in new_datasets if d.get("id") != TEST_DATASET_ID - ] - cleanup_config = { - "datasets": cleanup_datasets, - "version": "1.0", - "last_update": f"{time.time()}", - } - - response = requests.put( - f"{BASE_URL}/api/config/dataset-definitions", - headers={"Content-Type": "application/json"}, - json=cleanup_config, - ) - - if response.ok: - print("✅ Test dataset cleaned up") - else: - print( - f"⚠️ Warning: Failed to clean up test dataset: {response.status_code}" - ) - - return True - else: - print( - f"❌ Backend did not reload configuration. Available datasets: {list(backend_datasets.keys())}" - ) - return False - - except requests.exceptions.ConnectionError: - print( - "❌ Could not connect to backend. Make sure the Flask server is running on http://localhost:5000" - ) - return False - except Exception as e: - print(f"❌ Test failed with error: {e}") - return False - - -if __name__ == "__main__": - success = test_config_reload() - if success: - print("\n🎉 Configuration reload test PASSED!") - else: - print("\n💥 Configuration reload test FAILED!") diff --git a/test_endpoint.py b/test_endpoint.py deleted file mode 100644 index 7500193..0000000 --- a/test_endpoint.py +++ /dev/null @@ -1,23 +0,0 @@ -import requests -import json - -# Test the symbols load endpoint -url = "http://localhost:5050/api/symbols/load" -data = { - "asc_file_path": "D:/Proyectos/Scripts/Siemens/S7_snap7_Stremer_n_Log/config/data/test_symbols.asc" -} - -try: - response = requests.post(url, json=data) - print(f"Status Code: {response.status_code}") - print(f"Response Headers: {response.headers}") - print(f"Response Text: {response.text}") - - if response.headers.get("content-type", "").startswith("application/json"): - result = response.json() - print(f"JSON Response: {json.dumps(result, indent=2)}") - else: - print("Response is not JSON, probably HTML error page") - -except Exception as e: - print(f"Error: {e}") diff --git a/test_header_validation.py b/test_header_validation.py deleted file mode 100644 index 9d9b73e..0000000 --- a/test_header_validation.py +++ /dev/null @@ -1,105 +0,0 @@ -""" -Test script for CSV header validation functionality -""" - -import csv -import os -import tempfile -import shutil -from datetime import datetime - - -def test_header_validation(): - """Test the header validation logic without full system dependencies""" - - # Create a temporary test directory - test_dir = tempfile.mkdtemp() - - try: - # Test 1: Create a CSV file with old headers - old_csv_path = os.path.join(test_dir, "test_data_14.csv") - old_headers = ["timestamp", "var1", "var2"] - - with open(old_csv_path, "w", newline="", encoding="utf-8") as f: - writer = csv.writer(f) - writer.writerow(old_headers) - writer.writerow(["2025-08-14 12:00:00", "100", "200"]) - writer.writerow(["2025-08-14 12:00:01", "101", "201"]) - - print(f"✅ Created test CSV file: {old_csv_path}") - - # Test 2: Read headers function - def read_csv_headers(file_path): - try: - with open(file_path, "r", newline="", encoding="utf-8") as file: - reader = csv.reader(file) - headers = next(reader, []) - return headers - except (IOError, StopIteration) as e: - print(f"Could not read headers from {file_path}: {e}") - return [] - - # Test 3: Compare headers function - def compare_headers(existing_headers, new_headers): - return existing_headers == new_headers - - # Test 4: Rename file function - def rename_csv_file_with_timestamp(original_path, prefix): - directory = os.path.dirname(original_path) - timestamp = datetime.now().strftime("%H_%M_%S") - new_filename = f"{prefix}_to_{timestamp}.csv" - new_path = os.path.join(directory, new_filename) - - # Ensure the new filename is unique - counter = 1 - while os.path.exists(new_path): - new_filename = f"{prefix}_to_{timestamp}_{counter}.csv" - new_path = os.path.join(directory, new_filename) - counter += 1 - - shutil.move(original_path, new_path) - return new_path - - # Test the functions - existing_headers = read_csv_headers(old_csv_path) - new_headers = ["timestamp", "var1", "var2", "var3"] # Different headers - - print(f"Existing headers: {existing_headers}") - print(f"New headers: {new_headers}") - print(f"Headers match: {compare_headers(existing_headers, new_headers)}") - - # Test header mismatch scenario - if not compare_headers(existing_headers, new_headers): - print("❌ Header mismatch detected! Renaming file...") - renamed_path = rename_csv_file_with_timestamp(old_csv_path, "test_data") - print(f"✅ File renamed to: {os.path.basename(renamed_path)}") - - # Create new file with correct headers - new_csv_path = old_csv_path # Same original path - with open(new_csv_path, "w", newline="", encoding="utf-8") as f: - writer = csv.writer(f) - writer.writerow(new_headers) - writer.writerow(["2025-08-14 12:00:02", "102", "202", "302"]) - - print( - f"✅ Created new CSV file with correct headers: {os.path.basename(new_csv_path)}" - ) - - # Verify the files - print(f"\nFiles in test directory:") - for file in os.listdir(test_dir): - if file.endswith(".csv"): - file_path = os.path.join(test_dir, file) - headers = read_csv_headers(file_path) - print(f" {file}: {headers}") - - print("\n✅ All tests passed!") - - finally: - # Clean up - shutil.rmtree(test_dir) - print(f"🧹 Cleaned up test directory: {test_dir}") - - -if __name__ == "__main__": - test_header_validation() diff --git a/test_request.json b/test_request.json deleted file mode 100644 index 00fe4876d23285bbff5aa082537e9f0df803665d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 126 zcmezWubM%Lp^PDsp@<<9NG37lFr+dR1I4Tu6d0m`bSOg*gAs!zLp*~MSWN{`Oa~~3 uERze=sl*Tq6)yprlnXQ^o}nDb%40}j$Op0@CK@xCF&F@0EdwtD7XtvrGZc;h diff --git a/test_symbol_loader.py b/test_symbol_loader.py deleted file mode 100644 index 1be2e9d..0000000 --- a/test_symbol_loader.py +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env python3 -""" -Test script for symbol loader functionality. -""" -import sys -import os - -# Add the project root to Python path -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) - -from utils.symbol_loader import SymbolLoader - - -def test_symbol_loader(): - """Test the symbol loader with the sample ASC file.""" - - # Create a simple logger for testing - class SimpleLogger: - def info(self, msg): - print(f"INFO: {msg}") - - def warning(self, msg): - print(f"WARNING: {msg}") - - def error(self, msg): - print(f"ERROR: {msg}") - - logger = SimpleLogger() - loader = SymbolLoader(logger) - - # Test file paths - test_asc_file = "config/data/test_symbols.asc" - output_json_file = "config/data/test_output.json" - - try: - print(f"Testing symbol loading from: {test_asc_file}") - - if not os.path.exists(test_asc_file): - print(f"ERROR: Test file not found: {test_asc_file}") - return False - - # Load symbols - symbols_count = loader.load_asc_and_save_json(test_asc_file, output_json_file) - - print(f"SUCCESS: Loaded {symbols_count} symbols") - print(f"Output saved to: {output_json_file}") - - # Verify output file - if os.path.exists(output_json_file): - with open(output_json_file, "r", encoding="utf-8") as f: - import json - - data = json.load(f) - print(f"JSON contains {len(data.get('symbols', []))} symbols") - - return True - - except Exception as e: - print(f"ERROR: {type(e).__name__}: {e}") - import traceback - - traceback.print_exc() - return False - - -if __name__ == "__main__": - test_symbol_loader()