This commit is contained in:
sebseb7
2025-12-25 01:48:20 +01:00
parent 53e2e04cb9
commit 2c6ddf61f5
2 changed files with 238 additions and 218 deletions

View File

@@ -46,44 +46,42 @@ class ViewDisplay extends Component {
if (loading) return <Container sx={{ mt: 4, textAlign: 'center' }}><CircularProgress /></Container>; if (loading) return <Container sx={{ mt: 4, textAlign: 'center' }}><CircularProgress /></Container>;
if (error) return <Container sx={{ mt: 4 }}><Typography color="error">{error}</Typography></Container>; if (error) return <Container sx={{ mt: 4 }}><Typography color="error">{error}</Typography></Container>;
// Parse view config into groups // Parse view config & Flatten groups if present
let groups = [];
if (view.config && view.config.groups) {
groups = view.config.groups;
} else {
// Legacy
let channels = []; let channels = [];
let axes = null; let axes = { left: {}, right: {} };
if (view.config) {
if (Array.isArray(view.config)) { if (Array.isArray(view.config)) {
channels = 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; channels = view.config.channels;
axes = view.config.axes; axes = view.config.axes;
} }
groups = [{ name: 'Default', channels, axes }];
} }
return ( return (
<Box sx={{ height: '100%', display: 'flex', flexDirection: 'column', p: 2, gap: 2 }}> <Box sx={{ height: '100%', display: 'flex', flexDirection: 'column', p: 2, gap: 2 }}>
<Typography variant="h5">{view.name}</Typography> <Typography variant="h5">{view.name}</Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, flexGrow: 1 }}>
{groups.map((group, idx) => (
<Paper key={idx} sx={{ flex: '1 1 45%', minWidth: '400px', p: 2, display: 'flex', flexDirection: 'column', height: '500px' }}>
<Typography variant="h6">{group.name}</Typography>
<Box sx={{ flexGrow: 1 }}> <Box sx={{ flexGrow: 1 }}>
<Chart <Chart
channelConfig={group.channels.map(c => ({ channelConfig={channels.map(c => ({
id: `${c.device}:${c.channel}`, id: `${c.device}:${c.channel}`,
alias: c.alias, alias: c.alias,
yAxis: c.yAxis || 'left', yAxis: c.yAxis || 'left',
color: c.color color: c.color
}))} }))}
axisConfig={group.axes} axisConfig={axes}
/> />
</Box> </Box>
</Paper>
))}
</Box>
</Box> </Box>
); );
} }

View File

