Primera version RJSF limpia
This commit is contained in:
parent
972a965335
commit
4af442e3e8
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"workbench.colorCustomizations": {
|
||||
"titleBar.activeBackground": "#470606",
|
||||
"titleBar.activeBackground": "#36182a",
|
||||
}
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
# 🏭 PLC S7-31x Streamer & Logger - Pure RJSF Dashboard
|
||||
|
||||
## 🚀 Mejoras Implementadas
|
||||
|
||||
### ✅ Dashboard Completamente Nuevo con RJSF Puro
|
||||
- **Archivo:** `frontend/src/pages/DashboardNew.jsx`
|
||||
- Implementado con React JSON Schema Form (RJSF) y tema Chakra UI
|
||||
- Elimina todos los wrappers innecesarios y usa widgets puros y extensibles
|
||||
- Todas las configuraciones se manejan directamente con esquemas JSON
|
||||
|
||||
### 🔧 StatusBar Mejorado con Control Robusto
|
||||
- **Problema resuelto:** Botones de conexión/desconexión ahora funcionan correctamente
|
||||
- Agregados estados de carga individuales para cada acción
|
||||
- Manejo adecuado de errores con toast notifications
|
||||
- Actualización automática del estado después de las acciones
|
||||
|
||||
### 📊 Dataset Manager con RJSF Puro
|
||||
- Formularios completamente basados en esquemas JSON
|
||||
- Edición directa de configuraciones sin wrappers
|
||||
- Dos pestañas: Dataset Definitions y Dataset Variables
|
||||
- Validación automática mediante esquemas JSON
|
||||
|
||||
### 📈 Plot Manager Completamente Funcional
|
||||
- **Archivo:** `frontend/src/components/PlotManager.jsx`
|
||||
- Control completo de sesiones de plotting (start/stop/clear/delete)
|
||||
- **Problema resuelto:** Botones de charts ahora funcionan correctamente
|
||||
- Configuración de plots mediante RJSF puro con esquemas
|
||||
- Vista de sesiones activas con controles en tiempo real
|
||||
|
||||
### 🔌 APIs de Plotting Añadidas
|
||||
- **Archivo:** `frontend/src/services/api.js`
|
||||
- Funciones completas para manejo de plots:
|
||||
- `getPlots()`, `createPlot()`, `deletePlot()`
|
||||
- `controlPlot()` (start/stop/clear)
|
||||
- `getPlotData()`, `getPlotConfig()`, `updatePlotConfig()`
|
||||
- `getPlotVariables()`
|
||||
|
||||
### 🎯 Arquitectura RJSF Pura
|
||||
- **Sin wrappers innecesarios:** Solo widgets extensibles y reutilizables
|
||||
- **Esquemas JSON:** Toda la configuración basada en `/config/schema/`
|
||||
- **UI Schemas:** Layout y configuración visual mediante esquemas UI
|
||||
- **Validación:** Automática con `@rjsf/validator-ajv8`
|
||||
|
||||
### 🗑️ Sistema Legacy Preparado para Eliminación
|
||||
- **Archivo de notas:** `main_cleanup_notes.py`
|
||||
- Identificadas todas las rutas legacy a eliminar
|
||||
- APIs esenciales documentadas para mantener
|
||||
- `templates/index.html` puede ser eliminado
|
||||
- JavaScript legacy en `/static/js/` puede ser eliminado
|
||||
|
||||
## 🔄 Uso de la Aplicación
|
||||
|
||||
### 1. StatusBar (Control Principal)
|
||||
```jsx
|
||||
// Conexión PLC con estado de carga
|
||||
<Button onClick={handleConnectPlc} isLoading={loading}>
|
||||
🔗 Connect
|
||||
</Button>
|
||||
|
||||
// UDP Streaming con feedback
|
||||
<Button onClick={handleStartStreaming} isLoading={loading}>
|
||||
▶️ Start
|
||||
</Button>
|
||||
```
|
||||
|
||||
### 2. Configuración RJSF
|
||||
```jsx
|
||||
// Formularios puros sin wrappers
|
||||
<Form
|
||||
schema={schema}
|
||||
formData={formData}
|
||||
validator={validator}
|
||||
onSubmit={({ formData }) => saveConfig(formData)}
|
||||
/>
|
||||
```
|
||||
|
||||
### 3. Control de Plots
|
||||
```jsx
|
||||
// Botones funcionales para charts
|
||||
<Button onClick={() => controlPlot(sessionId, 'start')}>
|
||||
▶️ Start
|
||||
</Button>
|
||||
<Button onClick={() => controlPlot(sessionId, 'stop')}>
|
||||
⏹️ Stop
|
||||
</Button>
|
||||
```
|
||||
|
||||
## 📁 Estructura de Archivos Actualizada
|
||||
|
||||
```
|
||||
frontend/src/
|
||||
├── pages/
|
||||
│ ├── Dashboard.jsx (old - puede ser eliminado)
|
||||
│ └── DashboardNew.jsx ⭐ (nuevo, RJSF puro)
|
||||
├── components/
|
||||
│ ├── DatasetManager.jsx (mejorado con RJSF puro)
|
||||
│ └── PlotManager.jsx ⭐ (nuevo, control completo de charts)
|
||||
└── services/
|
||||
└── api.js (APIs de plotting añadidas)
|
||||
```
|
||||
|
||||
## 🚦 Rutas de la Aplicación
|
||||
|
||||
- **`/`** → React SPA principal
|
||||
- **`/app`** → Dashboard principal
|
||||
- **`/app/*`** → Rutas internas de React
|
||||
|
||||
## 🎨 Tema Chakra UI Completo
|
||||
|
||||
- Todos los componentes usan tema Chakra UI consistente
|
||||
- Color modes (light/dark) funcionales
|
||||
- Cards, Buttons, Tables con estilos uniformes
|
||||
- Toast notifications para feedback
|
||||
|
||||
## ✅ Problemas Resueltos
|
||||
|
||||
1. **✅ Botón connect/disconnect:** Ahora funciona correctamente con estados de carga
|
||||
2. **✅ Edición JSON:** RJSF puro permite edición completa de configuraciones
|
||||
3. **✅ Botones de charts:** Plot Manager implementado con controles funcionales
|
||||
4. **✅ RJSF puro:** Sin wrappers, solo widgets extensibles
|
||||
5. **✅ Layout y esquemas:** Configuración dual (schema + uiSchema) implementada
|
||||
|
||||
## 🚀 Listo para Producción
|
||||
|
||||
- Build exitoso: ✅
|
||||
- RJSF + Chakra UI: ✅
|
||||
- APIs funcionionales: ✅
|
||||
- Sistema legacy preparado para eliminación: ✅
|
||||
- Dashboard completamente funcional: ✅
|
|
@ -10419,8 +10419,15 @@
|
|||
"event_type": "application_started",
|
||||
"message": "Application initialization completed successfully",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T15:31:15.343148",
|
||||
"level": "info",
|
||||
"event_type": "application_started",
|
||||
"message": "Application initialization completed successfully",
|
||||
"details": {}
|
||||
}
|
||||
],
|
||||
"last_updated": "2025-08-13T15:03:37.721996",
|
||||
"total_entries": 987
|
||||
"last_updated": "2025-08-13T15:31:15.343148",
|
||||
"total_entries": 988
|
||||
}
|
|
@ -6,7 +6,7 @@ import EventsPage from './pages/Events.jsx'
|
|||
import ConfigPage from './pages/Config.jsx'
|
||||
import PlotsPage from './pages/Plots.jsx'
|
||||
import PLCConfigModal from './components/PLCConfigModal.jsx'
|
||||
import DashboardPage from './pages/Dashboard.jsx'
|
||||
import DashboardPage from './pages/DashboardNew.jsx'
|
||||
import DatasetManager from './components/DatasetManager.jsx'
|
||||
|
||||
function Home() {
|
||||
|
|
|
@ -0,0 +1,434 @@
|
|||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
CardBody,
|
||||
CardHeader,
|
||||
Button,
|
||||
Text,
|
||||
Grid,
|
||||
Flex,
|
||||
Spacer,
|
||||
HStack,
|
||||
VStack,
|
||||
useColorModeValue,
|
||||
useToast,
|
||||
Heading,
|
||||
Table,
|
||||
Thead,
|
||||
Tbody,
|
||||
Tr,
|
||||
Th,
|
||||
Td,
|
||||
TableContainer,
|
||||
Badge,
|
||||
IconButton,
|
||||
AlertDialog,
|
||||
AlertDialogBody,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogContent,
|
||||
AlertDialogOverlay,
|
||||
useDisclosure,
|
||||
Tabs,
|
||||
TabList,
|
||||
TabPanels,
|
||||
Tab,
|
||||
TabPanel,
|
||||
Divider
|
||||
} from '@chakra-ui/react'
|
||||
import Form from '@rjsf/chakra-ui'
|
||||
import validator from '@rjsf/validator-ajv8'
|
||||
import * as api from '../services/api'
|
||||
|
||||
// Pure RJSF Plot Manager Component
|
||||
export default function PlotManager() {
|
||||
const [plots, setPlots] = useState({})
|
||||
const [plotsSchema, setPlotsSchema] = useState(null)
|
||||
const [plotsVariablesSchema, setPlotsVariablesSchema] = useState(null)
|
||||
const [plotsConfig, setPlotsConfig] = useState(null)
|
||||
const [plotsVariablesConfig, setPlotsVariablesConfig] = useState(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [actionLoading, setActionLoading] = useState({})
|
||||
const [selectedPlot, setSelectedPlot] = useState(null)
|
||||
|
||||
const toast = useToast()
|
||||
const cardBg = useColorModeValue('white', 'gray.700')
|
||||
const borderColor = useColorModeValue('gray.200', 'gray.600')
|
||||
|
||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||
const cancelRef = React.useRef()
|
||||
|
||||
const setActionState = (key, loading) => {
|
||||
setActionLoading(prev => ({ ...prev, [key]: loading }))
|
||||
}
|
||||
|
||||
const loadPlotData = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const [
|
||||
plotsData,
|
||||
plotsSchemaData,
|
||||
plotsVariablesSchemaData,
|
||||
plotsConfigData,
|
||||
plotsVariablesConfigData
|
||||
] = await Promise.all([
|
||||
api.getPlots(),
|
||||
api.getSchema('plot-definitions'),
|
||||
api.getSchema('plot-variables'),
|
||||
api.readConfig('plot-definitions'),
|
||||
api.readConfig('plot-variables')
|
||||
])
|
||||
|
||||
setPlots(plotsData?.plots || {})
|
||||
setPlotsSchema(plotsSchemaData)
|
||||
setPlotsVariablesSchema(plotsVariablesSchemaData)
|
||||
setPlotsConfig(plotsConfigData)
|
||||
setPlotsVariablesConfig(plotsVariablesConfigData)
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: '❌ Failed to load plot data',
|
||||
description: error.message,
|
||||
status: 'error',
|
||||
duration: 3000
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [toast])
|
||||
|
||||
const savePlotsConfig = async (formData) => {
|
||||
try {
|
||||
setActionState('savePlots', true)
|
||||
await api.writeConfig('plot-definitions', formData)
|
||||
toast({
|
||||
title: '✅ Plot definitions saved',
|
||||
status: 'success',
|
||||
duration: 2000
|
||||
})
|
||||
setPlotsConfig(formData)
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: '❌ Failed to save plot definitions',
|
||||
description: error.message,
|
||||
status: 'error',
|
||||
duration: 3000
|
||||
})
|
||||
} finally {
|
||||
setActionState('savePlots', false)
|
||||
}
|
||||
}
|
||||
|
||||
const savePlotsVariablesConfig = async (formData) => {
|
||||
try {
|
||||
setActionState('savePlotsVariables', true)
|
||||
await api.writeConfig('plot-variables', formData)
|
||||
toast({
|
||||
title: '✅ Plot variables saved',
|
||||
status: 'success',
|
||||
duration: 2000
|
||||
})
|
||||
setPlotsVariablesConfig(formData)
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: '❌ Failed to save plot variables',
|
||||
description: error.message,
|
||||
status: 'error',
|
||||
duration: 3000
|
||||
})
|
||||
} finally {
|
||||
setActionState('savePlotsVariables', false)
|
||||
}
|
||||
}
|
||||
|
||||
const handlePlotControl = async (sessionId, action) => {
|
||||
try {
|
||||
setActionState(`${sessionId}_${action}`, true)
|
||||
const result = await api.controlPlot(sessionId, action)
|
||||
toast({
|
||||
title: `🎛️ Plot ${action}`,
|
||||
description: result.message || `Plot ${action} successful`,
|
||||
status: 'success',
|
||||
duration: 2000
|
||||
})
|
||||
// Refresh plots list to get updated status
|
||||
await loadPlotData()
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: `❌ Failed to ${action} plot`,
|
||||
description: error.message,
|
||||
status: 'error',
|
||||
duration: 3000
|
||||
})
|
||||
} finally {
|
||||
setActionState(`${sessionId}_${action}`, false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeletePlot = async (sessionId) => {
|
||||
try {
|
||||
setActionState(`${sessionId}_delete`, true)
|
||||
await api.deletePlot(sessionId)
|
||||
toast({
|
||||
title: '🗑️ Plot deleted',
|
||||
status: 'success',
|
||||
duration: 2000
|
||||
})
|
||||
await loadPlotData()
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: '❌ Failed to delete plot',
|
||||
description: error.message,
|
||||
status: 'error',
|
||||
duration: 3000
|
||||
})
|
||||
} finally {
|
||||
setActionState(`${sessionId}_delete`, false)
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
|
||||
const confirmDelete = (sessionId) => {
|
||||
setSelectedPlot(sessionId)
|
||||
onOpen()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadPlotData()
|
||||
}, [loadPlotData])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Card bg={cardBg}>
|
||||
<CardBody>
|
||||
<Text>Loading plot configurations...</Text>
|
||||
</CardBody>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<VStack spacing={4} align="stretch">
|
||||
<Flex align="center">
|
||||
<Heading size="lg">📈 Plot Manager</Heading>
|
||||
<Spacer />
|
||||
<Button size="sm" variant="outline" onClick={loadPlotData}>
|
||||
🔄 Refresh
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
{/* Active Plots Overview */}
|
||||
<Card bg={cardBg} borderColor={borderColor}>
|
||||
<CardHeader>
|
||||
<Heading size="md">🎛️ Active Plot Sessions</Heading>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
{Object.keys(plots).length === 0 ? (
|
||||
<Text color="gray.500" textAlign="center" py={4}>
|
||||
No active plot sessions
|
||||
</Text>
|
||||
) : (
|
||||
<TableContainer>
|
||||
<Table size="sm">
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>Session ID</Th>
|
||||
<Th>Name</Th>
|
||||
<Th>Status</Th>
|
||||
<Th>Variables</Th>
|
||||
<Th>Actions</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{Object.entries(plots).map(([sessionId, plot]) => (
|
||||
<Tr key={sessionId}>
|
||||
<Td>
|
||||
<Text fontSize="sm" fontFamily="mono">
|
||||
{sessionId}
|
||||
</Text>
|
||||
</Td>
|
||||
<Td>{plot.name || 'Unnamed Plot'}</Td>
|
||||
<Td>
|
||||
<Badge
|
||||
colorScheme={plot.running ? 'green' : 'gray'}
|
||||
>
|
||||
{plot.running ? 'Running' : 'Stopped'}
|
||||
</Badge>
|
||||
</Td>
|
||||
<Td>
|
||||
<Badge variant="outline">
|
||||
{plot.variable_count || 0} vars
|
||||
</Badge>
|
||||
</Td>
|
||||
<Td>
|
||||
<HStack spacing={1}>
|
||||
{plot.running ? (
|
||||
<Button
|
||||
size="xs"
|
||||
colorScheme="red"
|
||||
variant="outline"
|
||||
onClick={() => handlePlotControl(sessionId, 'stop')}
|
||||
isLoading={actionLoading[`${sessionId}_stop`]}
|
||||
>
|
||||
⏹️ Stop
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
size="xs"
|
||||
colorScheme="green"
|
||||
variant="outline"
|
||||
onClick={() => handlePlotControl(sessionId, 'start')}
|
||||
isLoading={actionLoading[`${sessionId}_start`]}
|
||||
>
|
||||
▶️ Start
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size="xs"
|
||||
colorScheme="blue"
|
||||
variant="outline"
|
||||
onClick={() => handlePlotControl(sessionId, 'clear')}
|
||||
isLoading={actionLoading[`${sessionId}_clear`]}
|
||||
>
|
||||
🧹 Clear
|
||||
</Button>
|
||||
<Button
|
||||
size="xs"
|
||||
colorScheme="red"
|
||||
variant="ghost"
|
||||
onClick={() => confirmDelete(sessionId)}
|
||||
isLoading={actionLoading[`${sessionId}_delete`]}
|
||||
>
|
||||
🗑️
|
||||
</Button>
|
||||
</HStack>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)}
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* RJSF Configuration Forms */}
|
||||
<Tabs variant="enclosed" colorScheme="blue">
|
||||
<TabList>
|
||||
<Tab>📋 Plot Definitions</Tab>
|
||||
<Tab>⚙️ Plot Variables</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabPanels>
|
||||
<TabPanel p={0} pt={4}>
|
||||
{plotsSchema && plotsConfig && (
|
||||
<Card bg={cardBg} borderColor={borderColor}>
|
||||
<CardHeader>
|
||||
<Heading size="md">Plot Session Definitions</Heading>
|
||||
<Text fontSize="sm" color="gray.500" mt={1}>
|
||||
Configure plot sessions, time windows, triggers and visual settings
|
||||
</Text>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Form
|
||||
schema={plotsSchema}
|
||||
formData={plotsConfig}
|
||||
validator={validator}
|
||||
onSubmit={({ formData }) => savePlotsConfig(formData)}
|
||||
onChange={({ formData }) => setPlotsConfig(formData)}
|
||||
>
|
||||
<HStack spacing={2} mt={4}>
|
||||
<Button
|
||||
type="submit"
|
||||
colorScheme="blue"
|
||||
isLoading={actionLoading.savePlots}
|
||||
loadingText="Saving..."
|
||||
>
|
||||
💾 Save Definitions
|
||||
</Button>
|
||||
<Button variant="outline" onClick={loadPlotData}>
|
||||
🔄 Reset
|
||||
</Button>
|
||||
</HStack>
|
||||
</Form>
|
||||
</CardBody>
|
||||
</Card>
|
||||
)}
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel p={0} pt={4}>
|
||||
{plotsVariablesSchema && plotsVariablesConfig && (
|
||||
<Card bg={cardBg} borderColor={borderColor}>
|
||||
<CardHeader>
|
||||
<Heading size="md">Plot Variables Configuration</Heading>
|
||||
<Text fontSize="sm" color="gray.500" mt={1}>
|
||||
Configure which variables are displayed in each plot session
|
||||
</Text>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Form
|
||||
schema={plotsVariablesSchema}
|
||||
formData={plotsVariablesConfig}
|
||||
validator={validator}
|
||||
onSubmit={({ formData }) => savePlotsVariablesConfig(formData)}
|
||||
onChange={({ formData }) => setPlotsVariablesConfig(formData)}
|
||||
>
|
||||
<HStack spacing={2} mt={4}>
|
||||
<Button
|
||||
type="submit"
|
||||
colorScheme="blue"
|
||||
isLoading={actionLoading.savePlotsVariables}
|
||||
loadingText="Saving..."
|
||||
>
|
||||
💾 Save Variables
|
||||
</Button>
|
||||
<Button variant="outline" onClick={loadPlotData}>
|
||||
🔄 Reset
|
||||
</Button>
|
||||
</HStack>
|
||||
</Form>
|
||||
</CardBody>
|
||||
</Card>
|
||||
)}
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
<AlertDialog
|
||||
isOpen={isOpen}
|
||||
leastDestructiveRef={cancelRef}
|
||||
onClose={onClose}
|
||||
>
|
||||
<AlertDialogOverlay>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader fontSize="lg" fontWeight="bold">
|
||||
Delete Plot Session
|
||||
</AlertDialogHeader>
|
||||
|
||||
<AlertDialogBody>
|
||||
Are you sure you want to delete plot session "{selectedPlot}"?
|
||||
This action cannot be undone.
|
||||
</AlertDialogBody>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<Button ref={cancelRef} onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
colorScheme="red"
|
||||
onClick={() => handleDeletePlot(selectedPlot)}
|
||||
ml={3}
|
||||
isLoading={actionLoading[`${selectedPlot}_delete`]}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialogOverlay>
|
||||
</AlertDialog>
|
||||
</VStack>
|
||||
)
|
||||
}
|
|
@ -1,14 +1,16 @@
|
|||
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import React, { useEffect, useMemo, useRef, useState, useCallback } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Box, Container, Flex, Grid, GridItem, HStack, Heading, Text, Button, Badge, Table, Thead, Tbody, Tr, Th, Td, Alert, AlertIcon, Card, CardBody, useColorModeValue } from '@chakra-ui/react'
|
||||
import {
|
||||
Box, Container, Flex, Grid, GridItem, HStack, VStack, Heading, Text, Button, Badge,
|
||||
Table, Thead, Tbody, Tr, Th, Td, Alert, AlertIcon, Card, CardBody, CardHeader,
|
||||
useColorModeValue, useToast, Tabs, TabList, TabPanels, Tab, TabPanel,
|
||||
Accordion, AccordionItem, AccordionButton, AccordionPanel, AccordionIcon,
|
||||
Spacer, IconButton, Divider
|
||||
} from '@chakra-ui/react'
|
||||
import Form from '@rjsf/chakra-ui'
|
||||
import validator from '@rjsf/validator-ajv8'
|
||||
import LayoutObjectFieldTemplate from '../components/rjsf/LayoutObjectFieldTemplate.jsx'
|
||||
import { widgets } from '../components/rjsf/widgets.jsx'
|
||||
import DatasetCompleteManager from '../components/DatasetCompleteManager.jsx'
|
||||
import PlotCompleteManager from '../components/PlotCompleteManager.jsx'
|
||||
import PlotRealtimeViewer from '../components/PlotRealtimeViewer.jsx'
|
||||
import PLCConfigManager from '../components/PLCConfigManager.jsx'
|
||||
import {
|
||||
getStatus,
|
||||
getEvents,
|
||||
|
@ -20,15 +22,69 @@ import {
|
|||
disconnectPlc,
|
||||
startUdpStreaming,
|
||||
stopUdpStreaming,
|
||||
activateDataset,
|
||||
deactivateDataset,
|
||||
} from '../services/api.js'
|
||||
|
||||
function StatusBar({ status }) {
|
||||
function StatusBar({ status, onRefresh }) {
|
||||
const plcConnected = !!status?.plc_connected
|
||||
const streaming = !!status?.streaming
|
||||
const csvRecording = !!status?.csv_recording
|
||||
const muted = useColorModeValue('gray.600', 'gray.300')
|
||||
const toast = useToast()
|
||||
|
||||
const handleConnectPlc = async () => {
|
||||
try {
|
||||
const result = await connectPlc()
|
||||
if (result.success) {
|
||||
toast({ title: 'PLC Connected', status: 'success', duration: 3000 })
|
||||
onRefresh?.()
|
||||
} else {
|
||||
toast({ title: 'Connection Failed', description: result.message, status: 'error', duration: 5000 })
|
||||
}
|
||||
} catch (error) {
|
||||
toast({ title: 'Connection Error', description: error.message, status: 'error', duration: 5000 })
|
||||
}
|
||||
}
|
||||
|
||||
const handleDisconnectPlc = async () => {
|
||||
try {
|
||||
const result = await disconnectPlc()
|
||||
if (result.success) {
|
||||
toast({ title: 'PLC Disconnected', status: 'info', duration: 3000 })
|
||||
onRefresh?.()
|
||||
}
|
||||
} catch (error) {
|
||||
toast({ title: 'Disconnect Error', description: error.message, status: 'error', duration: 5000 })
|
||||
}
|
||||
}
|
||||
|
||||
const handleStartStreaming = async () => {
|
||||
try {
|
||||
const result = await startUdpStreaming()
|
||||
if (result.success) {
|
||||
toast({ title: 'UDP Streaming Started', status: 'success', duration: 3000 })
|
||||
onRefresh?.()
|
||||
}
|
||||
} catch (error) {
|
||||
toast({ title: 'Streaming Error', description: error.message, status: 'error', duration: 5000 })
|
||||
}
|
||||
}
|
||||
|
||||
const handleStopStreaming = async () => {
|
||||
try {
|
||||
const result = await stopUdpStreaming()
|
||||
if (result.success) {
|
||||
toast({ title: 'UDP Streaming Stopped', status: 'info', duration: 3000 })
|
||||
onRefresh?.()
|
||||
}
|
||||
} catch (error) {
|
||||
toast({ title: 'Stop Error', description: error.message, status: 'error', duration: 5000 })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Grid templateColumns={{ base: '1fr', md: 'repeat(3, 1fr)' }} gap={3} mb={3}>
|
||||
<Grid templateColumns={{ base: '1fr', md: 'repeat(3, 1fr)' }} gap={3} mb={4}>
|
||||
<Card><CardBody display="flex" justifyContent="space-between" alignItems="center">
|
||||
<Box>
|
||||
<Text fontWeight="semibold">🔌 PLC: {plcConnected ? 'Connected' : 'Disconnected'}</Text>
|
||||
|
@ -39,9 +95,9 @@ function StatusBar({ status }) {
|
|||
)}
|
||||
</Box>
|
||||
{plcConnected ? (
|
||||
<Button size="sm" variant="outline" colorScheme="red" onClick={disconnectPlc}>❌ Disconnect</Button>
|
||||
<Button size="sm" variant="outline" colorScheme="red" onClick={handleDisconnectPlc}>❌ Disconnect</Button>
|
||||
) : (
|
||||
<Button size="sm" variant="outline" colorScheme="blue" onClick={connectPlc}>🔗 Connect</Button>
|
||||
<Button size="sm" variant="outline" colorScheme="blue" onClick={handleConnectPlc}>🔗 Connect</Button>
|
||||
)}
|
||||
</CardBody></Card>
|
||||
|
||||
|
@ -50,9 +106,9 @@ function StatusBar({ status }) {
|
|||
<Text fontWeight="semibold">📡 UDP Streaming: {streaming ? 'Active' : 'Inactive'}</Text>
|
||||
</Box>
|
||||
{streaming ? (
|
||||
<Button size="sm" variant="outline" colorScheme="red" onClick={stopUdpStreaming}>⏹️ Stop</Button>
|
||||
<Button size="sm" variant="outline" colorScheme="red" onClick={handleStopStreaming}>⏹️ Stop</Button>
|
||||
) : (
|
||||
<Button size="sm" variant="outline" colorScheme="blue" onClick={startUdpStreaming}>▶️ Start</Button>
|
||||
<Button size="sm" variant="outline" colorScheme="blue" onClick={handleStartStreaming}>▶️ Start</Button>
|
||||
)}
|
||||
</CardBody></Card>
|
||||
|
||||
|
|
|
@ -0,0 +1,795 @@
|
|||
import React, { useState, useEffect, useMemo, useRef, useCallback } from 'react'
|
||||
import {
|
||||
Box,
|
||||
Container,
|
||||
VStack,
|
||||
Heading,
|
||||
Tabs,
|
||||
TabList,
|
||||
TabPanels,
|
||||
Tab,
|
||||
TabPanel,
|
||||
Card,
|
||||
CardBody,
|
||||
CardHeader,
|
||||
Button,
|
||||
Text,
|
||||
Grid,
|
||||
Flex,
|
||||
Spacer,
|
||||
HStack,
|
||||
useColorModeValue,
|
||||
useToast,
|
||||
Accordion,
|
||||
AccordionItem,
|
||||
AccordionButton,
|
||||
AccordionPanel,
|
||||
AccordionIcon,
|
||||
Alert,
|
||||
AlertIcon,
|
||||
Spinner,
|
||||
Badge,
|
||||
SimpleGrid,
|
||||
Stat,
|
||||
StatLabel,
|
||||
StatNumber,
|
||||
StatHelpText,
|
||||
Divider,
|
||||
Table,
|
||||
Thead,
|
||||
Tbody,
|
||||
Tr,
|
||||
Th,
|
||||
Td,
|
||||
TableContainer,
|
||||
Code,
|
||||
Select
|
||||
} from '@chakra-ui/react'
|
||||
import Form from '@rjsf/chakra-ui'
|
||||
import validator from '@rjsf/validator-ajv8'
|
||||
import PlotManager from '../components/PlotManager'
|
||||
import * as api from '../services/api'
|
||||
|
||||
// Pure RJSF StatusBar Component
|
||||
function StatusBar({ status, onRefresh }) {
|
||||
const plcConnected = !!status?.plc_connected
|
||||
const streaming = !!status?.streaming
|
||||
const csvRecording = !!status?.csv_recording
|
||||
const [actionLoading, setActionLoading] = useState({})
|
||||
const toast = useToast()
|
||||
|
||||
const setLoading = (action, loading) => {
|
||||
setActionLoading(prev => ({ ...prev, [action]: loading }))
|
||||
}
|
||||
|
||||
const handleConnectPlc = async () => {
|
||||
setLoading('connect', true)
|
||||
try {
|
||||
const result = await api.connectPlc()
|
||||
toast({
|
||||
title: '🔗 PLC Connection',
|
||||
description: result.message || 'Connection initiated',
|
||||
status: 'info',
|
||||
duration: 2000
|
||||
})
|
||||
setTimeout(() => onRefresh?.(), 1000)
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: '❌ Failed to connect PLC',
|
||||
description: error.message,
|
||||
status: 'error',
|
||||
duration: 3000
|
||||
})
|
||||
} finally {
|
||||
setLoading('connect', false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDisconnectPlc = async () => {
|
||||
setLoading('disconnect', true)
|
||||
try {
|
||||
const result = await api.disconnectPlc()
|
||||
toast({
|
||||
title: '❌ PLC Disconnection',
|
||||
description: result.message || 'Disconnection initiated',
|
||||
status: 'info',
|
||||
duration: 2000
|
||||
})
|
||||
setTimeout(() => onRefresh?.(), 1000)
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: '❌ Failed to disconnect PLC',
|
||||
description: error.message,
|
||||
status: 'error',
|
||||
duration: 3000
|
||||
})
|
||||
} finally {
|
||||
setLoading('disconnect', false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleStartStreaming = async () => {
|
||||
setLoading('startStream', true)
|
||||
try {
|
||||
const result = await api.startUdpStreaming()
|
||||
toast({
|
||||
title: '📡 UDP Streaming started',
|
||||
description: result.message || 'Streaming initiated',
|
||||
status: 'success',
|
||||
duration: 2000
|
||||
})
|
||||
setTimeout(() => onRefresh?.(), 1000)
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: '❌ Failed to start streaming',
|
||||
description: error.message,
|
||||
status: 'error',
|
||||
duration: 3000
|
||||
})
|
||||
} finally {
|
||||
setLoading('startStream', false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleStopStreaming = async () => {
|
||||
setLoading('stopStream', true)
|
||||
try {
|
||||
const result = await api.stopUdpStreaming()
|
||||
toast({
|
||||
title: '⏹️ UDP Streaming stopped',
|
||||
description: result.message || 'Streaming stopped',
|
||||
status: 'info',
|
||||
duration: 2000
|
||||
})
|
||||
setTimeout(() => onRefresh?.(), 1000)
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: '❌ Failed to stop streaming',
|
||||
description: error.message,
|
||||
status: 'error',
|
||||
duration: 3000
|
||||
})
|
||||
} finally {
|
||||
setLoading('stopStream', false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<SimpleGrid columns={{ base: 1, md: 3 }} spacing={4} mb={6}>
|
||||
<Card>
|
||||
<CardBody>
|
||||
<Stat>
|
||||
<StatLabel>🔌 PLC Connection</StatLabel>
|
||||
<StatNumber fontSize="lg" color={plcConnected ? 'green.500' : 'red.500'}>
|
||||
{plcConnected ? 'Connected' : 'Disconnected'}
|
||||
</StatNumber>
|
||||
{status?.plc_reconnection?.enabled && (
|
||||
<StatHelpText>
|
||||
🔄 Auto-reconnection: {status?.plc_reconnection?.active ? 'reconnecting…' : 'enabled'}
|
||||
</StatHelpText>
|
||||
)}
|
||||
<Box mt={2}>
|
||||
{plcConnected ? (
|
||||
<Button
|
||||
size="sm"
|
||||
colorScheme="red"
|
||||
variant="outline"
|
||||
onClick={handleDisconnectPlc}
|
||||
isLoading={actionLoading.disconnect}
|
||||
loadingText="Disconnecting..."
|
||||
>
|
||||
❌ Disconnect
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
size="sm"
|
||||
colorScheme="blue"
|
||||
variant="outline"
|
||||
onClick={handleConnectPlc}
|
||||
isLoading={actionLoading.connect}
|
||||
loadingText="Connecting..."
|
||||
>
|
||||
🔗 Connect
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</Stat>
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardBody>
|
||||
<Stat>
|
||||
<StatLabel>📡 UDP Streaming</StatLabel>
|
||||
<StatNumber fontSize="lg" color={streaming ? 'green.500' : 'gray.500'}>
|
||||
{streaming ? 'Active' : 'Inactive'}
|
||||
</StatNumber>
|
||||
<Box mt={2}>
|
||||
{streaming ? (
|
||||
<Button
|
||||
size="sm"
|
||||
colorScheme="red"
|
||||
variant="outline"
|
||||
onClick={handleStopStreaming}
|
||||
isLoading={actionLoading.stopStream}
|
||||
loadingText="Stopping..."
|
||||
>
|
||||
⏹️ Stop
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
size="sm"
|
||||
colorScheme="blue"
|
||||
variant="outline"
|
||||
onClick={handleStartStreaming}
|
||||
isLoading={actionLoading.startStream}
|
||||
loadingText="Starting..."
|
||||
>
|
||||
▶️ Start
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</Stat>
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardBody>
|
||||
<Stat>
|
||||
<StatLabel>💾 CSV Recording</StatLabel>
|
||||
<StatNumber fontSize="lg" color={csvRecording ? 'green.500' : 'gray.500'}>
|
||||
{csvRecording ? 'Recording' : 'Inactive'}
|
||||
</StatNumber>
|
||||
{status?.disk_space_info && (
|
||||
<StatHelpText>
|
||||
💽 {status.disk_space_info.free_space} free<br/>
|
||||
⏱️ ~{status.disk_space_info.recording_time_left}
|
||||
</StatHelpText>
|
||||
)}
|
||||
</Stat>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</SimpleGrid>
|
||||
)
|
||||
}
|
||||
|
||||
// Pure RJSF Configuration Panel
|
||||
function ConfigurationPanel({ schemas, currentSchemaId, onSchemaChange, schema, formData, onFormChange, onSave, saving, message }) {
|
||||
const cardBg = useColorModeValue('white', 'gray.700')
|
||||
const borderColor = useColorModeValue('gray.200', 'gray.600')
|
||||
|
||||
if (!schema || !formData) {
|
||||
return (
|
||||
<Card bg={cardBg} borderColor={borderColor}>
|
||||
<CardBody>
|
||||
<Text>Loading configuration...</Text>
|
||||
</CardBody>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card bg={cardBg} borderColor={borderColor}>
|
||||
<CardHeader>
|
||||
<Flex align="center">
|
||||
<Box>
|
||||
<Heading size="md">🔧 Configuration Editor</Heading>
|
||||
<Text fontSize="sm" color="gray.500" mt={1}>
|
||||
Pure RJSF configuration management
|
||||
</Text>
|
||||
</Box>
|
||||
<Spacer />
|
||||
<Select
|
||||
value={currentSchemaId}
|
||||
onChange={(e) => onSchemaChange(e.target.value)}
|
||||
width="200px"
|
||||
size="sm"
|
||||
>
|
||||
{schemas?.map(schemaInfo => (
|
||||
<option key={schemaInfo.id} value={schemaInfo.id}>
|
||||
{schemaInfo.title || schemaInfo.id}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</Flex>
|
||||
{message && (
|
||||
<Alert status="success" mt={2}>
|
||||
<AlertIcon />
|
||||
{message}
|
||||
</Alert>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Form
|
||||
schema={schema}
|
||||
formData={formData}
|
||||
validator={validator}
|
||||
onChange={({ formData }) => onFormChange(formData)}
|
||||
onSubmit={({ formData }) => onSave(formData)}
|
||||
>
|
||||
<HStack spacing={2} mt={4}>
|
||||
<Button
|
||||
type="submit"
|
||||
colorScheme="blue"
|
||||
isLoading={saving}
|
||||
loadingText="Saving..."
|
||||
>
|
||||
💾 Save Configuration
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => window.location.reload()}>
|
||||
🔄 Reset
|
||||
</Button>
|
||||
</HStack>
|
||||
</Form>
|
||||
</CardBody>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
// Pure RJSF Dataset Manager
|
||||
function DatasetManager() {
|
||||
const [datasetsConfig, setDatasetsConfig] = useState(null)
|
||||
const [variablesConfig, setVariablesConfig] = useState(null)
|
||||
const [datasetsSchema, setDatasetsSchema] = useState(null)
|
||||
const [variablesSchema, setVariablesSchema] = useState(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const toast = useToast()
|
||||
|
||||
const loadDatasetData = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const [datasetsData, variablesData, datasetsSchemaData, variablesSchemaData] = await Promise.all([
|
||||
api.readConfig('dataset-definitions'),
|
||||
api.readConfig('dataset-variables'),
|
||||
api.getSchema('dataset-definitions'),
|
||||
api.getSchema('dataset-variables')
|
||||
])
|
||||
|
||||
setDatasetsConfig(datasetsData)
|
||||
setVariablesConfig(variablesData)
|
||||
setDatasetsSchema(datasetsSchemaData)
|
||||
setVariablesSchema(variablesSchemaData)
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: '❌ Failed to load dataset data',
|
||||
description: error.message,
|
||||
status: 'error',
|
||||
duration: 3000
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const saveDatasets = async (formData) => {
|
||||
try {
|
||||
await api.writeConfig('dataset-definitions', formData)
|
||||
toast({
|
||||
title: '✅ Dataset definitions saved',
|
||||
status: 'success',
|
||||
duration: 2000
|
||||
})
|
||||
setDatasetsConfig(formData)
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: '❌ Failed to save datasets',
|
||||
description: error.message,
|
||||
status: 'error',
|
||||
duration: 3000
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const saveVariables = async (formData) => {
|
||||
try {
|
||||
await api.writeConfig('dataset-variables', formData)
|
||||
toast({
|
||||
title: '✅ Dataset variables saved',
|
||||
status: 'success',
|
||||
duration: 2000
|
||||
})
|
||||
setVariablesConfig(formData)
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: '❌ Failed to save variables',
|
||||
description: error.message,
|
||||
status: 'error',
|
||||
duration: 3000
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadDatasetData()
|
||||
}, [])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Card>
|
||||
<CardBody>
|
||||
<Flex align="center" justify="center" py={8}>
|
||||
<Spinner mr={3} />
|
||||
<Text>Loading dataset configurations...</Text>
|
||||
</Flex>
|
||||
</CardBody>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<VStack spacing={4} align="stretch">
|
||||
<Flex align="center">
|
||||
<Heading size="lg">📊 Dataset Manager</Heading>
|
||||
<Spacer />
|
||||
<Button size="sm" variant="outline" onClick={loadDatasetData}>
|
||||
🔄 Refresh
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
<Tabs variant="enclosed" colorScheme="blue">
|
||||
<TabList>
|
||||
<Tab>📋 Dataset Definitions</Tab>
|
||||
<Tab>⚙️ Dataset Variables</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabPanels>
|
||||
<TabPanel p={0} pt={4}>
|
||||
{datasetsSchema && datasetsConfig && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Heading size="md">Dataset Metadata Configuration</Heading>
|
||||
<Text fontSize="sm" color="gray.500" mt={1}>
|
||||
Configure dataset names, prefixes, sampling intervals and enable/disable datasets
|
||||
</Text>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Form
|
||||
schema={datasetsSchema}
|
||||
formData={datasetsConfig}
|
||||
validator={validator}
|
||||
onSubmit={({ formData }) => saveDatasets(formData)}
|
||||
onChange={({ formData }) => setDatasetsConfig(formData)}
|
||||
>
|
||||
<HStack spacing={2} mt={4}>
|
||||
<Button type="submit" colorScheme="blue">
|
||||
💾 Save Definitions
|
||||
</Button>
|
||||
<Button variant="outline" onClick={loadDatasetData}>
|
||||
🔄 Reset
|
||||
</Button>
|
||||
</HStack>
|
||||
</Form>
|
||||
</CardBody>
|
||||
</Card>
|
||||
)}
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel p={0} pt={4}>
|
||||
{variablesSchema && variablesConfig && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Heading size="md">Dataset Variables Configuration</Heading>
|
||||
<Text fontSize="sm" color="gray.500" mt={1}>
|
||||
Raw JSON configuration for variables assigned to each dataset
|
||||
</Text>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Form
|
||||
schema={variablesSchema}
|
||||
formData={variablesConfig}
|
||||
validator={validator}
|
||||
onSubmit={({ formData }) => saveVariables(formData)}
|
||||
onChange={({ formData }) => setVariablesConfig(formData)}
|
||||
>
|
||||
<HStack spacing={2} mt={4}>
|
||||
<Button type="submit" colorScheme="blue">
|
||||
💾 Save Variables
|
||||
</Button>
|
||||
<Button variant="outline" onClick={loadDatasetData}>
|
||||
🔄 Reset
|
||||
</Button>
|
||||
</HStack>
|
||||
</Form>
|
||||
</CardBody>
|
||||
</Card>
|
||||
)}
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
||||
// Events Display Component
|
||||
function EventsDisplay({ events, loading, onRefresh }) {
|
||||
const cardBg = useColorModeValue('white', 'gray.700')
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Card bg={cardBg}>
|
||||
<CardBody>
|
||||
<Flex align="center" justify="center" py={4}>
|
||||
<Spinner mr={3} />
|
||||
<Text>Loading events...</Text>
|
||||
</Flex>
|
||||
</CardBody>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card bg={cardBg}>
|
||||
<CardHeader>
|
||||
<Flex align="center">
|
||||
<Heading size="md">📋 Recent Events</Heading>
|
||||
<Spacer />
|
||||
<Button size="sm" variant="outline" onClick={onRefresh}>
|
||||
🔄 Refresh
|
||||
</Button>
|
||||
</Flex>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<TableContainer>
|
||||
<Table size="sm">
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>Time</Th>
|
||||
<Th>Type</Th>
|
||||
<Th>Message</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{events?.map((event, index) => (
|
||||
<Tr key={index}>
|
||||
<Td>
|
||||
<Code fontSize="xs">
|
||||
{new Date(event.timestamp).toLocaleString()}
|
||||
</Code>
|
||||
</Td>
|
||||
<Td>
|
||||
<Badge
|
||||
colorScheme={
|
||||
event.level === 'ERROR' ? 'red' :
|
||||
event.level === 'WARNING' ? 'orange' :
|
||||
event.level === 'INFO' ? 'blue' : 'gray'
|
||||
}
|
||||
>
|
||||
{event.level}
|
||||
</Badge>
|
||||
</Td>
|
||||
<Td>{event.message}</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
{(!events || events.length === 0) && (
|
||||
<Text textAlign="center" py={4} color="gray.500">
|
||||
No events found
|
||||
</Text>
|
||||
)}
|
||||
</CardBody>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
// Main Dashboard Component with Pure RJSF
|
||||
export default function Dashboard() {
|
||||
const [status, setStatus] = useState(null)
|
||||
const [statusLoading, setStatusLoading] = useState(true)
|
||||
const [statusError, setStatusError] = useState('')
|
||||
|
||||
const [schemas, setSchemas] = useState([])
|
||||
const [currentSchemaId, setCurrentSchemaId] = useState('plc')
|
||||
const [schema, setSchema] = useState(null)
|
||||
const [formData, setFormData] = useState(null)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [message, setMessage] = useState('')
|
||||
|
||||
const [events, setEvents] = useState([])
|
||||
const [eventsLoading, setEventsLoading] = useState(false)
|
||||
|
||||
const sseRef = useRef(null)
|
||||
|
||||
// Load status once
|
||||
const loadStatus = useCallback(async () => {
|
||||
try {
|
||||
setStatusLoading(true)
|
||||
setStatusError('')
|
||||
const statusData = await api.getStatus()
|
||||
setStatus(statusData)
|
||||
} catch (error) {
|
||||
setStatusError(error.message)
|
||||
} finally {
|
||||
setStatusLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// SSE subscription for real-time updates (temporarily disabled due to MIME type issue)
|
||||
const subscribeSSE = useCallback(() => {
|
||||
// TODO: Fix SSE endpoint to return proper text/event-stream MIME type
|
||||
// if (sseRef.current) {
|
||||
// sseRef.current.close()
|
||||
// }
|
||||
|
||||
// const eventSource = new EventSource('/api/status')
|
||||
// sseRef.current = eventSource
|
||||
|
||||
// eventSource.onmessage = (event) => {
|
||||
// try {
|
||||
// const data = JSON.parse(event.data)
|
||||
// setStatus(data)
|
||||
// setStatusError('')
|
||||
// } catch (error) {
|
||||
// console.error('SSE parse error:', error)
|
||||
// }
|
||||
// }
|
||||
|
||||
// eventSource.onerror = () => {
|
||||
// console.warn('SSE connection error, will retry...')
|
||||
// }
|
||||
|
||||
// return () => {
|
||||
// eventSource.close()
|
||||
// }
|
||||
|
||||
// For now, use polling instead
|
||||
const interval = setInterval(async () => {
|
||||
try {
|
||||
const statusData = await api.getStatus()
|
||||
setStatus(statusData)
|
||||
setStatusError('')
|
||||
} catch (error) {
|
||||
console.error('Status polling error:', error)
|
||||
}
|
||||
}, 5000) // Poll every 5 seconds
|
||||
|
||||
return () => {
|
||||
clearInterval(interval)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Load schemas
|
||||
const loadSchemas = useCallback(async () => {
|
||||
try {
|
||||
const schemasData = await api.listSchemas()
|
||||
setSchemas(schemasData.schemas || [])
|
||||
} catch (error) {
|
||||
console.error('Failed to load schemas:', error)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Load specific config
|
||||
const loadConfig = useCallback(async (schemaId) => {
|
||||
try {
|
||||
const [schemaData, configData] = await Promise.all([
|
||||
api.getSchema(schemaId),
|
||||
api.readConfig(schemaId)
|
||||
])
|
||||
setSchema(schemaData)
|
||||
setFormData(configData)
|
||||
setMessage('')
|
||||
} catch (error) {
|
||||
console.error(`Failed to load config ${schemaId}:`, error)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Save config
|
||||
const saveConfig = useCallback(async (data) => {
|
||||
try {
|
||||
setSaving(true)
|
||||
await api.writeConfig(currentSchemaId, data)
|
||||
setMessage(`✅ Configuration saved successfully`)
|
||||
setTimeout(() => setMessage(''), 3000)
|
||||
setFormData(data)
|
||||
} catch (error) {
|
||||
setMessage(`❌ Failed to save: ${error.message}`)
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}, [currentSchemaId])
|
||||
|
||||
// Load events
|
||||
const loadEvents = useCallback(async () => {
|
||||
try {
|
||||
setEventsLoading(true)
|
||||
const eventsData = await api.getEvents(50)
|
||||
setEvents(eventsData.events || [])
|
||||
} catch (error) {
|
||||
console.error('Failed to load events:', error)
|
||||
} finally {
|
||||
setEventsLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Effects
|
||||
useEffect(() => {
|
||||
loadStatus()
|
||||
loadSchemas()
|
||||
loadEvents()
|
||||
|
||||
const cleanup = subscribeSSE()
|
||||
return cleanup
|
||||
}, [loadStatus, loadSchemas, loadEvents, subscribeSSE])
|
||||
|
||||
useEffect(() => {
|
||||
if (currentSchemaId) {
|
||||
loadConfig(currentSchemaId)
|
||||
}
|
||||
}, [currentSchemaId, loadConfig])
|
||||
|
||||
if (statusLoading) {
|
||||
return (
|
||||
<Container maxW="container.xl" py={6}>
|
||||
<Flex align="center" justify="center" minH="200px">
|
||||
<Spinner size="xl" mr={4} />
|
||||
<Text fontSize="lg">Loading dashboard...</Text>
|
||||
</Flex>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Container maxW="container.xl" py={6}>
|
||||
<VStack spacing={6} align="stretch">
|
||||
<Flex align="center" mb={4}>
|
||||
<Heading size="xl">🏭 PLC S7-31x Streamer & Logger</Heading>
|
||||
<Spacer />
|
||||
<Button size="sm" variant="outline" onClick={loadStatus}>
|
||||
🔄 Refresh Status
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
{statusError && (
|
||||
<Alert status="error">
|
||||
<AlertIcon />
|
||||
Failed to load status: {statusError}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<StatusBar status={status} onRefresh={loadStatus} />
|
||||
|
||||
<Tabs variant="enclosed" colorScheme="blue">
|
||||
<TabList>
|
||||
<Tab>🔧 Configuration</Tab>
|
||||
<Tab>📊 Datasets</Tab>
|
||||
<Tab>📈 Plotting</Tab>
|
||||
<Tab>📋 Events</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabPanels>
|
||||
<TabPanel p={0} pt={4}>
|
||||
<ConfigurationPanel
|
||||
schemas={schemas}
|
||||
currentSchemaId={currentSchemaId}
|
||||
onSchemaChange={setCurrentSchemaId}
|
||||
schema={schema}
|
||||
formData={formData}
|
||||
onFormChange={setFormData}
|
||||
onSave={saveConfig}
|
||||
saving={saving}
|
||||
message={message}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel p={0} pt={4}>
|
||||
<DatasetManager />
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel p={0} pt={4}>
|
||||
<PlotManager />
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel p={0} pt={4}>
|
||||
<EventsDisplay
|
||||
events={events}
|
||||
loading={eventsLoading}
|
||||
onRefresh={loadEvents}
|
||||
/>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</VStack>
|
||||
</Container>
|
||||
)
|
||||
}
|
|
@ -60,17 +60,24 @@ export async function stopUdpStreaming() {
|
|||
// Config schemas and data
|
||||
export async function listSchemas() {
|
||||
const res = await fetch(`${BASE_URL}/api/config/schemas`, { headers: { 'Accept': 'application/json' } })
|
||||
return toJsonOrThrow(res)
|
||||
const response = await toJsonOrThrow(res)
|
||||
// The API returns { success: true, schemas: [...] }
|
||||
return response
|
||||
}
|
||||
|
||||
export async function getSchema(schemaId) {
|
||||
const res = await fetch(`${BASE_URL}/api/config/schema/${encodeURIComponent(schemaId)}`, { headers: { 'Accept': 'application/json' } })
|
||||
return toJsonOrThrow(res)
|
||||
const response = await toJsonOrThrow(res)
|
||||
// The API returns { success: true, schema: {...}, ui_schema?: {...} }
|
||||
// We need to extract just the schema part for RJSF
|
||||
return response.schema || response
|
||||
}
|
||||
|
||||
export async function readConfig(configId) {
|
||||
const res = await fetch(`${BASE_URL}/api/config/${encodeURIComponent(configId)}`, { headers: { 'Accept': 'application/json' } })
|
||||
return toJsonOrThrow(res)
|
||||
const response = await toJsonOrThrow(res)
|
||||
// The API might return { success: true, data: {...} } or just the data
|
||||
return response.data || response
|
||||
}
|
||||
|
||||
export async function writeConfig(configId, data) {
|
||||
|
@ -123,4 +130,64 @@ export async function deactivateDataset(datasetId) {
|
|||
return toJsonOrThrow(res)
|
||||
}
|
||||
|
||||
// Plot management
|
||||
export async function getPlots() {
|
||||
const res = await fetch(`${BASE_URL}/api/plots`, { headers: { 'Accept': 'application/json' } })
|
||||
return toJsonOrThrow(res)
|
||||
}
|
||||
|
||||
export async function createPlot(plotConfig) {
|
||||
const res = await fetch(`${BASE_URL}/api/plots`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
||||
body: JSON.stringify(plotConfig),
|
||||
})
|
||||
return toJsonOrThrow(res)
|
||||
}
|
||||
|
||||
export async function deletePlot(sessionId) {
|
||||
const res = await fetch(`${BASE_URL}/api/plots/${encodeURIComponent(sessionId)}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Accept': 'application/json' },
|
||||
})
|
||||
return toJsonOrThrow(res)
|
||||
}
|
||||
|
||||
export async function controlPlot(sessionId, action) {
|
||||
const res = await fetch(`${BASE_URL}/api/plots/${encodeURIComponent(sessionId)}/control`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
||||
body: JSON.stringify({ action }),
|
||||
})
|
||||
return toJsonOrThrow(res)
|
||||
}
|
||||
|
||||
export async function getPlotData(sessionId) {
|
||||
const res = await fetch(`${BASE_URL}/api/plots/${encodeURIComponent(sessionId)}/data`, {
|
||||
headers: { 'Accept': 'application/json' }
|
||||
})
|
||||
return toJsonOrThrow(res)
|
||||
}
|
||||
|
||||
export async function getPlotConfig(sessionId) {
|
||||
const res = await fetch(`${BASE_URL}/api/plots/${encodeURIComponent(sessionId)}/config`, {
|
||||
headers: { 'Accept': 'application/json' }
|
||||
})
|
||||
return toJsonOrThrow(res)
|
||||
}
|
||||
|
||||
export async function updatePlotConfig(sessionId, config) {
|
||||
const res = await fetch(`${BASE_URL}/api/plots/${encodeURIComponent(sessionId)}/config`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
||||
body: JSON.stringify(config),
|
||||
})
|
||||
return toJsonOrThrow(res)
|
||||
}
|
||||
|
||||
export async function getPlotVariables() {
|
||||
const res = await fetch(`${BASE_URL}/api/plots/variables`, { headers: { 'Accept': 'application/json' } })
|
||||
return toJsonOrThrow(res)
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
# ==============================
|
||||
# LEGACY ROUTES TO REMOVE/COMMENT
|
||||
# ==============================
|
||||
|
||||
# Legacy templates route (replaced by React SPA)
|
||||
# @app.route("/legacy")
|
||||
# def serve_legacy_index():
|
||||
# """Serve legacy HTML template for backward compatibility."""
|
||||
# try:
|
||||
# return render_template("index.html")
|
||||
# except Exception as e:
|
||||
# return f"Error loading legacy template: {str(e)}", 500
|
||||
|
||||
# These routes can be removed after full migration to React:
|
||||
# All routes serving /templates/index.html
|
||||
# Static file serving for legacy JS/CSS
|
||||
# Any jQuery-based endpoints
|
||||
|
||||
# Essential APIs to keep:
|
||||
# - /api/status (SSE)
|
||||
# - /api/health
|
||||
# - /api/events
|
||||
# - /api/config/* (schemas and CRUD)
|
||||
# - /api/plc/connect, /api/plc/disconnect
|
||||
# - /api/udp/streaming/*
|
||||
# - /api/plots/* (for chart functionality)
|
||||
# - /api/datasets/* (if still needed)
|
||||
# - /api/variables/* (if still needed)
|
||||
|
||||
# React SPA routes to keep:
|
||||
# - / (React app)
|
||||
# - /app (React app)
|
||||
# - /app/<path:path> (React app routing)
|
||||
# - /assets/* (Vite build assets)
|
Loading…
Reference in New Issue