From 2c6ddf61f5111a807cb0fa85daf23169e1319c47 Mon Sep 17 00:00:00 2001 From: sebseb7 Date: Thu, 25 Dec 2025 01:48:20 +0100 Subject: [PATCH] u --- uiserver/src/components/ViewDisplay.js | 52 ++-- uiserver/src/components/ViewManager.js | 404 +++++++++++++------------ 2 files changed, 238 insertions(+), 218 deletions(-) diff --git a/uiserver/src/components/ViewDisplay.js b/uiserver/src/components/ViewDisplay.js index fd33614..1972d14 100644 --- a/uiserver/src/components/ViewDisplay.js +++ b/uiserver/src/components/ViewDisplay.js @@ -46,43 +46,41 @@ class ViewDisplay extends Component { if (loading) return ; if (error) return {error}; - // Parse view config into groups - let groups = []; - if (view.config && view.config.groups) { - groups = view.config.groups; - } else { - // Legacy - let channels = []; - let axes = null; + // Parse view config & Flatten groups if present + let channels = []; + let axes = { left: {}, right: {} }; + + if (view.config) { if (Array.isArray(view.config)) { channels = view.config; - } else if (view.config && view.config.channels) { + } else if (view.config.groups) { + // Flatten groups + view.config.groups.forEach(g => { + if (g.channels) channels = [...channels, ...g.channels]; + if (g.axes) { + if (g.axes.left) axes.left = { ...axes.left, ...g.axes.left }; + if (g.axes.right) axes.right = { ...axes.right, ...g.axes.right }; + } + }); + } else if (view.config.channels) { channels = view.config.channels; axes = view.config.axes; } - groups = [{ name: 'Default', channels, axes }]; } return ( {view.name} - - {groups.map((group, idx) => ( - - {group.name} - - ({ - id: `${c.device}:${c.channel}`, - alias: c.alias, - yAxis: c.yAxis || 'left', - color: c.color - }))} - axisConfig={group.axes} - /> - - - ))} + + ({ + id: `${c.device}:${c.channel}`, + alias: c.alias, + yAxis: c.yAxis || 'left', + color: c.color + }))} + axisConfig={axes} + /> ); diff --git a/uiserver/src/components/ViewManager.js b/uiserver/src/components/ViewManager.js index c4cffb7..f94a3aa 100644 --- a/uiserver/src/components/ViewManager.js +++ b/uiserver/src/components/ViewManager.js @@ -11,6 +11,8 @@ import DeleteIcon from '@mui/icons-material/Delete'; import EditIcon from '@mui/icons-material/Edit'; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import ArrowForwardIcon from '@mui/icons-material/ArrowForward'; +import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward'; +import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward'; import { withRouter } from './withRouter'; import Chart from './Chart'; @@ -22,25 +24,41 @@ const RANGES = { '3m': 90 * 24 * 60 * 60 * 1000, }; +const GRUVBOX_COLORS = [ + '#cc241d', '#fb4934', // Red + '#98971a', '#b8bb26', // Green + '#d79921', '#fabd2f', // Yellow + '#458588', '#83a598', // Blue + '#b16286', '#d3869b', // Purple + '#689d6a', '#8ec07c', // Aqua + '#d65d0e', '#fe8019', // Orange + '#928374', '#a89984', // Gray + '#282828', '#ebdbb2', // Bg/Fg (maybe skip dark bg) + '#1d2021', '#fbf1c7' // Hard dark/light +]; + class ViewManager extends Component { constructor(props) { super(props); this.state = { views: [], open: false, + colorPickerOpen: 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', + + // Editor State (Flat) + viewConfig: [], // [{ device, channel, alias, yAxis, color }] + axisConfig: { left: { min: '', max: '' }, right: { min: '', max: '' } }, + + // Edit Helpers + paramSelDevice: '', + paramSelChannel: '', + paramAlias: '', + paramYAxis: 'left', + + pickerTargetIndex: null, // Global Time State rangeLabel: '1d', @@ -60,7 +78,6 @@ class ViewManager extends Component { componentDidUpdate(prevProps) { if (prevProps.user !== this.props.user) { - // If user changes (e.g. login/logout), refresh this.refreshViews(); if (this.isAdmin()) { fetch('/api/devices') @@ -83,6 +100,49 @@ class ViewManager extends Component { .catch(console.error); }; + parseViewData(view) { + // Flatten config for display/editing + let channels = []; + let axes = { left: { min: '', max: '' }, right: { min: '', max: '' } }; + + const config = view.config; + if (!config) return { channels, axes }; + + if (Array.isArray(config)) { + // Very old legacy (array of channels) + channels = config; + } else if (config.groups) { + // Group format (Recent) - Flatten it! + config.groups.forEach(g => { + if (g.channels) { + channels = [...channels, ...g.channels]; + } + // Merge axes? Just take first group's axes or default? + // Let's defer to default or first group if present + if (g.axes) { + if (g.axes.left) axes.left = { ...axes.left, ...g.axes.left }; + if (g.axes.right) axes.right = { ...axes.right, ...g.axes.right }; + } + }); + } else if (config.channels) { + // Standard Legacy + channels = config.channels; + if (config.axes) axes = config.axes; + } + + // Normalize channels + channels = channels.map((c, i) => ({ + ...c, + color: c.color || this.getNextColor(i) + })); + + return { channels, axes }; + } + + getNextColor(idx) { + return GRUVBOX_COLORS[idx % GRUVBOX_COLORS.length]; + } + handleOpenCreate = () => { this.setState({ editingId: null, @@ -95,26 +155,10 @@ class ViewManager extends Component { handleOpenEdit = (v, e) => { e.stopPropagation(); - fetch(`/api/views/${v.id}`) .then(res => res.json()) .then(data => { - let channels = []; - let axes = { left: { min: '', max: '' }, right: { min: '', max: '' } }; - - if (Array.isArray(data.config)) { - channels = data.config; - } else if (data.config && data.config.channels) { - channels = data.config.channels; - if (data.config.axes) { - axes = { - left: { ...axes.left, ...data.config.axes.left }, - right: { ...axes.right, ...data.config.axes.right } - }; - } - } - channels = channels.map(c => ({ ...c, yAxis: c.yAxis || 'left' })); - + const { channels, axes } = this.parseViewData(data); this.setState({ editingId: v.id, viewName: v.name, @@ -137,7 +181,7 @@ class ViewManager extends Component { }; handleSave = async () => { - const { viewName, viewConfig, editingId, axisConfig } = this.state; + const { viewName, viewConfig, axisConfig, editingId } = this.state; const { user } = this.props; if (!viewName || viewConfig.length === 0) return; @@ -145,6 +189,7 @@ class ViewManager extends Component { const url = editingId ? `/api/views/${editingId}` : '/api/views'; const method = editingId ? 'PUT' : 'POST'; + // Save as flat format again const finalConfig = { channels: viewConfig, axes: axisConfig @@ -171,155 +216,128 @@ class ViewManager extends Component { } }; - addConfigItem = () => { - const { selDevice, selChannel, alias, yAxis, viewConfig } = this.state; - if (selDevice && selChannel) { - this.setState({ - viewConfig: [...viewConfig, { - device: selDevice, - channel: selChannel, - alias: alias || `${selDevice}:${selChannel}`, - yAxis: yAxis - }], - selDevice: '', - selChannel: '', - alias: '', - yAxis: 'left' - }); - } + // --- Editor Logic --- + addChannel = () => { + const { paramSelDevice, paramSelChannel, paramAlias, paramYAxis, viewConfig } = this.state; + if (!paramSelDevice || !paramSelChannel) return; + + const color = this.getNextColor(viewConfig.length); + + this.setState(prev => ({ + viewConfig: [...prev.viewConfig, { + device: paramSelDevice, + channel: paramSelChannel, + alias: paramAlias || `${paramSelDevice}:${paramSelChannel}`, + yAxis: paramYAxis, + color: color + }], + paramSelDevice: '', + paramSelChannel: '', + paramAlias: '', + paramYAxis: 'left' + })); }; - handleAxisChange = (axis, field, value) => { - this.setState(prevState => ({ + removeChannel = (idx) => { + this.setState(prev => ({ + viewConfig: prev.viewConfig.filter((_, i) => i !== idx) + })); + }; + + moveChannel = (idx, dir) => { + const newConfig = [...this.state.viewConfig]; + const target = idx + dir; + if (target < 0 || target >= newConfig.length) return; + [newConfig[idx], newConfig[target]] = [newConfig[target], newConfig[idx]]; + this.setState({ viewConfig: newConfig }); + }; + + updateAxis = (axis, field, val) => { + this.setState(prev => ({ axisConfig: { - ...prevState.axisConfig, - [axis]: { ...prevState.axisConfig[axis], [field]: value } + ...prev.axisConfig, + [axis]: { ...prev.axisConfig[axis], [field]: val } } })); }; - parseViewData(view) { - // view.config is already an object from API (or array for legacy) - let channelsData = []; - let axesData = null; + openColorPicker = (idx) => { + this.setState({ colorPickerOpen: true, pickerTargetIndex: idx }); + }; - if (Array.isArray(view.config)) { - channelsData = view.config; - } else if (view.config && view.config.channels) { - channelsData = view.config.channels; - axesData = view.config.axes; + selectColor = (color) => { + const { pickerTargetIndex, viewConfig } = this.state; + if (pickerTargetIndex !== null) { + const newConfig = [...viewConfig]; + newConfig[pickerTargetIndex].color = color; + this.setState({ viewConfig: newConfig, colorPickerOpen: false, pickerTargetIndex: null }); } + }; - const channelConfig = channelsData.map(item => ({ - id: `${item.device}:${item.channel}`, - alias: item.alias, - yAxis: item.yAxis || 'left' - })); - - return { channelConfig, axesData }; - } - + // --- Navigation --- handleRangeChange = (e, newVal) => { - if (newVal) { - this.setState({ rangeLabel: newVal }); - } + if (newVal) this.setState({ rangeLabel: newVal }); }; handleTimeNav = (direction) => { const { windowEnd, rangeLabel } = this.state; const rangeMs = RANGES[rangeLabel]; - let currentEnd = windowEnd ? windowEnd.getTime() : Date.now(); - let newEnd; + let newEnd = direction === -1 ? currentEnd - rangeMs : currentEnd + rangeMs; - if (direction === -1) { - newEnd = currentEnd - rangeMs; + if (direction === 1 && newEnd >= Date.now()) { + this.setState({ windowEnd: null }); } else { - newEnd = currentEnd + rangeMs; - if (newEnd >= Date.now()) { - this.setState({ windowEnd: null }); - return; - } + this.setState({ windowEnd: new Date(newEnd) }); } - this.setState({ windowEnd: new Date(newEnd) }); }; render() { const { - views, open, editingId, viewName, availableDevices, viewConfig, axisConfig, - selDevice, selChannel, alias, yAxis, - rangeLabel, windowEnd + views, open, editingId, viewName, availableDevices, + viewConfig, axisConfig, + paramSelDevice, paramSelChannel, paramAlias, paramYAxis, + rangeLabel, windowEnd, colorPickerOpen } = this.state; const isAdmin = this.isAdmin(); - const channelsForDevice = availableDevices.filter(d => d.device === selDevice).map(d => d.channel); const uniqueDevices = [...new Set(availableDevices.map(d => d.device))]; + const channelsForDevice = availableDevices.filter(d => d.device === paramSelDevice).map(d => d.channel); const rangeMs = RANGES[rangeLabel]; - - // Format Date Range for Display let dateDisplay = "Live"; if (windowEnd) { const start = new Date(windowEnd.getTime() - rangeMs); dateDisplay = `${start.toLocaleString()} - ${windowEnd.toLocaleString()}`; } else { - // For Live, technically it's "Last X" dateDisplay = `Live (Last ${rangeLabel})`; } return ( - {/* Global Time Controls (Sticky) */} - + {/* Global Time Controls */} + - - 3h - 1d - 1w - 1m - 3m + + {Object.keys(RANGES).map(r => {r})} this.handleTimeNav(-1)}> this.handleTimeNav(1)} disabled={!windowEnd}> - {dateDisplay} - - - {isAdmin && ( - - )} - + {isAdmin && } + {/* View List */} {views.map(view => { - const { channelConfig, axesData } = this.parseViewData(view); + const { channels, axes } = this.parseViewData(view); return ( - {view.name} + {view.name} {isAdmin && ( this.handleOpenEdit(view, e)}> @@ -329,8 +347,13 @@ class ViewManager extends Component { ({ + id: `${c.device}:${c.channel}`, + alias: c.alias, + yAxis: c.yAxis || 'left', + color: c.color + }))} + axisConfig={axes} windowEnd={windowEnd} range={rangeMs} /> @@ -341,97 +364,96 @@ class ViewManager extends Component { {views.length === 0 && No views available.} + {/* Edit Dialog */} this.setState({ open: false })} maxWidth="md" fullWidth> {editingId ? 'Edit View' : 'Create New View'} this.setState({ viewName: e.target.value })} - sx={{ mb: 2 }} + margin="dense" label="View Name" fullWidth value={viewName} + onChange={(e) => this.setState({ viewName: e.target.value })} sx={{ mb: 2 }} /> - {/* Axis Config Section */} + {/* Axis Config */} - Axis Configuration (Soft Limits) + Axis Configuration Left: - this.handleAxisChange('left', 'min', e.target.value)} /> - this.handleAxisChange('left', 'max', e.target.value)} /> + this.updateAxis('left', 'min', e.target.value)} /> + this.updateAxis('left', 'max', e.target.value)} /> Right: - this.handleAxisChange('right', 'min', e.target.value)} /> - this.handleAxisChange('right', 'max', e.target.value)} /> + this.updateAxis('right', 'min', e.target.value)} /> + this.updateAxis('right', 'max', e.target.value)} /> - - 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' }} - /> + + Channels + + {viewConfig.map((ch, idx) => ( + + this.openColorPicker(idx)}> + + + + this.moveChannel(idx, -1)} disabled={idx === 0}> + this.moveChannel(idx, 1)} disabled={idx === viewConfig.length - 1}> + this.removeChannel(idx)}> + ))} + + + {/* Add Channel */} + + + + + this.setState({ paramAlias: e.target.value })} /> + - - Click a chip to edit its settings. - - + + + {/* Color Picker Dialog */} + this.setState({ colorPickerOpen: false })}> + Select Color + + + {GRUVBOX_COLORS.map(c => ( + this.selectColor(c)} + sx={{ + width: 40, height: 40, bgcolor: c, cursor: 'pointer', borderRadius: 1, + border: '1px solid #000', + '&:hover': { opacity: 0.8 } + }} + /> + ))} + + + ); }