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.
This commit is contained in:
Miguel 2025-08-15 22:55:03 +02:00
parent a4f74b70ed
commit 3417056b06
25 changed files with 741 additions and 5348 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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 <Text>Loading dataset configuration...</Text>
}
return (
<VStack align="stretch" spacing={4}>
{message && (
<Alert status={message.includes('Error') ? 'error' : 'success'}>
<AlertIcon />
{message}
</Alert>
)}
{/* Tabla de Datasets */}
<Card>
<CardHeader>
<Heading size="sm">📊 Dataset Management</Heading>
<Text fontSize="sm" color={muted}>
Manage your datasets: create, edit and configure
</Text>
</CardHeader>
<CardBody>
{datasetSchema?.properties?.datasets ? (
<FormTable
schema={datasetSchema.properties.datasets}
uiSchema={datasetUiSchema.datasets}
data={fullData.datasets || {}}
onChange={saveDatasets}
title="Datasets"
keyField="id"
/>
) : (
<Alert status="warning">
<AlertIcon />
Dataset schema for individual datasets not available
</Alert>
)}
</CardBody>
</Card>
<Divider />
{/* Variables del Dataset */}
<Card>
<CardHeader>
<HStack justify="space-between">
<Heading size="sm">🔧 Dataset Variables</Heading>
<HStack>
<Text fontSize="sm" color={muted}>Dataset:</Text>
<Select
size="sm"
value={selectedDatasetId}
onChange={(e) => setSelectedDatasetId(e.target.value)}
placeholder="Select dataset"
width="200px"
>
{datasetOptions.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</Select>
</HStack>
</HStack>
</CardHeader>
<CardBody>
{!selectedDatasetId ? (
<Alert status="info">
<AlertIcon />
Select a dataset to manage its variables
</Alert>
) : variableSchema ? (
<FormTable
schema={variableSchema}
uiSchema={variableUiSchema}
data={currentDatasetVariables}
onChange={saveDatasetVariables}
title={`Variables for: ${fullData.datasets?.[selectedDatasetId]?.name || selectedDatasetId}`}
keyField="name"
liveValues={liveValues}
/>
) : (
<Alert status="warning">
<AlertIcon />
Variable schema not available
</Alert>
)}
</CardBody>
</Card>
</VStack>
)
}

View File

@ -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 <Text>Loading datasets...</Text>
}
return (
<VStack align="stretch" spacing={4}>
{message && (
<Alert status={message.includes('Error') ? 'error' : 'success'}>
<AlertIcon />
{message}
</Alert>
)}
{/* Datasets */}
<Card>
<CardHeader>
<Heading size="sm">📊 Dataset Definitions</Heading>
</CardHeader>
<CardBody>
{datasetSchema ? (
<FormTable
schema={datasetSchema}
uiSchema={datasetUiSchema}
data={datasets}
onChange={saveDatasets}
title="Datasets"
keyField="id"
/>
) : (
<Alert status="warning">
<AlertIcon />
Dataset schema not available
</Alert>
)}
</CardBody>
</Card>
<Divider />
{/* Variables del Dataset */}
<Card>
<CardHeader>
<HStack justify="space-between">
<Heading size="sm">🔧 Dataset Variables</Heading>
<HStack>
<Text fontSize="sm" color={muted}>Dataset:</Text>
<Select
size="sm"
value={selectedDatasetId}
onChange={(e) => setSelectedDatasetId(e.target.value)}
placeholder="Select dataset"
width="200px"
>
{datasetOptions.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</Select>
</HStack>
</HStack>
</CardHeader>
<CardBody>
{!selectedDatasetId ? (
<Alert status="info">
<AlertIcon />
Select a dataset to manage its variables
</Alert>
) : variableSchema ? (
<FormTable
schema={variableSchema}
uiSchema={variableUiSchema}
data={currentDatasetVariables}
onChange={saveDatasetVariables}
title={`Variables for: ${datasets[selectedDatasetId]?.name || selectedDatasetId}`}
keyField="name"
/>
) : (
<Alert status="warning">
<AlertIcon />
Variable schema not available
</Alert>
)}
</CardBody>
</Card>
</VStack>
)
}

View File

