Enhance variable management and plotting features. Introduced VariableContext for managing variable updates, updated dataset variables and application events, improved plot variable schema, and refined UI components for better usability and performance.

This commit is contained in:
Miguel 2025-08-14 12:07:07 +02:00
parent 09263d39f8
commit d0d675d804
10 changed files with 386 additions and 224 deletions

View File

@ -1,165 +1,5 @@
{
"events": [
{
"timestamp": "2025-07-17T18:00:01.179115",
"level": "info",
"event_type": "dataset_activated",
"message": "Dataset activated: DAR",
"details": {
"dataset_id": "dar",
"variables_count": 4,
"streaming_count": 4,
"prefix": "dar"
}
},
{
"timestamp": "2025-07-17T18:01:12.766609",
"level": "info",
"event_type": "dataset_activated",
"message": "Dataset activated: DAR",
"details": {
"dataset_id": "dar",
"variables_count": 4,
"streaming_count": 4,
"prefix": "dar"
}
},
{
"timestamp": "2025-07-17T18:01:12.769824",
"level": "info",
"event_type": "streaming_started",
"message": "Multi-dataset streaming started: 1 datasets activated",
"details": {
"activated_datasets": 1,
"total_datasets": 2,
"udp_host": "127.0.0.1",
"udp_port": 9870
}
},
{
"timestamp": "2025-07-17T18:01:26.427183",
"level": "info",
"event_type": "Application started",
"message": "Application initialization completed successfully",
"details": {}
},
{
"timestamp": "2025-07-17T18:01:26.451766",
"level": "info",
"event_type": "plc_connection",
"message": "Successfully connected to PLC 10.1.33.11",
"details": {
"ip": "10.1.33.11",
"rack": 0,
"slot": 2
}
},
{
"timestamp": "2025-07-17T18:01:26.456954",
"level": "info",
"event_type": "dataset_activated",
"message": "Dataset activated: DAR",
"details": {
"dataset_id": "dar",
"variables_count": 4,
"streaming_count": 4,
"prefix": "dar"
}
},
{
"timestamp": "2025-07-17T18:01:30.909879",
"level": "info",
"event_type": "dataset_activated",
"message": "Dataset activated: DAR",
"details": {
"dataset_id": "dar",
"variables_count": 4,
"streaming_count": 4,
"prefix": "dar"
}
},
{
"timestamp": "2025-07-17T18:01:30.911944",
"level": "info",
"event_type": "streaming_started",
"message": "Multi-dataset streaming started: 1 datasets activated",
"details": {
"activated_datasets": 1,
"total_datasets": 2,
"udp_host": "127.0.0.1",
"udp_port": 9870
}
},
{
"timestamp": "2025-07-17T18:20:09.887378",
"level": "info",
"event_type": "Application started",
"message": "Application initialization completed successfully",
"details": {}
},
{
"timestamp": "2025-07-17T18:20:09.913286",
"level": "info",
"event_type": "plc_connection",
"message": "Successfully connected to PLC 10.1.33.11",
"details": {
"ip": "10.1.33.11",
"rack": 0,
"slot": 2
}
},
{
"timestamp": "2025-07-17T18:20:09.917270",
"level": "info",
"event_type": "dataset_activated",
"message": "Dataset activated: DAR",
"details": {
"dataset_id": "dar",
"variables_count": 4,
"streaming_count": 4,
"prefix": "dar"
}
},
{
"timestamp": "2025-07-18T09:39:20.220724",
"level": "info",
"event_type": "dataset_activated",
"message": "Dataset activated: DAR",
"details": {
"dataset_id": "dar",
"variables_count": 4,
"streaming_count": 3,
"prefix": "dar"
}
},
{
"timestamp": "2025-07-18T09:39:20.226708",
"level": "info",
"event_type": "streaming_started",
"message": "Multi-dataset streaming started: 1 datasets activated",
"details": {
"activated_datasets": 1,
"total_datasets": 2,
"udp_host": "127.0.0.1",
"udp_port": 9870
}
},
{
"timestamp": "2025-07-18T09:40:58.642342",
"level": "info",
"event_type": "dataset_deactivated",
"message": "Dataset deactivated: DAR",
"details": {
"dataset_id": "dar"
}
},
{
"timestamp": "2025-07-18T09:40:58.644338",
"level": "info",
"event_type": "streaming_stopped",
"message": "Multi-dataset streaming stopped: 1 datasets deactivated",
"details": {}
},
{
"timestamp": "2025-07-18T09:40:58.647328",
"level": "info",
@ -10414,8 +10254,155 @@
"trigger_variable": null,
"auto_started": true
}
},
{
"timestamp": "2025-08-14T11:42:26.264053",
"level": "info",
"event_type": "application_started",
"message": "Application initialization completed successfully",
"details": {}
},
{
"timestamp": "2025-08-14T11:42:26.345728",
"level": "info",
"event_type": "dataset_activated",
"message": "Dataset activated: DAR",
"details": {
"dataset_id": "DAR",
"variables_count": 3,
"streaming_count": 3,
"prefix": "gateway_phoenix"
}
},
{
"timestamp": "2025-08-14T11:42:26.362513",
"level": "info",
"event_type": "csv_recording_started",
"message": "CSV recording started: 1 datasets activated",
"details": {
"activated_datasets": 1,
"total_datasets": 3
}
},
{
"timestamp": "2025-08-14T11:42:26.424426",
"level": "error",
"event_type": "csv_cleanup_failed",
"message": "CSV cleanup failed: 'max_hours'",
"details": {}
},
{
"timestamp": "2025-08-14T11:49:00.624419",
"level": "info",
"event_type": "application_started",
"message": "Application initialization completed successfully",
"details": {}
},
{
"timestamp": "2025-08-14T11:49:00.691022",
"level": "info",
"event_type": "dataset_activated",
"message": "Dataset activated: DAR",
"details": {
"dataset_id": "DAR",
"variables_count": 3,
"streaming_count": 3,
"prefix": "gateway_phoenix"
}
},
{
"timestamp": "2025-08-14T11:49:00.703362",
"level": "info",
"event_type": "csv_recording_started",
"message": "CSV recording started: 1 datasets activated",
"details": {
"activated_datasets": 1,
"total_datasets": 3
}
},
{
"timestamp": "2025-08-14T11:49:00.738130",
"level": "error",
"event_type": "csv_cleanup_failed",
"message": "CSV cleanup failed: 'max_hours'",
"details": {}
},
{
"timestamp": "2025-08-14T12:00:00.237052",
"level": "error",
"event_type": "csv_cleanup_failed",
"message": "CSV cleanup failed: 'max_hours'",
"details": {}
},
{
"timestamp": "2025-08-14T12:03:29.892080",
"level": "info",
"event_type": "plot_session_created",
"message": "Plot session 'UR29' created and started",
"details": {
"session_id": "plot_1",
"variables": [
"UR29_Brix",
"UR29_ma"
],
"time_window": 25,
"trigger_variable": null,
"auto_started": true
}
},
{
"timestamp": "2025-08-14T12:05:58.231793",
"level": "info",
"event_type": "application_started",
"message": "Application initialization completed successfully",
"details": {}
},
{
"timestamp": "2025-08-14T12:05:58.282781",
"level": "info",
"event_type": "dataset_activated",
"message": "Dataset activated: DAR",
"details": {
"dataset_id": "DAR",
"variables_count": 2,
"streaming_count": 2,
"prefix": "gateway_phoenix"
}
},
{
"timestamp": "2025-08-14T12:05:58.294856",
"level": "info",
"event_type": "csv_recording_started",
"message": "CSV recording started: 1 datasets activated",
"details": {
"activated_datasets": 1,
"total_datasets": 3
}
},
{
"timestamp": "2025-08-14T12:05:58.314287",
"level": "error",
"event_type": "csv_cleanup_failed",
"message": "CSV cleanup failed: 'max_hours'",
"details": {}
},
{
"timestamp": "2025-08-14T12:06:22.595559",
"level": "info",
"event_type": "plot_session_created",
"message": "Plot session 'UR29' created and started",
"details": {
"session_id": "plot_1",
"variables": [
"UR29_Brix",
"UR29_ma"
],
"time_window": 25,
"trigger_variable": null,
"auto_started": true
}
}
],
"last_updated": "2025-08-14T11:34:45.734619",
"last_updated": "2025-08-14T12:06:22.595559",
"total_entries": 1000
}

