From 2d7bfe247d5c434e3125b72c6aed76355495423e Mon Sep 17 00:00:00 2001 From: sebseb7 Date: Thu, 25 Dec 2025 00:24:48 +0100 Subject: [PATCH] u --- uiserver/src/App.js | 215 ++++++------- uiserver/src/components/Chart.js | 201 +++++++----- uiserver/src/components/Login.js | 101 +++--- uiserver/src/components/Settings.js | 103 +++--- uiserver/src/components/ViewDisplay.js | 98 +++--- uiserver/src/components/ViewManager.js | 418 ++++++++++++++----------- uiserver/src/components/withRouter.js | 18 ++ 7 files changed, 630 insertions(+), 524 deletions(-) create mode 100644 uiserver/src/components/withRouter.js diff --git a/uiserver/src/App.js b/uiserver/src/App.js index 5bb54e3..7ef1007 100644 --- a/uiserver/src/App.js +++ b/uiserver/src/App.js @@ -1,130 +1,133 @@ -import React, { useState, useEffect } from 'react'; -import { BrowserRouter, Routes, Route, Link, Navigate, useNavigate } from 'react-router-dom'; -import { ThemeProvider, CssBaseline, AppBar, Toolbar, Typography, Button, Box, IconButton, Menu, MenuItem } from '@mui/material'; +import React, { Component } from 'react'; +import { BrowserRouter, Routes, Route, Link, Navigate } from 'react-router-dom'; +import { AppBar, Toolbar, Typography, Button, Box, IconButton, CssBaseline } from '@mui/material'; +import { ThemeProvider, createTheme } from '@mui/material/styles'; import SettingsIcon from '@mui/icons-material/Settings'; import ShowChartIcon from '@mui/icons-material/ShowChart'; import DashboardIcon from '@mui/icons-material/Dashboard'; -import AccountCircle from '@mui/icons-material/AccountCircle'; -import theme from './theme'; + import Settings from './components/Settings'; import Chart from './components/Chart'; import Login from './components/Login'; import ViewManager from './components/ViewManager'; import ViewDisplay from './components/ViewDisplay'; -function NavBar({ user, onLogout }) { - const navigate = useNavigate(); - const [anchorEl, setAnchorEl] = useState(null); - - const handleMenu = (event) => setAnchorEl(event.currentTarget); - const handleClose = () => setAnchorEl(null); - const handleLogout = () => { - handleClose(); - onLogout(); - navigate('/'); - }; - - return ( - - - navigate('/')}> - TischlerCtrl - - - - - - - {user ? ( -
- - - - - {user.username} ({user.role}) - Logout - -
- ) : ( - - )} -
-
- ); -} - -export default function App() { - const [user, setUser] = useState(null); - const [selectedChannels, setSelectedChannels] = useState([]); - - // Load persistence (User + Settings) - useEffect(() => { - try { - const savedSettings = localStorage.getItem('selectedChannels'); - if (savedSettings) setSelectedChannels(JSON.parse(savedSettings)); - - const savedUser = localStorage.getItem('user'); - if (savedUser) setUser(JSON.parse(savedUser)); - } catch (e) { - console.error("Failed to load persistence", e); +const darkTheme = createTheme({ + palette: { + mode: 'dark', + primary: { main: '#fb4934' }, // Gruvbox red + secondary: { main: '#83a598' }, // Gruvbox blue + background: { + default: '#282828', // Gruvbox dark bg + paper: '#3c3836', // Gruvbox dark lighter + }, + text: { + primary: '#ebdbb2', + secondary: '#a89984' } - }, []); + }, +}); - const handleLogin = (userData) => { - setUser(userData); - localStorage.setItem('user', JSON.stringify(userData)); +export default class App extends Component { + constructor(props) { + super(props); + this.state = { + selectedChannels: [], + user: null, // { username, role, token } + loading: true + }; + } + + componentDidMount() { + // Load selection from local storage + const saved = localStorage.getItem('selectedChannels'); + if (saved) { + try { + this.setState({ selectedChannels: JSON.parse(saved) }); + } catch (e) { + console.error("Failed to parse saved channels"); + } + } + + // Check for existing token + const token = localStorage.getItem('authToken'); + const username = localStorage.getItem('authUser'); + const role = localStorage.getItem('authRole'); + + if (token && username) { + this.setState({ user: { username, role, token } }); + } + + this.setState({ loading: false }); + } + + handleSelectionChange = (newSelection) => { + this.setState({ selectedChannels: newSelection }); + localStorage.setItem('selectedChannels', JSON.stringify(newSelection)); }; - const handleLogout = () => { - setUser(null); - localStorage.removeItem('user'); + handleLogin = (userData) => { + this.setState({ user: userData }); + localStorage.setItem('authToken', userData.token); + localStorage.setItem('authUser', userData.username); + localStorage.setItem('authRole', userData.role); }; - const handleToggleChannel = (id) => { - setSelectedChannels(prev => { - const newSelection = prev.includes(id) - ? prev.filter(c => c !== id) - : [...prev, id]; - localStorage.setItem('selectedChannels', JSON.stringify(newSelection)); - return newSelection; - }); + handleLogout = () => { + this.setState({ user: null }); + localStorage.removeItem('authToken'); + localStorage.removeItem('authUser'); + localStorage.removeItem('authRole'); }; - return ( - - - - - + render() { + const { selectedChannels, user, loading } = this.state; + + // While checking auth, we could show loader, but it's sync here mostly. + + return ( + + + + + + + + TischlerCtrl + + + + + + + {user ? ( + + ) : ( + + )} + + - } /> - } /> - } /> - } /> - } /> + + } /> + + } /> + } /> + } /> - - - - ); + + + ); + } } diff --git a/uiserver/src/components/Chart.js b/uiserver/src/components/Chart.js index 5487a51..6bd8028 100644 --- a/uiserver/src/components/Chart.js +++ b/uiserver/src/components/Chart.js @@ -1,23 +1,50 @@ -import React, { useEffect, useState } from 'react'; +import React, { Component } from 'react'; import { Paper, Typography, Box, CircularProgress } from '@mui/material'; import { LineChart } from '@mui/x-charts/LineChart'; -import { useTheme } from '@mui/material/styles'; -export default function Chart({ selectedChannels = [], channelConfig = null, axisConfig = null }) { - const [data, setData] = useState([]); - const [loading, setLoading] = useState(true); - const theme = useTheme(); +export default class Chart extends Component { + constructor(props) { + super(props); + this.state = { + data: [], + loading: true + }; + this.intervalId = null; + } - // Determine effective channels list - const effectiveChannels = channelConfig - ? channelConfig.map(c => c.id) - : selectedChannels; + componentDidMount() { + this.fetchData(); + this.intervalId = setInterval(this.fetchData, 60000); + } + + componentDidUpdate(prevProps) { + // Compare props to see if we need to refetch + const prevEffective = this.getEffectiveChannels(prevProps); + const currEffective = this.getEffectiveChannels(this.props); + + if (prevEffective.join(',') !== currEffective.join(',')) { + this.fetchData(); + } + } + + componentWillUnmount() { + if (this.intervalId) { + clearInterval(this.intervalId); + } + } + + getEffectiveChannels(props) { + return props.channelConfig + ? props.channelConfig.map(c => c.id) + : props.selectedChannels; + } + + fetchData = () => { + const effectiveChannels = this.getEffectiveChannels(this.props); - const fetchData = () => { // Only fetch if selection exists if (effectiveChannels.length === 0) { - setData([]); - setLoading(false); + this.setState({ data: [], loading: false }); return; } @@ -47,48 +74,15 @@ export default function Chart({ selectedChannels = [], channelConfig = null, axi }); const sortedData = Array.from(timeMap.values()).sort((a, b) => a.time - b.time); - setData(sortedData); - setLoading(false); + this.setState({ data: sortedData, loading: false }); }) .catch(err => { console.error("Failed to fetch data", err); - setLoading(false); + this.setState({ loading: false }); }); }; - useEffect(() => { - fetchData(); - const interval = setInterval(fetchData, 60000); - return () => clearInterval(interval); - }, [selectedChannels, channelConfig]); - - if (loading) return ; - if (effectiveChannels.length === 0) return No channels selected.; - - const series = effectiveChannels.map(id => { - // Find alias and axis if config exists - let label = id; - let yAxisKey = 'left'; - if (channelConfig) { - const item = channelConfig.find(c => c.id === id); - if (item) { - if (item.alias) label = item.alias; - if (item.yAxis) yAxisKey = item.yAxis; - } - } - - return { - dataKey: id, - label: label, - connectNulls: true, - showMark: false, - yAxisKey: yAxisKey, - }; - }); - - const hasRightAxis = series.some(s => s.yAxisKey === 'right'); - - const computeAxisLimits = (axisKey) => { + computeAxisLimits(axisKey, effectiveChannels, series) { // Collect all data points for this axis let axisMin = Infinity; let axisMax = -Infinity; @@ -98,6 +92,7 @@ export default function Chart({ selectedChannels = [], channelConfig = null, axi if (axisSeries.length === 0) return {}; // No data for this axis // Check if config exists for this axis + const { axisConfig } = this.props; let cfgMin = NaN; let cfgMax = NaN; if (axisConfig && axisConfig[axisKey]) { @@ -110,7 +105,7 @@ export default function Chart({ selectedChannels = [], channelConfig = null, axi // Calculate data bounds let hasData = false; - data.forEach(row => { + this.state.data.forEach(row => { axisSeries.forEach(key => { const val = row[key]; if (val !== null && val !== undefined) { @@ -128,43 +123,75 @@ export default function Chart({ selectedChannels = [], channelConfig = null, axi if (!isNaN(cfgMax)) axisMax = Math.max(axisMax, cfgMax); return { min: axisMin, max: axisMax }; - }; - - const leftLimits = computeAxisLimits('left'); - const rightLimits = computeAxisLimits('right'); - - const yAxes = [ - { id: 'left', scaleType: 'linear', ...leftLimits } - ]; - if (hasRightAxis) { - yAxes.push({ id: 'right', scaleType: 'linear', ...rightLimits }); } - return ( - - Last 24 Hours - - - date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) - }]} - yAxis={yAxes} - rightAxis={hasRightAxis ? 'right' : null} - slotProps={{ - legend: { - direction: 'row', - position: { vertical: 'top', horizontal: 'middle' }, - padding: 0, - }, - }} - /> - - - - ); + render() { + const { loading, data } = this.state; + const { channelConfig } = this.props; + const effectiveChannels = this.getEffectiveChannels(this.props); + + if (loading) return ; + if (effectiveChannels.length === 0) return No channels selected.; + + const series = effectiveChannels.map(id => { + // Find alias and axis if config exists + let label = id; + let yAxisKey = 'left'; + if (channelConfig) { + const item = channelConfig.find(c => c.id === id); + if (item) { + if (item.alias) label = item.alias; + if (item.yAxis) yAxisKey = item.yAxis; + } + } + + return { + dataKey: id, + label: label, + connectNulls: true, + showMark: false, + yAxisKey: yAxisKey, + }; + }); + + const hasRightAxis = series.some(s => s.yAxisKey === 'right'); + + const leftLimits = this.computeAxisLimits('left', effectiveChannels, series); + const rightLimits = this.computeAxisLimits('right', effectiveChannels, series); + + const yAxes = [ + { id: 'left', scaleType: 'linear', ...leftLimits } + ]; + if (hasRightAxis) { + yAxes.push({ id: 'right', scaleType: 'linear', ...rightLimits }); + } + + return ( + + Last 24 Hours + + + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) + }]} + yAxis={yAxes} + rightAxis={hasRightAxis ? 'right' : null} + slotProps={{ + legend: { + direction: 'row', + position: { vertical: 'top', horizontal: 'middle' }, + padding: 0, + }, + }} + /> + + + + ); + } } diff --git a/uiserver/src/components/Login.js b/uiserver/src/components/Login.js index ec8a1da..6f2d2ce 100644 --- a/uiserver/src/components/Login.js +++ b/uiserver/src/components/Login.js @@ -1,18 +1,21 @@ -import React, { useState } from 'react'; -import { - Paper, TextField, Button, Typography, Container, Alert -} from '@mui/material'; -import { useNavigate } from 'react-router-dom'; +import React, { Component } from 'react'; +import { Container, Paper, TextField, Button, Typography, Box } from '@mui/material'; +import { withRouter } from './withRouter'; -export default function Login({ onLogin }) { - const [username, setUsername] = useState(''); - const [password, setPassword] = useState(''); - const [error, setError] = useState(''); - const navigate = useNavigate(); +class Login extends Component { + constructor(props) { + super(props); + this.state = { + username: '', + password: '', + error: '' + }; + } - const handleSubmit = async (e) => { + handleSubmit = async (e) => { e.preventDefault(); - setError(''); + const { username, password } = this.state; + const { onLogin, router } = this.props; try { const res = await fetch('/api/login', { @@ -20,45 +23,53 @@ export default function Login({ onLogin }) { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }) }); - const data = await res.json(); - if (!res.ok) { - throw new Error(data.error || 'Login failed'); + if (res.ok) { + onLogin(data); + router.navigate('/'); + } else { + this.setState({ error: data.error || 'Login failed' }); } - - onLogin(data); // { token, role, username } - navigate('/'); } catch (err) { - setError(err.message); + this.setState({ error: 'Network error' }); } }; - return ( - - - Login - {error && {error}} + render() { + const { username, password, error } = this.state; -
- setUsername(e.target.value)} - fullWidth - /> - setPassword(e.target.value)} - fullWidth - /> - - -
-
- ); + return ( + + + Login + {error && {error}} + + this.setState({ username: e.target.value })} + /> + this.setState({ password: e.target.value })} + /> + + + + + ); + } } + +export default withRouter(Login); diff --git a/uiserver/src/components/Settings.js b/uiserver/src/components/Settings.js index d1faf3d..602012f 100644 --- a/uiserver/src/components/Settings.js +++ b/uiserver/src/components/Settings.js @@ -1,71 +1,62 @@ -import React, { useEffect, useState } from 'react'; -import { - List, ListItem, ListItemIcon, ListItemText, ListItemSecondaryAction, - Switch, Paper, Typography, CircularProgress, Container -} from '@mui/material'; -import SensorsIcon from '@mui/icons-material/Sensors'; +import React, { Component } from 'react'; +import { Container, Typography, List, ListItem, ListItemText, Switch, CircularProgress } from '@mui/material'; -export default function Settings({ selectedChannels, onToggleChannel }) { - const [devices, setDevices] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); +export default class Settings extends Component { + constructor(props) { + super(props); + this.state = { + devices: [], + loading: true + }; + } - useEffect(() => { + componentDidMount() { fetch('/api/devices') - .then(res => { - if (!res.ok) throw new Error('Failed to load devices'); - return res.json(); - }) - .then(data => { - setDevices(data); - setLoading(false); - }) + .then(res => res.json()) + .then(data => this.setState({ devices: data, loading: false })) .catch(err => { - setError(err.message); - setLoading(false); + console.error("Failed to fetch devices", err); + this.setState({ loading: false }); }); - }, []); + } - if (loading) return ; - if (error) return Error: {error}; + toggleChannel = (id) => { + // Toggle selection + // We need to notify parent component about change (onSelectionChange) + const { selectedChannels, onSelectionChange } = this.props; + const newSelection = selectedChannels.includes(id) + ? selectedChannels.filter(c => c !== id) + : [...selectedChannels, id]; - return ( - - - Channel Selection - - + onSelectionChange(newSelection); + }; + + render() { + const { loading, devices } = this.state; + const { selectedChannels } = this.props; + + if (loading) return ; + + return ( + + Settings + Select Channels for Live View - {devices.map((device, index) => { - const id = `${device.device}:${device.channel}`; - const isSelected = selectedChannels.includes(id); - + {devices.map((item, idx) => { + const id = `${item.device}:${item.channel}`; return ( - - - - - + + this.toggleChannel(id)} /> - - onToggleChannel(id)} - /> - ); })} - {devices.length === 0 && ( - - - - )} - - - ); + + ); + } } diff --git a/uiserver/src/components/ViewDisplay.js b/uiserver/src/components/ViewDisplay.js index 2c1263e..4784b10 100644 --- a/uiserver/src/components/ViewDisplay.js +++ b/uiserver/src/components/ViewDisplay.js @@ -1,57 +1,77 @@ -import React, { useEffect, useState } from 'react'; -import { useParams } from 'react-router-dom'; +import React, { Component } from 'react'; import { Box, Typography, Container, CircularProgress } from '@mui/material'; import Chart from './Chart'; +import { withRouter } from './withRouter'; -export default function ViewDisplay() { - const { id } = useParams(); - const [view, setView] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); +class ViewDisplay extends Component { + constructor(props) { + super(props); + this.state = { + view: null, + loading: true, + error: null + }; + } + + componentDidMount() { + this.fetchView(); + } + + componentDidUpdate(prevProps) { + if (prevProps.router.params.id !== this.props.router.params.id) { + this.fetchView(); + } + } + + fetchView() { + const { id } = this.props.router.params; + this.setState({ loading: true, error: null }); - useEffect(() => { fetch(`/api/views/${id}`) .then(res => { if (!res.ok) throw new Error('View not found'); return res.json(); }) .then(data => { - setView(data); - setLoading(false); + this.setState({ view: data, loading: false }); }) .catch(err => { - setError(err.message); - setLoading(false); + this.setState({ error: err.message, loading: false }); }); - }, [id]); - - if (loading) return ; - if (error) return {error}; - - // Parse view config (compat with both array and object format) - let channelsData = []; - let axesData = null; - - if (Array.isArray(view.config)) { - channelsData = view.config; - } else if (view.config && view.config.channels) { - channelsData = view.config.channels; - axesData = view.config.axes; } - // Map view config to Chart format with aliases and axis - const channelConfig = channelsData.map(item => ({ - id: `${item.device}:${item.channel}`, - alias: item.alias, - yAxis: item.yAxis || 'left' - })); + render() { + const { view, loading, error } = this.state; - return ( - - - {view.name} + if (loading) return ; + if (error) return {error}; + + // Parse view config + let channelsData = []; + let axesData = null; + + if (Array.isArray(view.config)) { + channelsData = view.config; + } else if (view.config && view.config.channels) { + channelsData = view.config.channels; + axesData = view.config.axes; + } + + const channelConfig = channelsData.map(item => ({ + id: `${item.device}:${item.channel}`, + alias: item.alias, + yAxis: item.yAxis || 'left' + })); + + return ( + + + {view.name} + + - - - ); + ); + } } + +export default withRouter(ViewDisplay); diff --git a/uiserver/src/components/ViewManager.js b/uiserver/src/components/ViewManager.js index 5e5b539..2e6b74a 100644 --- a/uiserver/src/components/ViewManager.js +++ b/uiserver/src/components/ViewManager.js @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { Component } from 'react'; import { Container, Typography, List, ListItem, ListItemText, ListItemIcon, Button, TextField, Dialog, DialogTitle, DialogContent, DialogActions, @@ -8,61 +8,78 @@ import DashboardIcon from '@mui/icons-material/Dashboard'; import AddIcon from '@mui/icons-material/Add'; import DeleteIcon from '@mui/icons-material/Delete'; import EditIcon from '@mui/icons-material/Edit'; -import { useNavigate } from 'react-router-dom'; +import { withRouter } from './withRouter'; -export default function ViewManager({ user }) { - const [views, setViews] = useState([]); - const [open, setOpen] = useState(false); - const [editingId, setEditingId] = useState(null); // ID if editing, null if creating - const [viewName, setViewName] = useState(''); - const [availableDevices, setAvailableDevices] = useState([]); +class ViewManager extends Component { + constructor(props) { + super(props); + this.state = { + views: [], + open: false, + editingId: null, + viewName: '', + availableDevices: [], + viewConfig: [], // [{ device, channel, alias, yAxis }] + axisConfig: { + left: { min: '', max: '' }, + right: { min: '', max: '' } + }, + // Config item selection state + selDevice: '', + selChannel: '', + alias: '', + yAxis: 'left' + }; + } - const [viewConfig, setViewConfig] = useState([]); // [{ device, channel, alias, yAxis }] - const [axisConfig, setAxisConfig] = useState({ - left: { min: '', max: '' }, - right: { min: '', max: '' } - }); - - // Selection state for new item - const [selDevice, setSelDevice] = useState(''); - const [selChannel, setSelChannel] = useState(''); - const [alias, setAlias] = useState(''); - const [yAxis, setYAxis] = useState('left'); - - const navigate = useNavigate(); - const isAdmin = user && user.role === 'admin'; - - useEffect(() => { - refreshViews(); - if (isAdmin) { + componentDidMount() { + this.refreshViews(); + if (this.isAdmin()) { fetch('/api/devices') .then(res => res.json()) - .then(setAvailableDevices) + .then(devices => this.setState({ availableDevices: devices })) .catch(console.error); } - }, [isAdmin]); + } - const refreshViews = () => { + componentDidUpdate(prevProps) { + if (prevProps.user !== this.props.user) { + // If user changes (e.g. login/logout), refresh + this.refreshViews(); + if (this.isAdmin()) { + fetch('/api/devices') + .then(res => res.json()) + .then(devices => this.setState({ availableDevices: devices })) + .catch(console.error); + } + } + } + + isAdmin() { + const { user } = this.props; + return user && user.role === 'admin'; + } + + refreshViews = () => { fetch('/api/views') .then(res => res.json()) - .then(setViews) + .then(views => this.setState({ views })) .catch(console.error); }; - const handleOpenCreate = () => { - setEditingId(null); - setViewName(''); - setViewConfig([]); - setAxisConfig({ left: { min: '', max: '' }, right: { min: '', max: '' } }); - setOpen(true); + handleOpenCreate = () => { + this.setState({ + editingId: null, + viewName: '', + viewConfig: [], + axisConfig: { left: { min: '', max: '' }, right: { min: '', max: '' } }, + open: true + }); }; - const handleOpenEdit = (v, e) => { + handleOpenEdit = (v, e) => { e.stopPropagation(); - setEditingId(v.id); - setViewName(v.name); - // Fetch full config for this view fetch(`/api/views/${v.id}`) .then(res => res.json()) .then(data => { @@ -70,45 +87,48 @@ export default function ViewManager({ user }) { let axes = { left: { min: '', max: '' }, right: { min: '', max: '' } }; if (Array.isArray(data.config)) { - // Legacy format: config is just the array of channels channels = data.config; } else if (data.config && data.config.channels) { - // New format: { channels, axes } channels = data.config.channels; if (data.config.axes) { - // Merge with defaults axes = { left: { ...axes.left, ...data.config.axes.left }, right: { ...axes.right, ...data.config.axes.right } }; } } - // Ensure config items have yAxis channels = channels.map(c => ({ ...c, yAxis: c.yAxis || 'left' })); - setViewConfig(channels); - setAxisConfig(axes); - setOpen(true); + this.setState({ + editingId: v.id, + viewName: v.name, + viewConfig: channels, + axisConfig: axes, + open: true + }); }); }; - const handleDelete = async (id, e) => { + handleDelete = async (id, e) => { e.stopPropagation(); if (!window.confirm("Are you sure?")) return; + const { user } = this.props; await fetch(`/api/views/${id}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${user.token}` } }); - refreshViews(); + this.refreshViews(); }; - const handleSave = async () => { + handleSave = async () => { + const { viewName, viewConfig, editingId, axisConfig } = this.state; + const { user } = this.props; + if (!viewName || viewConfig.length === 0) return; const url = editingId ? `/api/views/${editingId}` : '/api/views'; const method = editingId ? 'PUT' : 'POST'; - // Prepare config object const finalConfig = { channels: viewConfig, axes: axisConfig @@ -125,8 +145,8 @@ export default function ViewManager({ user }) { }); if (res.ok) { - setOpen(false); - refreshViews(); + this.setState({ open: false }); + this.refreshViews(); } else { alert('Failed to save view'); } @@ -135,157 +155,173 @@ export default function ViewManager({ user }) { } }; - const addConfigItem = () => { + addConfigItem = () => { + const { selDevice, selChannel, alias, yAxis, viewConfig } = this.state; if (selDevice && selChannel) { - setViewConfig([...viewConfig, { - device: selDevice, - channel: selChannel, - alias: alias || `${selDevice}:${selChannel}`, - yAxis: yAxis - }]); - setSelDevice(''); - setSelChannel(''); - setAlias(''); - setYAxis('left'); + this.setState({ + viewConfig: [...viewConfig, { + device: selDevice, + channel: selChannel, + alias: alias || `${selDevice}:${selChannel}`, + yAxis: yAxis + }], + selDevice: '', + selChannel: '', + alias: '', + yAxis: 'left' + }); } }; - // Derived state for channels - const channelsForDevice = availableDevices.filter(d => d.device === selDevice).map(d => d.channel); - const uniqueDevices = [...new Set(availableDevices.map(d => d.device))]; - - const handleAxisChange = (axis, field, value) => { - setAxisConfig(prev => ({ - ...prev, - [axis]: { ...prev[axis], [field]: value } + handleAxisChange = (axis, field, value) => { + this.setState(prevState => ({ + axisConfig: { + ...prevState.axisConfig, + [axis]: { ...prevState.axisConfig[axis], [field]: value } + } })); }; - return ( - - - Views - {isAdmin && ( - - )} - + render() { + const { + views, open, editingId, viewName, availableDevices, viewConfig, axisConfig, + selDevice, selChannel, alias, yAxis + } = this.state; + const { router } = this.props; + const isAdmin = this.isAdmin(); - - {views.map(view => ( - navigate(`/views/${view.id}`)} - > - - - {isAdmin && ( - - handleOpenEdit(view, e)}> - handleDelete(view.id, e)}> - - )} - - ))} - {views.length === 0 && No views available.} - + const channelsForDevice = availableDevices.filter(d => d.device === selDevice).map(d => d.channel); + const uniqueDevices = [...new Set(availableDevices.map(d => d.device))]; - {/* Create/Edit Dialog */} - setOpen(false)} maxWidth="md" fullWidth> - {editingId ? 'Edit View' : 'Create New View'} - - setViewName(e.target.value)} - sx={{ mb: 2 }} - /> + return ( + + + Views + {isAdmin && ( + + )} + - {/* Axis Config Section */} - - Axis Configuration (Soft Limits) - - - Left: - handleAxisChange('left', 'min', e.target.value)} /> - handleAxisChange('left', 'max', e.target.value)} /> - - - Right: - handleAxisChange('right', 'min', e.target.value)} /> - handleAxisChange('right', 'max', e.target.value)} /> - - - - - - Add Channels - - - Device - - - - Channel - - - - Axis - - - setAlias(e.target.value)} - sx={{ flexGrow: 1 }} + + {views.map(view => ( + router.navigate(`/views/${view.id}`)} + > + + - + {isAdmin && ( + + this.handleOpenEdit(view, e)}> + this.handleDelete(view.id, e)}> + + )} + + ))} + {views.length === 0 && No views available.} + + + this.setState({ open: false })} maxWidth="md" fullWidth> + {editingId ? 'Edit View' : 'Create New View'} + + this.setState({ viewName: e.target.value })} + sx={{ mb: 2 }} + /> + + {/* Axis Config Section */} + + Axis Configuration (Soft Limits) + + + Left: + this.handleAxisChange('left', 'min', e.target.value)} /> + this.handleAxisChange('left', 'max', e.target.value)} /> + + + Right: + this.handleAxisChange('right', 'min', e.target.value)} /> + this.handleAxisChange('right', 'max', e.target.value)} /> + + - - {viewConfig.map((item, idx) => ( - { - setSelDevice(item.device); - setSelChannel(item.channel); - setAlias(item.alias); - setYAxis(item.yAxis); - setViewConfig(viewConfig.filter((_, i) => i !== idx)); - }} - onDelete={() => setViewConfig(viewConfig.filter((_, i) => i !== idx))} - sx={{ cursor: 'pointer' }} + + Add Channels + + + Device + + + + Channel + + + + Axis + + + this.setState({ alias: e.target.value })} + sx={{ flexGrow: 1 }} /> - ))} + + + + + {viewConfig.map((item, idx) => ( + { + this.setState({ + selDevice: item.device, + selChannel: item.channel, + alias: item.alias, + yAxis: item.yAxis, + viewConfig: viewConfig.filter((_, i) => i !== idx) + }); + }} + onDelete={() => this.setState({ viewConfig: viewConfig.filter((_, i) => i !== idx) })} + sx={{ cursor: 'pointer' }} + /> + ))} + + + Click a chip to edit its settings. + - - Click a chip to edit its settings. - - - - - - - - - - ); + + + + + + + + ); + } } + +export default withRouter(ViewManager); diff --git a/uiserver/src/components/withRouter.js b/uiserver/src/components/withRouter.js new file mode 100644 index 0000000..a06d504 --- /dev/null +++ b/uiserver/src/components/withRouter.js @@ -0,0 +1,18 @@ +import React from 'react'; +import { useNavigate, useLocation, useParams } from 'react-router-dom'; + +export function withRouter(Component) { + function ComponentWithRouterProp(props) { + let location = useLocation(); + let navigate = useNavigate(); + let params = useParams(); + return ( + + ); + } + + return ComponentWithRouterProp; +}