@ -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 <Text>Loading datasets...</Text>
}
return (
<VStack align="stretch" spacing={4}>
{message && (
<Alert status={message.includes('Error') ? 'error' : 'success'}>
<AlertIcon />
{message}
</Alert>
)}
{/* Tabla de Datasets */}
<Card>
<CardHeader>
<Heading size="sm">📊 Datasets</Heading>
</CardHeader>
<CardBody>
{datasetSchema ? (
<EditableTable
schema={datasetSchema}
uiSchema={datasetUiSchema.additionalProperties || {}}
data={datasetsForTable}
onChange={(newData) => {
// 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"
/>
) : (
<Alert status="warning">
<AlertIcon />
No dataset schema available
</Alert>
)}
</CardBody>
</Card>
<Divider />
{/* Selector de Dataset y Tabla de Variables */}
<Card>
<CardHeader>
<HStack justify="space-between">
<Heading size="sm">🔧 Dataset Variables</Heading>
<HStack>
<Text fontSize="sm" color={muted}>Dataset:</Text>
<Select
size="sm"
value={selectedDatasetId}
onChange={(e) => setSelectedDatasetId(e.target.value)}
placeholder="Select dataset"
width="200px"
>
{datasetOptions.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</Select>
</HStack>
</HStack>
</CardHeader>
<CardBody>
{!selectedDatasetId ? (
<Alert status="info">
<AlertIcon />
Select a dataset to manage its variables
</Alert>
) : variableSchema ? (
<EditableTable
schema={variableSchema}
uiSchema={variableUiSchema.additionalProperties || {}}
data={variablesForTable}
onChange={(newData) => {
// 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"
/>
) : (
<Alert status="warning">
<AlertIcon />
No variable schema available
</Alert>
)}
</CardBody>
</Card>
</VStack>
)
}

View File

@ -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 (
<Alert status="info">
<AlertIcon />
Select a dataset from the overview above to manage its variables.
</Alert>
)
}
return (
<VStack spacing={4} align="stretch">
<Card bg={cardBg} borderColor={borderColor}>
<CardHeader>
<Flex align="center">
<Box>
<Heading size="md">Variables for "{selectedDataset.name}"</Heading>
<Text fontSize="sm" color="gray.500" mt={1}>
ID: {selectedDatasetId} {Object.keys(datasetVariables).length} variables {streamingVariables.length} streaming
</Text>
</Box>
<Spacer />
<Button colorScheme="blue" onClick={handleAddVariable}>
Add Variable
</Button>
</Flex>
</CardHeader>
<CardBody>
{Object.keys(datasetVariables).length === 0 ? (
<Text color="gray.500" textAlign="center" py={8}>
No variables configured for this dataset.
Click "Add Variable" to get started.
</Text>
) : (
<Grid templateColumns="repeat(auto-fill, minmax(350px, 1fr))" gap={4}>
{Object.entries(datasetVariables).map(([varName, variable]) => (
<VariableCard
key={varName}
name={varName}
variable={variable}
isStreaming={streamingVariables.includes(varName)}
onEdit={() => handleEditVariable(varName)}
onDelete={() => handleDeleteVariable(varName)}
onToggleStreaming={() => toggleVariableStreaming(varName)}
/>
))}
</Grid>
)}
</CardBody>
</Card>
<VariableEditModal
isOpen={isOpen}
onClose={() => {
onClose()
setEditingVariable(null)
}}
variable={editingVariable}
onSave={handleSaveVariable}
/>
</VStack>
)
}
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 (
<Card bg={cardBg} borderColor={borderColor} size="sm">
<CardBody>
<VStack align="start" spacing={3}>
<HStack justify="space-between" w="full">
<Heading size="sm" color="blue.600">{name}</Heading>
<HStack spacing={1}>
<IconButton
icon="✏️"
size="xs"
variant="ghost"
onClick={onEdit}
aria-label="Edit variable"
/>
<IconButton
icon="🗑️"
size="xs"
variant="ghost"
colorScheme="red"
onClick={onDelete}
aria-label="Delete variable"
/>
</HStack>
</HStack>
<Grid templateColumns="1fr 1fr" gap={2} w="full" fontSize="sm">
<Text><strong>Area:</strong> {getAreaLabel(variable.area)}</Text>
<Text><strong>Type:</strong> {getTypeLabel(variable.type)}</Text>
{variable.area === 'db' && (
<Text><strong>DB:</strong> {variable.db}</Text>
)}
<Text><strong>Offset:</strong> {variable.offset}</Text>
{variable.bit !== undefined && (
<Text><strong>Bit:</strong> {variable.bit}</Text>
)}
</Grid>
<HStack justify="space-between" w="full">
<Badge colorScheme={isStreaming ? 'green' : 'gray'}>
{isStreaming ? '📡 Streaming' : '📴 Not streaming'}
</Badge>
<Button
size="xs"
variant="outline"
colorScheme={isStreaming ? 'red' : 'green'}
onClick={onToggleStreaming}
>
{isStreaming ? 'Disable' : 'Enable'} Streaming
</Button>
</HStack>
</VStack>
</CardBody>
</Card>
)
}
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 (
<Modal isOpen={isOpen} onClose={onClose} size="lg">
<ModalOverlay />
<ModalContent>
<ModalHeader>
{isEditing ? `Edit Variable: ${variable.name}` : 'Add New Variable'}
</ModalHeader>
<ModalCloseButton />
<form onSubmit={handleSubmit}>
<ModalBody>
<VStack spacing={4}>
<FormControl isRequired>
<FormLabel>Variable Name</FormLabel>
<Input
value={formData.name || ''}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="e.g., Temperature_Tank_1"
/>
</FormControl>
<Grid templateColumns="1fr 1fr" gap={4} w="full">
<FormControl isRequired>
<FormLabel>Memory Area</FormLabel>
<Select
value={formData.area || 'db'}
onChange={(e) => setFormData({ ...formData, area: e.target.value })}
>
{PLC_AREAS.map(area => (
<option key={area.value} value={area.value}>
{area.label}
</option>
))}
</Select>
</FormControl>
<FormControl isRequired>
<FormLabel>Data Type</FormLabel>
<Select
value={formData.type || 'real'}
onChange={(e) => setFormData({ ...formData, type: e.target.value })}
>
{DATA_TYPES.map(type => (
<option key={type.value} value={type.value}>
{type.label}
</option>
))}
</Select>
</FormControl>
</Grid>
<Grid templateColumns="1fr 1fr 1fr" gap={4} w="full">
{formData.area === 'db' && (
<FormControl>
<FormLabel>DB Number</FormLabel>
<NumberInput
value={formData.db || 1}
onChange={(_, num) => setFormData({ ...formData, db: num })}
min={1} max={9999}
>
<NumberInputField />
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
</FormControl>
)}
<FormControl isRequired>
<FormLabel>Offset</FormLabel>
<NumberInput
value={formData.offset || 0}
onChange={(_, num) => setFormData({ ...formData, offset: num })}
min={0} max={8191}
>
<NumberInputField />
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
</FormControl>
{['e', 'a', 'mb'].includes(formData.area) && (
<FormControl>
<FormLabel>Bit Position</FormLabel>
<NumberInput
value={formData.bit || 0}
onChange={(_, num) => setFormData({ ...formData, bit: num })}
min={0} max={7}
>
<NumberInputField />
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
</FormControl>
)}
</Grid>
<FormControl>
<Checkbox
isChecked={formData.streaming || false}
onChange={(e) => setFormData({ ...formData, streaming: e.target.checked })}
>
Enable streaming to PlotJuggler
</Checkbox>
</FormControl>
</VStack>
</ModalBody>
<ModalFooter>
<Button variant="outline" mr={3} onClick={onClose}>
Cancel
</Button>
<Button type="submit" colorScheme="blue">
{isEditing ? 'Update' : 'Add'} Variable
</Button>
</ModalFooter>
</form>
</ModalContent>
</Modal>
)
}

View File

@ -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 (
<Card>
<CardBody>
<VStack spacing={4}>
<Spinner />
<Text>Loading dataset variables configuration...</Text>
</VStack>
</CardBody>
</Card>
)
}
if (!schema) {
return (
<Card>
<CardBody>
<Alert status="error">
<AlertIcon />
Failed to load schema for dataset variables
</Alert>
</CardBody>
</Card>
)
}
return (
<Card>
<CardHeader>
<VStack align="start" spacing={2}>
<Heading size="sm">🔧 Dataset Variables</Heading>
<Text fontSize="sm" color={muted}>
Configure PLC variables for each dataset - use symbols or manual configuration
</Text>
{datasetOptions.length > 0 && (
<HStack>
<Text fontSize="sm" color={muted}>Reference Dataset:</Text>
<Select
size="sm"
value={selectedDatasetId || ''}
onChange={(e) => onSelectDataset && onSelectDataset(e.target.value)}
placeholder="Select dataset for reference"
width="250px"
>
{datasetOptions.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</Select>
</HStack>
)}
</VStack>
</CardHeader>
<CardBody>
{message && (
<Alert status={message.includes('Error') ? 'error' : 'success'} mb={4}>
<AlertIcon />
{message}
</Alert>
)}
<Form
schema={schema}
uiSchema={uiSchema}
formData={variables}
validator={validator}
onChange={({ formData }) => setVariables(formData)}
onSubmit={({ formData }) => saveVariables(formData)}
templates={{ ObjectFieldTemplate: LayoutObjectFieldTemplate }}
widgets={allWidgets}
showErrorList={false}
disabled={saving}
>
<HStack mt={4}>
<button type="submit" disabled={saving}>
{saving ? 'Saving...' : 'Save Variables'}
</button>
</HStack>
</Form>
</CardBody>
</Card>
)
}