View File

@ -4,28 +4,20 @@
"dataset_id": "DAR",
"variables": [
{
"area": "db",
"db": 1011,
"name": "UR29_Brix",
"offset": 1322,
"streaming": true,
"type": "real"
},
{
"area": "db",
"db": 1011,
"offset": 1322,
"type": "real",
"streaming": true
},
{
"name": "UR29_ma",
"offset": 1296,
"streaming": true,
"type": "real"
},
{
"area": "db",
"db": 1011,
"name": "fUR29_Brix",
"offset": 1322,
"streaming": true,
"type": "real"
"offset": 1296,
"type": "real",
"streaming": true
}
]
},

View File

@ -28,7 +28,12 @@
"variable_name": {
"type": "string",
"title": "Variable Name",
"description": "Name of the variable to plot (must exist in dataset variables)"
"description": "Name of the variable to plot (selected from dataset variables using search widget)"
},
"label": {
"type": "string",
"title": "Display Label",
"description": "Label shown in the plot legend"
},
"color": {
"type": "string",
@ -37,6 +42,21 @@
"pattern": "^#[0-9A-Fa-f]{6}$",
"default": "#3498db"
},
"line_width": {
"type": "number",
"title": "Line Width",
"description": "Width of the line in the plot",
"default": 2,
"minimum": 1,
"maximum": 10
},
"y_axis": {
"type": "string",
"title": "Y-Axis",
"description": "Which Y-axis to use for this variable",
"enum": ["left", "right"],
"default": "left"
},
"enabled": {
"type": "boolean",
"title": "Enable Plotting",
@ -46,7 +66,7 @@
},
"required": [
"variable_name",
"color"
"label"
]
}
}

