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
-
-
- } onClick={() => navigate('/')}>
- Views
-
- } onClick={() => navigate('/live')}>
- Live
-
- } onClick={() => navigate('/settings')}>
- Settings
-
-
- {user ? (
-
-
-
-
-
-
- ) : (
-
- )}
-
-
- );
-}
-
-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
+
+
+ }>Views
+ }>Live
+ }>Settings
+
+ {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;
-
-
-
- );
+ 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 && (
- } onClick={handleOpenCreate}>
- Create View
-
- )}
-
+ 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 */}
-
+
+ );
+ }
}
+
+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;
+}