View File

@ -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 (
<Select {...commonProps} placeholder="Select...">
{property.enum.map(option => (
<option key={option} value={option}>{option}</option>
))}
</Select>
)
}
if (property?.type === 'boolean' || widget === 'checkbox') {
return (
<Checkbox
isChecked={!!value}
onChange={(e) => setValue({ ...itemData, [propertyName]: e.target.checked })}
/>
)
}
if (property?.type === 'number' || property?.type === 'integer' || widget === 'updown') {
return (
<NumberInput
size="sm"
value={value || ''}
onChange={(valueString) => setValue({ ...itemData, [propertyName]: parseFloat(valueString) || null })}
min={property?.minimum}
max={property?.maximum}
step={property?.type === 'integer' ? 1 : 0.01}
>
<NumberInputField />
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
)
}
return <Input {...commonProps} placeholder={uiConfig['ui:placeholder'] || ''} />
}
const renderValue = (propertyName, value) => {
const property = properties[propertyName]
if (property?.type === 'boolean') {
return <Checkbox isChecked={!!value} isReadOnly />
}
if (value === null || value === undefined) {
return <Text color={muted}>-</Text>
}
return <Text>{String(value)}</Text>
}
const getColumnTitle = (propertyName) => {
const property = properties[propertyName]
return property?.title || propertyName
}
if (propertyNames.length === 0) {
return (
<Alert status="warning">
<AlertIcon />
No schema properties defined for this table
</Alert>
)
}
return (
<VStack align="stretch" spacing={3}>
<HStack justify="space-between">
<Text fontWeight="semibold">{title}</Text>
{allowAdd && (
<Button size="sm" leftIcon={<AddIcon />} onClick={onAddOpen}>
Add Item
</Button>
)}
</HStack>
<Box overflowX="auto" borderWidth="1px" borderRadius="md" borderColor={borderColor}>
<Table size="sm">
<Thead>
<Tr>
{propertyNames.map(prop => (
<Th key={prop}>{getColumnTitle(prop)}</Th>
))}
{(allowEdit || allowDelete) && <Th width="100px">Actions</Th>}
</Tr>
</Thead>
<Tbody>
{dataKeys.length === 0 ? (
<Tr>
<Td colSpan={propertyNames.length + 1}>
<Text color={muted} textAlign="center">No items</Text>
</Td>
</Tr>
) : (
dataKeys.map(key => (
<Tr key={key}>
{propertyNames.map(prop => (
<Td key={prop}>
{renderValue(prop, dataAsObject[key][prop])}
</Td>
))}
{(allowEdit || allowDelete) && (
<Td>
<HStack spacing={1}>
{allowEdit && (
<IconButton
icon={<EditIcon />}
size="xs"
variant="outline"
onClick={() => openEdit(key)}
/>
)}
{allowDelete && (
<IconButton
icon={<DeleteIcon />}
size="xs"
variant="outline"
colorScheme="red"
onClick={() => handleDelete(key)}
/>
)}
</HStack>
</Td>
)}
</Tr>
))
)}
</Tbody>
</Table>
</Box>
{/* Add Modal */}
<Modal isOpen={isAddOpen} onClose={onAddClose}>
<ModalOverlay />
<ModalContent>
<ModalHeader>Add New Item</ModalHeader>
<ModalCloseButton />
<ModalBody>
<VStack spacing={3}>
{propertyNames.map(prop => (
<Box key={prop} width="100%">
<Text fontSize="sm" fontWeight="medium" mb={1}>
{getColumnTitle(prop)}
{schema.required?.includes(prop) && <Text as="span" color="red.500">*</Text>}
</Text>
{renderInput(prop, newItem[prop], setNewItem, newItem)}
{uiSchema[prop]?.['ui:help'] && (
<Text fontSize="xs" color={muted} mt={1}>
{uiSchema[prop]['ui:help']}
</Text>
)}
</Box>
))}
</VStack>
</ModalBody>
<ModalFooter>
<Button variant="ghost" mr={3} onClick={onAddClose}>Cancel</Button>
<Button colorScheme="blue" onClick={handleAdd}>Add</Button>
</ModalFooter>
</ModalContent>
</Modal>
{/* Edit Modal */}
<Modal isOpen={isEditOpen} onClose={onEditClose}>
<ModalOverlay />
<ModalContent>
<ModalHeader>Edit Item: {editingKey}</ModalHeader>
<ModalCloseButton />
<ModalBody>
<VStack spacing={3}>
{propertyNames.map(prop => (
<Box key={prop} width="100%">
<Text fontSize="sm" fontWeight="medium" mb={1}>
{getColumnTitle(prop)}
{schema.required?.includes(prop) && <Text as="span" color="red.500">*</Text>}
</Text>
{renderInput(prop, editingData[prop], setEditingData, editingData)}
{uiSchema[prop]?.['ui:help'] && (
<Text fontSize="xs" color={muted} mt={1}>
{uiSchema[prop]['ui:help']}
</Text>
)}
</Box>
))}
</VStack>
</ModalBody>
<ModalFooter>
<Button variant="ghost" mr={3} onClick={onEditClose}>Cancel</Button>
<Button colorScheme="blue" onClick={handleEdit}>Save</Button>
</ModalFooter>
</ModalContent>
</Modal>
</VStack>
)
}

View File

