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 && (
- } onClick={this.handleOpenCreate}>
- Create View
-
- )}
-
+ {isAdmin && } onClick={this.handleOpenCreate}>Create View}
+ {/* 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 */}
+
+ {/* Color Picker Dialog */}
+
);
}