View File

@ -27,29 +27,49 @@
"items": {
"ui:order": [
"variable_name",
"label",
"color",
"line_width",
"y_axis",
"enabled"
],
"ui:layout": [
[
{
"name": "variable_name",
"width": 6
"width": 4
},
{
"name": "label",
"width": 2
},
{
"name": "color",
"width": 3
"width": 2
},
{
"name": "line_width",
"width": 2
},
{
"name": "y_axis",
"width": 1
},
{
"name": "enabled",
"width": 3
"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": "UR29_Brix",
"ui:help": "<22> Name of the variable to plot (must exist in dataset variables)"
"ui:placeholder": "Chart legend label...",
"ui:help": "📊 Label shown in the plot legend for this variable"
},
"color": {
"ui:widget": "color",
@ -72,6 +92,14 @@
]
}
},
"line_width": {
"ui:widget": "updown",
"ui:help": "📏 Width of the line in the plot (1-10 pixels)"
},
"y_axis": {
"ui:widget": "select",
"ui:help": "📊 Which Y-axis to use for this variable (left or right)"
},
"enabled": {
"ui:widget": "checkbox",
"ui:help": "📊 Enable this variable to be displayed in the real-time plot"

View File

@ -36,10 +36,12 @@ 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'
// Pure RJSF Plot Manager Component
export default function PlotManager() {
const { triggerVariableRefresh } = useVariableContext()
const [plots, setPlots] = useState({})
const [plotsSchemaData, setPlotsSchemaData] = useState(null)
const [plotsVariablesSchemaData, setPlotsVariablesSchemaData] = useState(null)
@ -238,6 +240,8 @@ export default function PlotManager() {
duration: 2000
})
setPlotsVariablesConfig(formData)
// Trigger refresh of variable selectors (though they don't depend on plot vars directly)
triggerVariableRefresh()
} catch (error) {
toast({
title: '❌ Failed to save plot variables',
@ -411,15 +415,10 @@ export default function PlotManager() {
type: "object",
title: "Plot Variable",
properties: {
dataset_id: {
type: "string",
title: "Dataset Source",
description: "Which dataset contains this variable"
},
variable_name: {
type: "string",
title: "Variable Name",
description: "Name of the variable to plot"
description: "Select variable from datasets with search and metadata"
},
label: {
type: "string",
@ -445,7 +444,7 @@ export default function PlotManager() {
default: "left"
}
},
required: ["dataset_id", "variable_name", "label"]
required: ["variable_name", "label"]
}
}
}
@ -455,13 +454,26 @@ export default function PlotManager() {
variables: {
items: {
"ui:layout": [[
{ "name": "dataset_id", "width": 2 },
{ "name": "variable_name", "width": 3 },
{ "name": "variable_name", "width": 4 },
{ "name": "label", "width": 2 },
{ "name": "color", "width": 2 },
{ "name": "line_width", "width": 1 },
{ "name": "line_width", "width": 2 },
{ "name": "y_axis", "width": 2 }
]]
]],
variable_name: {
"ui:widget": "variableSelector",
"ui:placeholder": "Search and select variable from datasets...",
"ui:help": "🔍 Search variables from configured datasets with live values and metadata"
},
label: {
"ui:placeholder": "Chart legend label..."
},
color: {
"ui:widget": "color"
},
line_width: {
"ui:widget": "updown"
}
}
}
}

View File

@ -1,14 +1,27 @@
import React, { useState, useEffect, useMemo, useRef } from 'react'
import React, { useState, useEffect, useMemo, useRef, useCallback } from 'react'
import {
FormControl, FormLabel, FormHelperText, Select, VStack, HStack,
Text, Badge, Box, Icon, Input, useColorModeValue, Spinner
Text, Badge, Box, Icon, Input, useColorModeValue, Spinner, IconButton, Tooltip
} from '@chakra-ui/react'
import { SearchIcon } from '@chakra-ui/icons'
import { SearchIcon, RepeatIcon } from '@chakra-ui/icons'
import { readConfig } from '../../services/api.js'
import { useVariableContext } from '../../contexts/VariableContext.jsx'
// Widget for selecting existing dataset variables with filtering and search
export function VariableSelectorWidget(props) {
const { id, value, required, disabled, readonly, label, onChange, onBlur, onFocus, rawErrors = [] } = props
const { id, value, required, disabled, readonly, label, onChange, onBlur, onFocus, rawErrors = [], options = {} } = props
// Extract refresh trigger from options if provided
const refreshTrigger = options?.refreshTrigger || 0
// Use variable context if available, fallback to local state
let contextTrigger = 0
try {
const variableContext = useVariableContext()
contextTrigger = variableContext?.variableRefreshTrigger || 0
} catch {
// Context not available, use local trigger only
}
const [datasetVariables, setDatasetVariables] = useState({})
const [loading, setLoading] = useState(true)
@ -16,6 +29,7 @@ export function VariableSelectorWidget(props) {
const [selectedDataset, setSelectedDataset] = useState('all')
const [liveValue, setLiveValue] = useState(undefined)
const [liveStatus, setLiveStatus] = useState('idle')
const [refreshing, setRefreshing] = useState(false)
const esRef = useRef(null)
const borderColor = useColorModeValue('gray.300', 'gray.600')
@ -24,24 +38,67 @@ export function VariableSelectorWidget(props) {
const selectedVarBgColor = useColorModeValue('blue.50', 'blue.900')
const selectedVarBorderColor = useColorModeValue('blue.200', 'blue.700')
// Load dataset variables on mount
useEffect(() => {
const loadDatasetVariables = async () => {
try {
setLoading(true)
const response = await readConfig('dataset-variables')
setDatasetVariables(response.data?.dataset_variables || {})
} catch (error) {
console.error('Error loading dataset variables:', error)
setDatasetVariables({})
} finally {
setLoading(false)
}
// Load dataset variables function (extracted for reuse)
const loadDatasetVariables = useCallback(async () => {
try {
setLoading(true)
const response = await readConfig('dataset-variables')
// Handle the array-based structure: { variables: [{dataset_id, variables: [...]}] }
const datasetVariablesArray = response?.variables || []
// Convert array format to object format for easier processing
const datasetVariablesObj = {}
datasetVariablesArray.forEach(item => {
if (item.dataset_id && item.variables) {
datasetVariablesObj[item.dataset_id] = {
variables: {}
}
// Convert variables array to object with variable name as key
item.variables.forEach(variable => {
if (variable.name) {
datasetVariablesObj[item.dataset_id].variables[variable.name] = variable
}
})
}
})
setDatasetVariables(datasetVariablesObj)
} catch (error) {
console.error('Error loading dataset variables:', error)
setDatasetVariables({})
} finally {
setLoading(false)
}
loadDatasetVariables()
}, [])
// Manual refresh function
const handleRefresh = useCallback(async () => {
setRefreshing(true)
await loadDatasetVariables()
setRefreshing(false)
}, [loadDatasetVariables])
// Load dataset variables on mount and when refresh trigger changes
useEffect(() => {
loadDatasetVariables()
}, [loadDatasetVariables, refreshTrigger, contextTrigger])
// Auto-refresh when component gains focus (optional behavior)
const handleFocusWithRefresh = useCallback((event) => {
// Call original onFocus if provided
if (onFocus) {
onFocus(id, event.target.value)
}
// Auto-refresh data on focus (debounced to avoid excessive calls)
const now = Date.now()
const lastRefresh = window._lastVariableRefresh || 0
if (now - lastRefresh > 30000) { // Refresh at most once every 30 seconds
window._lastVariableRefresh = now
handleRefresh()
}
}, [onFocus, id, handleRefresh])
// Create flattened list of all variables with their metadata
const allVariables = useMemo(() => {
const variables = []
@ -190,7 +247,7 @@ export function VariableSelectorWidget(props) {
{label && <FormLabel htmlFor={id}>{label}</FormLabel>}
<VStack spacing={3} align="stretch">
{/* Search and Dataset Filter */}
{/* Search and Dataset Filter with Refresh */}
<HStack spacing={2}>
<Box position="relative" flex="1">
<Input
@ -226,6 +283,19 @@ export function VariableSelectorWidget(props) {
</option>
))}
</Select>
<Tooltip label="Refresh variables list" placement="top">
<IconButton
icon={<RepeatIcon />}
onClick={handleRefresh}
isLoading={refreshing}
loadingText="Refreshing..."
size="md"
variant="outline"
colorScheme="blue"
aria-label="Refresh variables"
isDisabled={loading}
/>
</Tooltip>
</HStack>
{/* Variable Selection */}
@ -234,10 +304,11 @@ export function VariableSelectorWidget(props) {
value={value || ''}
onChange={(e) => onChange(e.target.value === '' ? undefined : e.target.value)}
onBlur={onBlur && ((e) => onBlur(id, e.target.value))}
onFocus={onFocus && ((e) => onFocus(id, e.target.value))}
onFocus={handleFocusWithRefresh}
borderColor={borderColor}
_focus={{ borderColor: focusBorderColor }}
bg={bgColor}
isDisabled={disabled || readonly}
>
<option value="">Select a variable...</option>
{filteredVariables.map((variable, index) => (

View File

@ -1,5 +1,6 @@
import { customWidgets } from './CustomWidgets'
import { widgets } from '../rjsf/widgets'
import VariableSelectorWidget from '../rjsf/VariableSelectorWidget'
// Comprehensive widget collection that merges all available widgets
// for full UI schema support with layouts
@ -17,9 +18,10 @@ export const allWidgets = {
select: widgets.SelectWidget,
checkbox: widgets.CheckboxWidget,
// Variable selector aliases
variableSelector: customWidgets.VariableSelectorWidget,
'variable-selector': customWidgets.VariableSelectorWidget,
// Variable selector aliases - use the advanced version with search and metadata
variableSelector: VariableSelectorWidget,
'variable-selector': VariableSelectorWidget,
VariableSelectorWidget: VariableSelectorWidget,
// PLC-specific widget aliases (if available)
plcArea: widgets.PlcAreaWidget,

View File

@ -0,0 +1,34 @@
import React, { createContext, useContext, useState, useCallback } from 'react'
// Context for managing variable data updates across components
const VariableContext = createContext()
export function VariableProvider({ children }) {
const [variableRefreshTrigger, setVariableRefreshTrigger] = useState(0)
// Function to trigger refresh of all variable selectors
const triggerVariableRefresh = useCallback(() => {
setVariableRefreshTrigger(prev => prev + 1)
}, [])
const value = {
variableRefreshTrigger,
triggerVariableRefresh
}
return (
<VariableContext.Provider value={value}>
{children}
</VariableContext.Provider>
)
}
export function useVariableContext() {
const context = useContext(VariableContext)
if (context === undefined) {
throw new Error('useVariableContext must be used within a VariableProvider')
}
return context
}
export default VariableContext

View File

@ -44,6 +44,7 @@ import validator from '@rjsf/validator-ajv8'
import PlotManager from '../components/PlotManager'
import allWidgets from '../components/widgets/AllWidgets'
import LayoutObjectFieldTemplate from '../components/rjsf/LayoutObjectFieldTemplate'
import { VariableProvider, useVariableContext } from '../contexts/VariableContext'
import * as api from '../services/api'
// StatusBar Component - Real-time PLC status with action buttons
@ -312,6 +313,7 @@ function ConfigurationPanel({ schemaData, formData, onFormChange, onSave, saving
// Dataset Manager - Type 3 Form Pattern implementation
function DatasetManager() {
const { triggerVariableRefresh } = useVariableContext()
const [datasetsConfig, setDatasetsConfig] = useState(null)
const [variablesConfig, setVariablesConfig] = useState(null)
const [datasetsSchemaData, setDatasetsSchemaData] = useState(null)
@ -379,6 +381,8 @@ function DatasetManager() {
duration: 2000
})
setVariablesConfig(formData)
// Trigger refresh of all variable selector widgets
triggerVariableRefresh()
} catch (error) {
toast({
title: '❌ Failed to save variables',
@ -599,7 +603,10 @@ function DatasetManager() {
templates={{ ObjectFieldTemplate: LayoutObjectFieldTemplate }}
onSubmit={({ formData }) => {
updateSelectedDatasetVariables(formData)
saveVariables(variablesConfig)
saveVariables(variablesConfig).then(() => {
// Additional trigger after successful save
triggerVariableRefresh()
})
}}
onChange={({ formData }) => updateSelectedDatasetVariables(formData)}
>
@ -707,8 +714,17 @@ function EventsDisplay({ events, loading, onRefresh }) {
)
}
// Main Dashboard Component - PLC S7-31x Streamer & Logger
// Main Dashboard Component - PLC S7-31x Streamer & Logger
export default function Dashboard() {
return (
<VariableProvider>
<DashboardContent />
</VariableProvider>
)
}
// Dashboard Content Component (separated to use context)
function DashboardContent() {
const [status, setStatus] = useState(null)
const [statusLoading, setStatusLoading] = useState(true)
const [statusError, setStatusError] = useState('')

View File

@ -3,11 +3,11 @@
"should_connect": true,
"should_stream": false,
"active_datasets": [
"Test",
"Fast",
"DAR",
"Test"
"DAR"
]
},
"auto_recovery_enabled": true,
"last_update": "2025-08-14T11:34:34.761474"
"last_update": "2025-08-14T12:05:58.306284"
}