@ -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 (
<Alert status="warning">
<AlertIcon />
Schema not available for {title}
</Alert>
)
}
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 (
<VStack align="stretch" spacing={3}>
<HStack justify="space-between">
<Text fontWeight="semibold">{title}</Text>
{allowAdd && (
<Button size="sm" leftIcon={<AddIcon />} onClick={startAdd}>
Add Item
</Button>
)}
</HStack>
{dataKeys.length === 0 && !addingNew && (
<Box p={4} borderWidth="1px" borderRadius="md" borderColor={borderColor}>
<Text color={muted} textAlign="center">No items found</Text>
</Box>
)}
{/* Formulario para agregar nuevo item */}
{addingNew && (
<Card borderColor="blue.200" borderWidth="2px">
<CardHeader pb={2}>
<HStack justify="space-between">
<Heading size="xs" color="blue.600">
Adding: {newKey}
</Heading>
<Button size="xs" variant="ghost" onClick={() => setAddingNew(false)}>
Cancel
</Button>
</HStack>
</CardHeader>
<CardBody pt={0}>
<Form
schema={itemSchema}
uiSchema={itemUiSchema}
formData={{}}
validator={validator}
onSubmit={({ formData }) => handleAdd(formData)}
templates={{ ObjectFieldTemplate: LayoutObjectFieldTemplate }}
widgets={allWidgets}
showErrorList={false}
>
<HStack mt={3}>
<Button type="submit" size="sm" colorScheme="blue">
Save
</Button>
<Button size="sm" variant="ghost" onClick={() => setAddingNew(false)}>
Cancel
</Button>
</HStack>
</Form>
</CardBody>
</Card>
)}
{/* Formularios para items existentes */}
{dataKeys.map(key => (
<Card key={key} borderWidth="1px" borderColor={borderColor}>
<CardHeader pb={2}>
<HStack justify="space-between">
<HStack>
<Heading size="xs">{key}</Heading>
{data[key] && typeof data[key].enabled === 'boolean' && (
<Badge colorScheme={data[key].enabled ? 'green' : 'red'} size="sm">
{data[key].enabled ? 'Enabled' : 'Disabled'}
</Badge>
)}
{editingKey === key && (
<Badge colorScheme="orange" size="sm">Editing</Badge>
)}
</HStack>
<HStack spacing={1}>
{editingKey === key ? (
<Button
size="xs"
variant="ghost"
onClick={() => setEditingKey(null)}
>
Cancel
</Button>
) : (
<>
<IconButton
icon={<EditIcon />}
size="xs"
variant="outline"
onClick={() => { setEditingFormData({ ...(data[key] || {}) }); setEditingKey(key) }}
/>
{allowDelete && (
<IconButton
icon={<DeleteIcon />}
size="xs"
variant="outline"
colorScheme="red"
onClick={() => handleDelete(key)}
/>
)}
</>
)}
</HStack>
</HStack>
</CardHeader>
<CardBody pt={0}>
{liveValues && liveValues[key] !== undefined && (
<Box mb={2}>
<Text fontSize="sm" color={muted}>
Live value: <Text as="span" fontWeight="semibold">{String(liveValues[key])}</Text>
</Text>
</Box>
)}
<Form
schema={itemSchema}
uiSchema={itemUiSchema}
formData={editingKey === key ? (editingFormData || {}) : (data[key] || {})}
validator={validator}
onChange={editingKey === key ? ({ formData }) => setEditingFormData(formData) : () => { }}
onSubmit={editingKey === key ? ({ formData }) => handleEdit(key, formData) : undefined}
templates={{ ObjectFieldTemplate: LayoutObjectFieldTemplate }}
widgets={allWidgets}
readonly={editingKey !== key}
showErrorList={false}
>
{editingKey === key && (
<HStack mt={3}>
<Button type="submit" size="sm" colorScheme="blue">
Save
</Button>
<Button size="sm" variant="ghost" onClick={() => { setEditingKey(null); setEditingFormData(null) }}>
Cancel
</Button>
</HStack>
)}
</Form>
</CardBody>
</Card>
))}
</VStack>
)
}

View File

@ -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 <Text>Loading plot configuration...</Text>
}
return (
<VStack align="stretch" spacing={4}>
{message && (
<Alert status={message.includes('Error') ? 'error' : 'success'}>
<AlertIcon />
{message}
</Alert>
)}
{/* Tabla de Plots */}
<Card>
<CardHeader>
<Heading size="sm">📈 Plot Management</Heading>
<Text fontSize="sm" color={muted}>
Manage your plots: create, edit and configure
</Text>
</CardHeader>
<CardBody>
{plotSchema?.properties?.plots ? (
<FormTable
schema={plotSchema.properties.plots}
uiSchema={plotUiSchema.plots}
data={fullData.plots || {}}
onChange={savePlots}
title="Plots"
keyField="session_id"
/>
) : (
<Alert status="warning">
<AlertIcon />
Plot schema for individual plots not available
</Alert>
)}
</CardBody>
</Card>
<Divider />
{/* Variables del Plot */}
<Card>
<CardHeader>
<HStack justify="space-between">
<Heading size="sm">🔧 Plot Variables</Heading>
<HStack>
<Text fontSize="sm" color={muted}>Plot:</Text>
<Select
size="sm"
value={selectedPlotId}
onChange={(e) => setSelectedPlotId(e.target.value)}
placeholder="Select plot"
width="200px"
>
{plotOptions.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</Select>
</HStack>
</HStack>
</CardHeader>
<CardBody>
{!selectedPlotId ? (
<Alert status="info">
<AlertIcon />
Select a plot to manage its variables
</Alert>
) : variableSchema ? (
<FormTable
schema={variableSchema}
uiSchema={variableUiSchema}
data={currentPlotVariables}
onChange={savePlotVariables}
title={`Variables for: ${fullData.plots?.[selectedPlotId]?.name || selectedPlotId}`}
keyField="variable_name"
/>
) : (
<Alert status="warning">
<AlertIcon />
Variable schema not available
</Alert>
)}
</CardBody>
</Card>
</VStack>
)
}

View File