@@ -11,6 +11,8 @@ import DeleteIcon from '@mui/icons-material/Delete';
import EditIcon from '@mui/icons-material/Edit'; import EditIcon from '@mui/icons-material/Edit';
import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import ArrowForwardIcon from '@mui/icons-material/ArrowForward'; 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 { withRouter } from './withRouter';
import Chart from './Chart'; import Chart from './Chart';
@@ -22,25 +24,41 @@ const RANGES = {
'3m': 90 * 24 * 60 * 60 * 1000, '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 { class ViewManager extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
views: [], views: [],
open: false, open: false,
colorPickerOpen: false,
editingId: null, editingId: null,
viewName: '', viewName: '',
availableDevices: [], availableDevices: [],
viewConfig: [], // [{ device, channel, alias, yAxis }]
axisConfig: { // Editor State (Flat)
left: { min: '', max: '' }, viewConfig: [], // [{ device, channel, alias, yAxis, color }]
right: { min: '', max: '' } axisConfig: { left: { min: '', max: '' }, right: { min: '', max: '' } },
},
// Config item selection state // Edit Helpers
selDevice: '', paramSelDevice: '',
selChannel: '', paramSelChannel: '',
alias: '', paramAlias: '',
yAxis: 'left', paramYAxis: 'left',
pickerTargetIndex: null,
// Global Time State // Global Time State
rangeLabel: '1d', rangeLabel: '1d',
@@ -60,7 +78,6 @@ class ViewManager extends Component {
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
if (prevProps.user !== this.props.user) { if (prevProps.user !== this.props.user) {
// If user changes (e.g. login/logout), refresh
this.refreshViews(); this.refreshViews();
if (this.isAdmin()) { if (this.isAdmin()) {
fetch('/api/devices') fetch('/api/devices')
@@ -83,6 +100,49 @@ class ViewManager extends Component {
.catch(console.error); .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 = () => { handleOpenCreate = () => {
this.setState({ this.setState({
editingId: null, editingId: null,
@@ -95,26 +155,10 @@ class ViewManager extends Component {
handleOpenEdit = (v, e) => { handleOpenEdit = (v, e) => {
e.stopPropagation(); e.stopPropagation();
fetch(`/api/views/${v.id}`) fetch(`/api/views/${v.id}`)
.then(res => res.json()) .then(res => res.json())
.then(data => { .then(data => {
let channels = []; const { channels, axes } = this.parseViewData(data);
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' }));
this.setState({ this.setState({
editingId: v.id, editingId: v.id,
viewName: v.name, viewName: v.name,
@@ -137,7 +181,7 @@ class ViewManager extends Component {
}; };
handleSave = async () => { handleSave = async () => {
const { viewName, viewConfig, editingId, axisConfig } = this.state; const { viewName, viewConfig, axisConfig, editingId } = this.state;
const { user } = this.props; const { user } = this.props;
if (!viewName || viewConfig.length === 0) return; if (!viewName || viewConfig.length === 0) return;
@@ -145,6 +189,7 @@ class ViewManager extends Component {
const url = editingId ? `/api/views/${editingId}` : '/api/views'; const url = editingId ? `/api/views/${editingId}` : '/api/views';
const method = editingId ? 'PUT' : 'POST'; const method = editingId ? 'PUT' : 'POST';
// Save as flat format again
const finalConfig = { const finalConfig = {
channels: viewConfig, channels: viewConfig,
axes: axisConfig axes: axisConfig
@@ -171,155 +216,128 @@ class ViewManager extends Component {
} }
}; };
addConfigItem = () => { // --- Editor Logic ---
const { selDevice, selChannel, alias, yAxis, viewConfig } = this.state; addChannel = () => {
if (selDevice && selChannel) { const { paramSelDevice, paramSelChannel, paramAlias, paramYAxis, viewConfig } = this.state;
this.setState({ if (!paramSelDevice || !paramSelChannel) return;
viewConfig: [...viewConfig, {
device: selDevice, const color = this.getNextColor(viewConfig.length);
channel: selChannel,
alias: alias || `${selDevice}:${selChannel}`, this.setState(prev => ({
yAxis: yAxis viewConfig: [...prev.viewConfig, {
device: paramSelDevice,
channel: paramSelChannel,
alias: paramAlias || `${paramSelDevice}:${paramSelChannel}`,
yAxis: paramYAxis,
color: color
}], }],
selDevice: '', paramSelDevice: '',
selChannel: '', paramSelChannel: '',
alias: '', paramAlias: '',
yAxis: 'left' paramYAxis: 'left'
}); }));
}
}; };
handleAxisChange = (axis, field, value) => { removeChannel = (idx) => {
this.setState(prevState => ({ 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: { axisConfig: {
...prevState.axisConfig, ...prev.axisConfig,
[axis]: { ...prevState.axisConfig[axis], [field]: value } [axis]: { ...prev.axisConfig[axis], [field]: val }
} }
})); }));
}; };
parseViewData(view) { openColorPicker = (idx) => {
// view.config is already an object from API (or array for legacy) this.setState({ colorPickerOpen: true, pickerTargetIndex: idx });
let channelsData = []; };
let axesData = null;
if (Array.isArray(view.config)) { selectColor = (color) => {
channelsData = view.config; const { pickerTargetIndex, viewConfig } = this.state;
} else if (view.config && view.config.channels) { if (pickerTargetIndex !== null) {
channelsData = view.config.channels; const newConfig = [...viewConfig];
axesData = view.config.axes; 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) => { handleRangeChange = (e, newVal) => {
if (newVal) { if (newVal) this.setState({ rangeLabel: newVal });
this.setState({ rangeLabel: newVal });
}
}; };
handleTimeNav = (direction) => { handleTimeNav = (direction) => {
const { windowEnd, rangeLabel } = this.state; const { windowEnd, rangeLabel } = this.state;
const rangeMs = RANGES[rangeLabel]; const rangeMs = RANGES[rangeLabel];
let currentEnd = windowEnd ? windowEnd.getTime() : Date.now(); let currentEnd = windowEnd ? windowEnd.getTime() : Date.now();
let newEnd; let newEnd = direction === -1 ? currentEnd - rangeMs : currentEnd + rangeMs;
if (direction === -1) { if (direction === 1 && newEnd >= Date.now()) {
newEnd = currentEnd - rangeMs;
} else {
newEnd = currentEnd + rangeMs;
if (newEnd >= Date.now()) {
this.setState({ windowEnd: null }); this.setState({ windowEnd: null });
return; } else {
}
}
this.setState({ windowEnd: new Date(newEnd) }); this.setState({ windowEnd: new Date(newEnd) });
}
}; };
render() { render() {
const { const {
views, open, editingId, viewName, availableDevices, viewConfig, axisConfig, views, open, editingId, viewName, availableDevices,
selDevice, selChannel, alias, yAxis, viewConfig, axisConfig,
rangeLabel, windowEnd paramSelDevice, paramSelChannel, paramAlias, paramYAxis,
rangeLabel, windowEnd, colorPickerOpen
} = this.state; } = this.state;
const isAdmin = this.isAdmin(); 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 uniqueDevices = [...new Set(availableDevices.map(d => d.device))];
const channelsForDevice = availableDevices.filter(d => d.device === paramSelDevice).map(d => d.channel);
const rangeMs = RANGES[rangeLabel]; const rangeMs = RANGES[rangeLabel];
// Format Date Range for Display
let dateDisplay = "Live"; let dateDisplay = "Live";
if (windowEnd) { if (windowEnd) {
const start = new Date(windowEnd.getTime() - rangeMs); const start = new Date(windowEnd.getTime() - rangeMs);
dateDisplay = `${start.toLocaleString()} - ${windowEnd.toLocaleString()}`; dateDisplay = `${start.toLocaleString()} - ${windowEnd.toLocaleString()}`;
} else { } else {
// For Live, technically it's "Last X"
dateDisplay = `Live (Last ${rangeLabel})`; dateDisplay = `Live (Last ${rangeLabel})`;
} }
return ( return (
<Container maxWidth="xl" sx={{ mt: 4 }}> <Container maxWidth="xl" sx={{ mt: 4 }}>
{/* Global Time Controls (Sticky) */} {/* Global Time Controls */}
<Paper sx={{ <Paper sx={{ position: 'sticky', top: 10, zIndex: 1000, p: 2, mb: 4, display: 'flex', alignItems: 'center', justifyContent: 'space-between', border: '1px solid #504945' }}>
position: 'sticky',
top: 10,
zIndex: 1000,
p: 2,
mb: 4,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
flexWrap: 'wrap',
gap: 2,
border: '1px solid #504945'
}}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<ToggleButtonGroup <ToggleButtonGroup value={rangeLabel} exclusive onChange={this.handleRangeChange} size="small">
value={rangeLabel} {Object.keys(RANGES).map(r => <ToggleButton key={r} value={r}>{r}</ToggleButton>)}
exclusive
onChange={this.handleRangeChange}
size="small"
>
<ToggleButton value="3h">3h</ToggleButton>
<ToggleButton value="1d">1d</ToggleButton>
<ToggleButton value="1w">1w</ToggleButton>
<ToggleButton value="1m">1m</ToggleButton>
<ToggleButton value="3m">3m</ToggleButton>
</ToggleButtonGroup> </ToggleButtonGroup>
<Box> <Box>
<IconButton onClick={() => this.handleTimeNav(-1)}><ArrowBackIcon /></IconButton> <IconButton onClick={() => this.handleTimeNav(-1)}><ArrowBackIcon /></IconButton>
<IconButton onClick={() => this.handleTimeNav(1)} disabled={!windowEnd}><ArrowForwardIcon /></IconButton> <IconButton onClick={() => this.handleTimeNav(1)} disabled={!windowEnd}><ArrowForwardIcon /></IconButton>
</Box> </Box>
</Box> </Box>
<Typography variant="h6">{dateDisplay}</Typography> <Typography variant="h6">{dateDisplay}</Typography>
{isAdmin && <Button variant="contained" startIcon={<AddIcon />} onClick={this.handleOpenCreate}>Create View</Button>}
<Box>
{isAdmin && (
<Button variant="contained" startIcon={<AddIcon />} onClick={this.handleOpenCreate}>
Create View
</Button>
)}
</Box>
</Paper> </Paper>
{/* View List */}
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 4 }}> <Box sx={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
{views.map(view => { {views.map(view => {
const { channelConfig, axesData } = this.parseViewData(view); const { channels, axes } = this.parseViewData(view);
return ( return (
<Paper key={view.id} sx={{ p: 2, display: 'flex', flexDirection: 'column' }}> <Paper key={view.id} sx={{ p: 2, display: 'flex', flexDirection: 'column' }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}> <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
<Typography variant="h5">{view.name}</Typography> <Typography variant="h4">{view.name}</Typography>
{isAdmin && ( {isAdmin && (
<Box> <Box>
<IconButton onClick={(e) => this.handleOpenEdit(view, e)}><EditIcon /></IconButton> <IconButton onClick={(e) => this.handleOpenEdit(view, e)}><EditIcon /></IconButton>
@@ -329,8 +347,13 @@ class ViewManager extends Component {
</Box> </Box>
<Box sx={{ height: '500px' }}> <Box sx={{ height: '500px' }}>
<Chart <Chart
channelConfig={channelConfig} channelConfig={channels.map(c => ({
axisConfig={axesData} id: `${c.device}:${c.channel}`,
alias: c.alias,
yAxis: c.yAxis || 'left',
color: c.color
}))}
axisConfig={axes}
windowEnd={windowEnd} windowEnd={windowEnd}
range={rangeMs} range={rangeMs}
/> />
@@ -341,97 +364,96 @@ class ViewManager extends Component {
{views.length === 0 && <Typography>No views available.</Typography>} {views.length === 0 && <Typography>No views available.</Typography>}
</Box> </Box>
{/* Edit Dialog */}
<Dialog open={open} onClose={() => this.setState({ open: false })} maxWidth="md" fullWidth> <Dialog open={open} onClose={() => this.setState({ open: false })} maxWidth="md" fullWidth>
<DialogTitle>{editingId ? 'Edit View' : 'Create New View'}</DialogTitle> <DialogTitle>{editingId ? 'Edit View' : 'Create New View'}</DialogTitle>
<DialogContent> <DialogContent>
<TextField <TextField
autoFocus margin="dense" label="View Name" fullWidth value={viewName}
margin="dense" onChange={(e) => this.setState({ viewName: e.target.value })} sx={{ mb: 2 }}
label="View Name"
fullWidth
value={viewName}
onChange={(e) => this.setState({ viewName: e.target.value })}
sx={{ mb: 2 }}
/> />
{/* Axis Config Section */} {/* Axis Config */}
<Box sx={{ p: 2, border: '1px solid #444', borderRadius: 1, mb: 2 }}> <Box sx={{ p: 2, border: '1px solid #444', borderRadius: 1, mb: 2 }}>
<Typography variant="subtitle2" gutterBottom>Axis Configuration (Soft Limits)</Typography> <Typography variant="subtitle2" gutterBottom>Axis Configuration</Typography>
<Box sx={{ display: 'flex', gap: 2 }}> <Box sx={{ display: 'flex', gap: 2 }}>
<Box sx={{ flex: 1, display: 'flex', gap: 1, alignItems: 'center' }}> <Box sx={{ flex: 1, display: 'flex', gap: 1, alignItems: 'center' }}>
<Typography variant="caption" sx={{ width: 40 }}>Left:</Typography> <Typography variant="caption" sx={{ width: 40 }}>Left:</Typography>
<TextField size="small" label="Min" type="number" value={axisConfig.left.min} onChange={(e) => this.handleAxisChange('left', 'min', e.target.value)} /> <TextField size="small" placeholder="Min" value={axisConfig.left.min} onChange={e => this.updateAxis('left', 'min', e.target.value)} />
<TextField size="small" label="Max" type="number" value={axisConfig.left.max} onChange={(e) => this.handleAxisChange('left', 'max', e.target.value)} /> <TextField size="small" placeholder="Max" value={axisConfig.left.max} onChange={e => this.updateAxis('left', 'max', e.target.value)} />
</Box> </Box>
<Box sx={{ flex: 1, display: 'flex', gap: 1, alignItems: 'center' }}> <Box sx={{ flex: 1, display: 'flex', gap: 1, alignItems: 'center' }}>
<Typography variant="caption" sx={{ width: 40 }}>Right:</Typography> <Typography variant="caption" sx={{ width: 40 }}>Right:</Typography>
<TextField size="small" label="Min" type="number" value={axisConfig.right.min} onChange={(e) => this.handleAxisChange('right', 'min', e.target.value)} /> <TextField size="small" placeholder="Min" value={axisConfig.right.min} onChange={e => this.updateAxis('right', 'min', e.target.value)} />
<TextField size="small" label="Max" type="number" value={axisConfig.right.max} onChange={(e) => this.handleAxisChange('right', 'max', e.target.value)} /> <TextField size="small" placeholder="Max" value={axisConfig.right.max} onChange={e => this.updateAxis('right', 'max', e.target.value)} />
</Box> </Box>
</Box> </Box>
</Box> </Box>
<Box sx={{ mt: 2, p: 2, border: '1px solid #444', borderRadius: 1 }}> <Box sx={{ p: 2, border: '1px solid #444', borderRadius: 1 }}>
<Typography variant="subtitle2">Add Channels</Typography> <Typography variant="subtitle2">Channels</Typography>
<Box sx={{ display: 'flex', gap: 1, mt: 1, alignItems: 'center' }}> <List dense>
<FormControl size="small" sx={{ minWidth: 150 }}> {viewConfig.map((ch, idx) => (
<InputLabel>Device</InputLabel> <ListItem key={idx} sx={{ pl: 0 }}>
<Select value={selDevice} label="Device" onChange={(e) => this.setState({ selDevice: e.target.value })}> <IconButton size="small" onClick={() => this.openColorPicker(idx)}>
<Box sx={{ width: 20, height: 20, bgcolor: ch.color || '#fff', borderRadius: '50%' }} />
</IconButton>
<ListItemText
primary={ch.alias}
secondary={`${ch.device}:${ch.channel} (${ch.yAxis})`}
sx={{ ml: 1 }}
/>
<IconButton size="small" onClick={() => this.moveChannel(idx, -1)} disabled={idx === 0}><ArrowUpwardIcon /></IconButton>
<IconButton size="small" onClick={() => this.moveChannel(idx, 1)} disabled={idx === viewConfig.length - 1}><ArrowDownwardIcon /></IconButton>
<IconButton size="small" color="error" onClick={() => this.removeChannel(idx)}><DeleteIcon /></IconButton>
</ListItem>
))}
</List>
{/* Add Channel */}
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center', mt: 1, bgcolor: '#32302f', p: 1, borderRadius: 1 }}>
<Select size="small" value={paramSelDevice} displayEmpty onChange={e => this.setState({ paramSelDevice: e.target.value })} sx={{ minWidth: 120 }}>
<MenuItem value=""><em>Device</em></MenuItem>
{uniqueDevices.map(d => <MenuItem key={d} value={d}>{d}</MenuItem>)} {uniqueDevices.map(d => <MenuItem key={d} value={d}>{d}</MenuItem>)}
</Select> </Select>
</FormControl> <Select size="small" value={paramSelChannel} displayEmpty onChange={e => this.setState({ paramSelChannel: e.target.value })} sx={{ minWidth: 120 }}>
<FormControl size="small" sx={{ minWidth: 150 }}> <MenuItem value=""><em>Channel</em></MenuItem>
<InputLabel>Channel</InputLabel>
<Select value={selChannel} label="Channel" onChange={(e) => this.setState({ selChannel: e.target.value })}>
{channelsForDevice.map(c => <MenuItem key={c} value={c}>{c}</MenuItem>)} {channelsForDevice.map(c => <MenuItem key={c} value={c}>{c}</MenuItem>)}
</Select> </Select>
</FormControl> <Select size="small" value={paramYAxis} onChange={e => this.setState({ paramYAxis: e.target.value })} sx={{ width: 80 }}>
<FormControl size="small" sx={{ minWidth: 100 }}>
<InputLabel>Axis</InputLabel>
<Select value={yAxis} label="Axis" onChange={(e) => this.setState({ yAxis: e.target.value })}>
<MenuItem value="left">Left</MenuItem> <MenuItem value="left">Left</MenuItem>
<MenuItem value="right">Right</MenuItem> <MenuItem value="right">Right</MenuItem>
</Select> </Select>
</FormControl> <TextField size="small" placeholder="Alias" value={paramAlias} onChange={e => this.setState({ paramAlias: e.target.value })} />
<TextField <Button variant="contained" size="small" onClick={this.addChannel}>Add</Button>
size="small"
label="Alias (Optional)"
value={alias}
onChange={(e) => this.setState({ alias: e.target.value })}
sx={{ flexGrow: 1 }}
/>
<Button variant="outlined" onClick={this.addConfigItem}>Add</Button>
</Box> </Box>
<Box sx={{ mt: 2, display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{viewConfig.map((item, idx) => (
<Chip
key={idx}
label={`${item.alias} (${item.yAxis})`}
onClick={() => {
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' }}
/>
))}
</Box>
<Typography variant="caption" sx={{ mt: 1, display: 'block', color: 'text.secondary' }}>
Click a chip to edit its settings.
</Typography>
</Box> </Box>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={() => this.setState({ open: false })}>Cancel</Button> <Button onClick={() => this.setState({ open: false })}>Cancel</Button>
<Button onClick={this.handleSave} color="primary">Save</Button> <Button onClick={this.handleSave} variant="contained">Save</Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>
{/* Color Picker Dialog */}
<Dialog open={colorPickerOpen} onClose={() => this.setState({ colorPickerOpen: false })}>
<DialogTitle>Select Color</DialogTitle>
<DialogContent>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, width: 300 }}>
{GRUVBOX_COLORS.map(c => (
<Box
key={c}
onClick={() => this.selectColor(c)}
sx={{
width: 40, height: 40, bgcolor: c, cursor: 'pointer', borderRadius: 1,
border: '1px solid #000',
'&:hover': { opacity: 0.8 }
}}
/>
))}
</Box>
</DialogContent>
</Dialog>
</Container> </Container>
); );
} }