@ -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 (
<VStack align="stretch" spacing={3}>
<HStack justify="space-between">
<Text fontWeight="semibold">{title}</Text>
<Button size="sm" leftIcon={<AddIcon />} onClick={onAddOpen}>
Add Variable
</Button>
</HStack>
<Box overflowX="auto" borderWidth="1px" borderRadius="md" borderColor={borderColor}>
<Table size="sm">
<Thead>
<Tr>
<Th>Variable Name</Th>
<Th width="100px">Actions</Th>
</Tr>
</Thead>
<Tbody>
{variables.length === 0 ? (
<Tr>
<Td colSpan={2}>
<Text color={muted} textAlign="center">No variables</Text>
</Td>
</Tr>
) : (
variables.map((variable, index) => (
<Tr key={index}>
<Td>{variable}</Td>
<Td>
<HStack spacing={1}>
<IconButton
icon={<EditIcon />}
size="xs"
variant="outline"
onClick={() => openEdit(index)}
/>
<IconButton
icon={<DeleteIcon />}
size="xs"
variant="outline"
colorScheme="red"
onClick={() => handleDelete(index)}
/>
</HStack>
</Td>
</Tr>
))
)}
</Tbody>
</Table>
</Box>
{/* Add Modal */}
<Modal isOpen={isAddOpen} onClose={onAddClose}>
<ModalOverlay />
<ModalContent>
<ModalHeader>Add Variable</ModalHeader>
<ModalCloseButton />
<ModalBody>
<VStack spacing={3}>
<Box width="100%">
<Text fontSize="sm" fontWeight="medium" mb={1}>
Variable Name *
</Text>
<Input
size="sm"
value={newVariable}
onChange={(e) => setNewVariable(e.target.value)}
placeholder="e.g., UR29_Brix"
/>
<Text fontSize="xs" color={muted} mt={1}>
Enter the name of the variable to plot
</Text>
</Box>
</VStack>
</ModalBody>
<ModalFooter>
<Button variant="ghost" mr={3} onClick={onAddClose}>Cancel</Button>
<Button colorScheme="blue" onClick={handleAdd}>Add</Button>
</ModalFooter>
</ModalContent>
</Modal>
{/* Edit Modal */}
<Modal isOpen={isEditOpen} onClose={onEditClose}>
<ModalOverlay />
<ModalContent>
<ModalHeader>Edit Variable</ModalHeader>
<ModalCloseButton />
<ModalBody>
<VStack spacing={3}>
<Box width="100%">
<Text fontSize="sm" fontWeight="medium" mb={1}>
Variable Name *
</Text>
<Input
size="sm"
value={editingValue}
onChange={(e) => setEditingValue(e.target.value)}
placeholder="e.g., UR29_Brix"
/>
<Text fontSize="xs" color={muted} mt={1}>
Enter the name of the variable to plot
</Text>
</Box>
</VStack>
</ModalBody>
<ModalFooter>
<Button variant="ghost" mr={3} onClick={onEditClose}>Cancel</Button>
<Button colorScheme="blue" onClick={handleEdit}>Save</Button>
</ModalFooter>
</ModalContent>
</Modal>
</VStack>
)
}
/**
* 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 <Text>Loading plots...</Text>
}
return (
<VStack align="stretch" spacing={4}>
{message && (
<Alert status={message.includes('Error') ? 'error' : 'success'}>
<AlertIcon />
{message}
</Alert>
)}
{/* Plots */}
<Card>
<CardHeader>
<Heading size="sm">📈 Plot Definitions</Heading>
</CardHeader>
<CardBody>
{plotSchema ? (
<FormTable
schema={plotSchema}
uiSchema={plotUiSchema}
data={plots}
onChange={savePlots}
title="Plots"
keyField="session_id"
/>
) : (
<Alert status="warning">
<AlertIcon />
Plot schema not available
</Alert>
)}
</CardBody>
</Card>
<Divider />
{/* Variables del Plot */}
<Card>
<CardHeader>
<HStack justify="space-between">
<Heading size="sm">🔧 Plot Variables</Heading>
<HStack>
<Text fontSize="sm" color={muted}>Plot:</Text>
<Select
size="sm"
value={selectedPlotId}
onChange={(e) => setSelectedPlotId(e.target.value)}
placeholder="Select plot"
width="200px"
>
{plotOptions.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</Select>
</HStack>
</HStack>
</CardHeader>
<CardBody>
{!selectedPlotId ? (
<Alert status="info">
<AlertIcon />
Select a plot to manage its variables
</Alert>
) : (
<PlotVariablesManager
variables={currentPlotVariables}
onChange={savePlotVariables}
title={`Variables for: ${plots[selectedPlotId]?.name || selectedPlotId}`}
/>
)}
</CardBody>
</Card>
</VStack>
)
}

File diff suppressed because it is too large Load Diff

View File

@ -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 (
<Modal isOpen={isOpen} onClose={onClose} isCentered>
<ModalOverlay />
<ModalContent>
<ModalHeader>
<Flex align="center">
<Box mr={3} fontSize="2xl"></Box>
{title}
</Flex>
</ModalHeader>
<ModalCloseButton />
<ModalBody>
<VStack spacing={3} align="stretch">
<Text>
{message}
</Text>
{itemName && (
<Box p={3} bg="gray.50" borderRadius="md" borderLeft="4px solid" borderColor="red.400">
<Text fontWeight="bold" color="red.600">
Item to delete: "{itemName}"
</Text>
</Box>
)}
<Alert status="warning" borderRadius="md">
<AlertIcon />
<Text fontSize="sm">
This action cannot be undone. Make sure you want to proceed.
</Text>
</Alert>
</VStack>
</ModalBody>
<ModalFooter>
<Button variant="outline" mr={3} onClick={onClose}>
Cancel
</Button>
<Button colorScheme="red" onClick={onConfirm}>
🗑 {confirmButtonText}
</Button>
</ModalFooter>
</ModalContent>
</Modal>
)
}
// 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 (
<Card>
<CardBody>
<Flex align="center" justify="center" py={4}>
<Spinner size="sm" mr={2} />
<Text>Loading {title}...</Text>
</Flex>
</CardBody>
</Card>
)
}
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 (
<Card>
<CardHeader>
<Flex align="center" justify="space-between">
<Box>
<Heading size="md">{icon} {title}</Heading>
<Text fontSize="sm" color="gray.500" mt={1}>
{items.length} item{items.length !== 1 ? 's' : ''} configured
</Text>
</Box>
<HStack spacing={2}>
{/* Si se proporciona toggle de expansión externa, agregar botón de colapso */}
{onToggleExpansion && (
<Button
size="sm"
variant="outline"
onClick={onToggleExpansion}
rightIcon={isExpanded ? <ChevronUpIcon /> : <ChevronDownIcon />}
>
{isExpanded ? 'Collapse' : 'Expand'}
</Button>
)}
<Button size="sm" colorScheme="green" onClick={addItem}>
Add Item
</Button>
<Button size="sm" colorScheme="red" onClick={saveChanges}>
💾 Save All
</Button>
</HStack>
</Flex>
</CardHeader>
{/* Usar Collapse si se proporciona estado de expansión externo */}
{onToggleExpansion ? (
<Collapse in={isExpanded}>
<CardBody>
{items.length === 0 ? (
<Box textAlign="center" py={8}>
<Text color="gray.500" mb={4}>
No items configured yet
</Text>
<Button colorScheme="green" onClick={addItem}>
Add First Item
</Button>
</Box>
) : (
<VStack spacing={3} align="stretch">
{items.map((item, index) => {
const isItemExpanded = expandedItems.has(index)
const itemLabel = getItemLabel ? getItemLabel(item) : (item.name || item.id || `Item ${index + 1}`)
return (
<Card key={index} variant="outline" size="sm">
<CardHeader py={2}>
<Flex align="center" justify="space-between">
<HStack spacing={2}>
<Button
size="xs"
variant="ghost"
onClick={() => toggleItemExpansion(index)}
rightIcon={isItemExpanded ? <ChevronUpIcon /> : <ChevronDownIcon />}
>
{itemLabel}
</Button>
<Badge colorScheme="green" size="sm">#{index + 1}</Badge>
</HStack>
<Button
size="xs"
colorScheme="red"
variant="ghost"
onClick={() => handleDeleteClick(index)}
title="Delete this item"
>
🗑
</Button>
</Flex>
</CardHeader>
<Collapse in={isItemExpanded}>
<CardBody pt={0}>
<Form
schema={itemSchema}
uiSchema={itemUiSchema}
formData={item}
validator={validator}
widgets={allWidgets}
templates={{ ObjectFieldTemplate: LayoutObjectFieldTemplate }}
onChange={({ formData: newItemData }) => updateItem(index, newItemData)}
>
<div></div> {/* Prevents form buttons from showing */}
</Form>
</CardBody>
</Collapse>
</Card>
)
})}
</VStack>
)}
</CardBody>
</Collapse>
) : (
<CardBody>
{items.length === 0 ? (
<Box textAlign="center" py={8}>
<Text color="gray.500" mb={4}>
No items configured yet
</Text>
<Button colorScheme="green" onClick={addItem}>
Add First Item
</Button>
</Box>
) : (
<VStack spacing={3} align="stretch">
{items.map((item, index) => {
const isItemExpanded = expandedItems.has(index)
const itemLabel = getItemLabel ? getItemLabel(item) : (item.name || item.id || `Item ${index + 1}`)
return (
<Card key={index} variant="outline" size="sm">
<CardHeader py={2}>
<Flex align="center" justify="space-between">
<HStack spacing={2}>
<Button
size="xs"
variant="ghost"
onClick={() => toggleItemExpansion(index)}
rightIcon={isItemExpanded ? <ChevronUpIcon /> : <ChevronDownIcon />}
>
{itemLabel}
</Button>
<Badge colorScheme="green" size="sm">#{index + 1}</Badge>
</HStack>
<Button
size="xs"
colorScheme="red"
variant="ghost"
onClick={() => handleDeleteClick(index)}
title="Delete this item"
>
🗑
</Button>
</Flex>
</CardHeader>
<Collapse in={isItemExpanded}>
<CardBody pt={0}>
<Form
schema={itemSchema}
uiSchema={itemUiSchema}
formData={item}
validator={validator}
widgets={allWidgets}
templates={{ ObjectFieldTemplate: LayoutObjectFieldTemplate }}
onChange={({ formData: newItemData }) => updateItem(index, newItemData)}
>
<div></div> {/* Prevents form buttons from showing */}
</Form>
</CardBody>
</Collapse>
</Card>
)
})}
</VStack>
)}
</CardBody>
)}
{/* Confirmation Dialog for Deletion */}
<ConfirmationDialog
isOpen={confirmDelete.isOpen}
onClose={handleCancelDelete}
onConfirm={handleConfirmDelete}
title={`Delete ${title.replace(/📊|📋|⚙️|📈|🗂️/g, '').trim()} Item`}
message={`Are you sure you want to delete this ${title.toLowerCase().replace(/📊|📋|⚙️|📈|🗂️/g, '').trim()} item?`}
itemName={confirmDelete.itemName}
confirmButtonText="Delete Item"
/>
</Card>
)
}
// Collapsible Plot Component
function CollapsiblePlotChart({ plotDefinition, plotVariables, onConfigUpdate, onReloadConfig, onRemove, isExpanded, onToggleExpansion }) {
return (
<Card>
<CardHeader>
<Flex align="center" justify="space-between">
<Box>
<Heading size="sm">📈 {plotDefinition.name || plotDefinition.id}</Heading>
<Text fontSize="xs" color="gray.500">
{plotDefinition.time_window}s window {plotVariables?.length || 0} variables
</Text>
</Box>
<HStack>
<Button
size="xs"
variant="outline"
onClick={() => onToggleExpansion(plotDefinition.id)}
rightIcon={isExpanded ? <ChevronUpIcon /> : <ChevronDownIcon />}
>
{isExpanded ? 'Hide' : 'Show'} Chart
</Button>
</HStack>
</Flex>
</CardHeader>
<Collapse in={isExpanded}>
<CardBody pt={0}>
<PlotRealtimeSession
plotDefinition={plotDefinition}
plotVariables={plotVariables}
onConfigUpdate={onConfigUpdate}
onReloadConfig={onReloadConfig}
onRemove={onRemove}
isCollapsed={true}
/>
</CardBody>
</Collapse>
</Card>
)
}
// 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 (
<Card>
<CardBody>
<Flex align="center" justify="center" py={8}>
<Spinner mr={3} />
<Text>Loading plot configurations...</Text>
</Flex>
</CardBody>
</Card>
)
}
return (
<VStack spacing={6} align="stretch">
<Flex align="center">
<Heading size="lg">📈 Plot Manager</Heading>
<Spacer />
<Button size="sm" variant="outline" onClick={loadPlotData}>
🔄 Refresh
</Button>
</Flex>
{/* Real-time Charts Section */}
<Card>
<CardHeader>
<Heading size="md">🔴 Real-time Plot Sessions</Heading>
<Text fontSize="sm" color="gray.500" mt={1}>
Live charts for configured plot sessions - click to expand/collapse
</Text>
</CardHeader>
<CardBody>
{getPlotDefinitions().length === 0 ? (
<Text color="gray.500" textAlign="center" py={8}>
No plot sessions configured. Create plot definitions below to get started.
</Text>
) : (
<VStack spacing={4} align="stretch">
{getPlotDefinitions().map((plotDef) => (
<CollapsiblePlotChart
key={plotDef.id}
plotDefinition={plotDef}
plotVariables={getPlotVariables(plotDef.id)}
onConfigUpdate={updatePlotConfig}
onReloadConfig={loadPlotData}
onRemove={() => {}}
isExpanded={isPlotExpanded(plotDef.id)}
onToggleExpansion={togglePlotExpansion}
/>
))}
</VStack>
)}
</CardBody>
</Card>
<Divider />
{/* Plot Definitions - Collapsible */}
<CollapsiblePlotItemsForm
data={plotsConfig}
schema={plotsSchemaData?.schema}
uiSchema={plotsSchemaData?.uiSchema}
onSave={savePlotsConfig}
title="Plot Definitions"
icon="📋"
getItemLabel={(item) => `${item.name || item.id} (${item.time_window || 60}s)`}
isExpanded={configExpanded}
onToggleExpansion={toggleConfigExpansion}
/>
{/* Plot Variables Configuration - Type 3 Form Pattern */}
<Card>
<CardHeader>
<Heading size="md"> Plot Variables Configuration</Heading>
<Text fontSize="sm" color="gray.500" mt={1}>
Select a plot, then configure which variables are displayed in that plot session
</Text>
</CardHeader>
<CardBody>
{/* Step 1: Plot Selector (Combo) */}
<VStack spacing={4} align="stretch">
<Box>
<Text fontSize="sm" fontWeight="bold" mb={2}>
🎯 Select Plot Session
</Text>
<Select
value={selectedPlotId}
onChange={(e) => setSelectedPlotId(e.target.value)}
placeholder="Choose a plot to configure..."
size="md"
>
{getPlotDefinitions().map(plot => (
<option key={plot.id} value={plot.id}>
📈 {plot.name} ({plot.id})
</option>
))}
</Select>
{getPlotDefinitions().length === 0 && (
<Text fontSize="sm" color="orange.500" mt={2}>
No plots available. Configure plot definitions first above.
</Text>
)}
</Box>
{/* Variables Configuration Form */}
{selectedPlotId && (
<Box>
<Divider mb={4} />
<Text fontSize="sm" fontWeight="bold" mb={2}>
Configure Variables for Plot "{selectedPlotId}"
</Text>
{/* Simplified schema for selected plot variables */}
{(() => {
const selectedPlotVars = getSelectedPlotVariables()
// Schema for this plot's variables - 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 (
<Form
schema={singlePlotSchema}
uiSchema={singlePlotUiSchema}
formData={selectedPlotVars}
validator={validator}
widgets={allWidgets}
templates={{ ObjectFieldTemplate: LayoutObjectFieldTemplate }}
onSubmit={({ formData }) => {
const updatedConfig = updateSelectedPlotVariables(formData)
savePlotVariables(updatedConfig).then(() => {
// Additional trigger after successful save
triggerVariableRefresh?.()
})
}}
onChange={({ formData }) => updateSelectedPlotVariables(formData)}
>
<HStack spacing={2} mt={4}>
<Button type="submit" colorScheme="red">
💾 Save Variables for {selectedPlotId}
</Button>
<Button variant="outline" onClick={loadPlotData}>
🔄 Reset
</Button>
</HStack>
</Form>
)
})()}
</Box>
)}
{!selectedPlotId && getPlotDefinitions().length > 0 && (
<Box textAlign="center" py={8}>
<Text color="gray.500">
👆 Select a plot above to configure its variables
</Text>
</Box>
)}
</VStack>
</CardBody>
</Card>
</VStack>
)
}

View File

@ -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 <Text color={muted}>Cargando sesiones de plots</Text>
}
if (sessionsList.length === 0) {
return (
<Card>
<CardBody>
<Text color={muted}>No hay sesiones de plot. Cree o edite plots en la sección superior.</Text>
</CardBody>
</Card>
)
}
return (
<VStack spacing={4} align="stretch">
{sessionsList.map((session) => (
<PlotRealtimeCard
key={session.session_id}
session={session}
onControl={controlSession}
onRefresh={refreshSession}
/>
))}
</VStack>
)
}
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 (
<Card bg={cardBg} borderColor={borderColor}>
<CardHeader>
<FlexHeader
session={session}
muted={muted}
onRefresh={() => onRefresh(session.session_id)}
onFullscreen={openFullscreen}
/>
</CardHeader>
<CardBody>
<ChartjsPlot session={enhancedSession} height="360px" />
<HStack mt={3} spacing={2}>
<Button size="sm" onClick={() => handleControlClick('start')} colorScheme="green"> Start</Button>
<Button size="sm" onClick={() => handleControlClick('pause')} colorScheme="yellow"> Pause</Button>
<Button size="sm" onClick={() => handleControlClick('clear')} variant="outline">🗑 Clear</Button>
<Button size="sm" onClick={() => handleControlClick('stop')} colorScheme="red"> Stop</Button>
<Spacer />
<Button
size="sm"
onClick={openFullscreen}
colorScheme="blue"
variant="solid"
>
🔍 Fullscreen
</Button>
</HStack>
</CardBody>
{/* Fullscreen Modal */}
<Modal isOpen={isFullscreen} onClose={closeFullscreen} size="full">
<ModalOverlay bg="blackAlpha.800" />
<ModalContent bg={cardBg} m={0} borderRadius={0}>
<ModalHeader>
<HStack>
<Text>📈 {session.name || session.session_id} - Fullscreen Mode</Text>
<Spacer />
<Text fontSize="sm" color={muted}>
Zoom: Drag to select area | Pan: Shift + Drag | Double-click to reset
</Text>
</HStack>
</ModalHeader>
<ModalCloseButton size="lg" />
<ModalBody p={4}>
<VStack spacing={4} h="full">
<ChartjsPlot session={enhancedSession} height="calc(100vh - 120px)" />
<HStack spacing={2}>
<Button size="sm" onClick={() => handleControlClick('start')} colorScheme="green"> Start</Button>
<Button size="sm" onClick={() => handleControlClick('pause')} colorScheme="yellow"> Pause</Button>
<Button size="sm" onClick={() => handleControlClick('clear')} variant="outline">🗑 Clear</Button>
<Button size="sm" onClick={() => handleControlClick('stop')} colorScheme="red"> Stop</Button>
{chartControlsRef.current && (
<Button
size="sm"
onClick={() => chartControlsRef.current.resetZoom?.()}
variant="outline"
>
🔄 Reset Zoom
</Button>
)}
</HStack>
</VStack>
</ModalBody>
</ModalContent>
</Modal>
</Card>
)
}
function FlexHeader({ session, muted, onRefresh, onFullscreen }) {
return (
<HStack align="center">
<Box>
<Heading size="sm">📈 {session.name || session.session_id}</Heading>
<Text fontSize="sm" color={muted} mt={1}>
Variables: {session.variables_count || 0} | Status: <strong>{session.is_active ? (session.is_paused ? 'Paused' : 'Active') : 'Stopped'}</strong>
</Text>
</Box>
<Spacer />
<HStack>
<Button
size="sm"
variant="outline"
onClick={onFullscreen}
colorScheme="blue"
>
🔍 Fullscreen
</Button>
<IconButton
icon={<SettingsIcon />}
size="sm"
variant="outline"
aria-label="Refresh status"
onClick={onRefresh}
/>
</HStack>
</HStack>
)
}

View File

@ -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 (
<VStack align="stretch" spacing={3}>
<HStack justify="space-between">
<Text fontWeight="semibold">{title}</Text>
<Button size="sm" leftIcon={<AddIcon />} onClick={onOpen}>
Add Variable
</Button>
</HStack>
<Box overflowX="auto" borderWidth="1px" borderRadius="md" borderColor={borderColor}>
<Table size="sm">
<Thead>
<Tr>
<Th>Variable Name</Th>
<Th width="100px">Actions</Th>
</Tr>
</Thead>
<Tbody>
{variables.length === 0 ? (
<Tr>
<Td colSpan={2}>
<Text color={muted} textAlign="center">No variables</Text>
</Td>
</Tr>
) : (
variables.map((variable, index) => (
<Tr key={index}>
<Td>{variable}</Td>
<Td>
<HStack spacing={1}>
<IconButton
icon={<EditIcon />}
size="xs"
variant="outline"
onClick={() => openEdit(index)}
/>
<IconButton
icon={<DeleteIcon />}
size="xs"
variant="outline"
colorScheme="red"
onClick={() => handleDelete(index)}
/>
</HStack>
</Td>
</Tr>
))
)}
</Tbody>
</Table>
</Box>
{/* Add Modal */}
<Modal isOpen={isOpen} onClose={onClose}>
<ModalOverlay />
<ModalContent>
<ModalHeader>Add Variable</ModalHeader>
<ModalCloseButton />
<ModalBody>
<VStack spacing={3}>
<Box width="100%">
<Text fontSize="sm" fontWeight="medium" mb={1}>
Variable Name *
</Text>
<Input
size="sm"
value={newVariable}
onChange={(e) => setNewVariable(e.target.value)}
placeholder="e.g., UR29_Brix"
/>
<Text fontSize="xs" color={muted} mt={1}>
Enter the name of the variable to plot
</Text>
</Box>
</VStack>
</ModalBody>
<ModalFooter>
<Button variant="ghost" mr={3} onClick={onClose}>Cancel</Button>
<Button colorScheme="blue" onClick={handleAdd}>Add</Button>
</ModalFooter>
</ModalContent>
</Modal>
{/* Edit Modal */}
<Modal isOpen={isEditOpen} onClose={onEditClose}>
<ModalOverlay />
<ModalContent>
<ModalHeader>Edit Variable</ModalHeader>
<ModalCloseButton />
<ModalBody>
<VStack spacing={3}>
<Box width="100%">
<Text fontSize="sm" fontWeight="medium" mb={1}>
Variable Name *
</Text>
<Input
size="sm"
value={editingValue}
onChange={(e) => setEditingValue(e.target.value)}
placeholder="e.g., UR29_Brix"
/>
<Text fontSize="xs" color={muted} mt={1}>
Enter the name of the variable to plot
</Text>
</Box>
</VStack>
</ModalBody>
<ModalFooter>
<Button variant="ghost" mr={3} onClick={onEditClose}>Cancel</Button>
<Button colorScheme="blue" onClick={handleEdit}>Save</Button>
</ModalFooter>
</ModalContent>
</Modal>
</VStack>
)
}
/**
* 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 <Text>Loading plots...</Text>
}
return (
<VStack align="stretch" spacing={4}>
{message && (
<Alert status={message.includes('Error') ? 'error' : 'success'}>
<AlertIcon />
{message}
</Alert>
)}
{/* Tabla de Plots */}
<Card>
<CardHeader>
<Heading size="sm">📈 Plots</Heading>
</CardHeader>
<CardBody>
{plotSchema ? (
<EditableTable
schema={plotSchema}
uiSchema={plotUiSchema.additionalProperties || {}}
data={plotsForTable}
onChange={(newData) => {
// 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"
/>
) : (
<Alert status="warning">
<AlertIcon />
No plot schema available
</Alert>
)}
</CardBody>
</Card>
<Divider />
{/* Selector de Plot y Tabla de Variables */}
<Card>
<CardHeader>
<HStack justify="space-between">
<Heading size="sm">🔧 Plot Variables</Heading>
<HStack>
<Text fontSize="sm" color={muted}>Plot:</Text>
<Select
size="sm"
value={selectedPlotId}
onChange={(e) => setSelectedPlotId(e.target.value)}
placeholder="Select plot"
width="200px"
>
{plotOptions.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</Select>
</HStack>
</HStack>
</CardHeader>
<CardBody>
{!selectedPlotId ? (
<Alert status="info">
<AlertIcon />
Select a plot to manage its variables
</Alert>
) : (
<PlotVariablesTable
variables={variablesForTable}
onChange={savePlotVariables}
title={`Variables for plot: ${plots[selectedPlotId]?.name || selectedPlotId}`}
/>
)}
</CardBody>
</Card>
</VStack>
)
}

View File

@ -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 <Text>Loading coordinator...</Text>
}
return (
<Box p={3} bg={bgColor} borderRadius="md" border="1px" borderColor="gray.200">
<VStack align="start" spacing={2}>
<HStack>
<Text fontWeight="bold">🔗 Tab Coordination Status</Text>
<Badge colorScheme={isLeader ? 'green' : 'blue'}>
{isLeader ? '👑 Leader' : '👥 Follower'}
</Badge>
</HStack>
<Text fontSize="sm" color="gray.600">
Tab ID: <code>{tabInfo.tabId}</code>
</Text>
<Text fontSize="sm" color="gray.600">
Role: {isLeader ? 'Making real connections to backend' : 'Receiving data from leader tab'}
</Text>
<Text fontSize="xs" color="gray.500">
Only the leader tab creates actual HTTP connections. Other tabs receive data via BroadcastChannel.
</Text>
</VStack>
</Box>
)
}

View File

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

10
main.py
View File

@ -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)
# ==============================

View File

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

View File

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

View File

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

Binary file not shown.

View File

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