u
This commit is contained in:
@@ -46,43 +46,41 @@ class ViewDisplay extends Component {
|
||||
if (loading) return <Container sx={{ mt: 4, textAlign: 'center' }}><CircularProgress /></Container>;
|
||||
if (error) return <Container sx={{ mt: 4 }}><Typography color="error">{error}</Typography></Container>;
|
||||
|
||||
// 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 (
|
||||
<Box sx={{ height: '100%', display: 'flex', flexDirection: 'column', p: 2, gap: 2 }}>
|
||||
<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 }}>
|
||||
<Chart
|
||||
channelConfig={group.channels.map(c => ({
|
||||
id: `${c.device}:${c.channel}`,
|
||||
alias: c.alias,
|
||||
yAxis: c.yAxis || 'left',
|
||||
color: c.color
|
||||
}))}
|
||||
axisConfig={group.axes}
|
||||
/>
|
||||
</Box>
|
||||
</Paper>
|
||||
))}
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
<Chart
|
||||
channelConfig={channels.map(c => ({
|
||||
id: `${c.device}:${c.channel}`,
|
||||
alias: c.alias,
|
||||
yAxis: c.yAxis || 'left',
|
||||
color: c.color
|
||||
}))}
|
||||
axisConfig={axes}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
<Container maxWidth="xl" sx={{ mt: 4 }}>
|
||||
{/* Global Time Controls (Sticky) */}
|
||||
<Paper sx={{
|
||||
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'
|
||||
}}>
|
||||
{/* Global Time Controls */}
|
||||
<Paper sx={{ position: 'sticky', top: 10, zIndex: 1000, p: 2, mb: 4, display: 'flex', alignItems: 'center', justifyContent: 'space-between', border: '1px solid #504945' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
<ToggleButtonGroup
|
||||
value={rangeLabel}
|
||||
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 value={rangeLabel} exclusive onChange={this.handleRangeChange} size="small">
|
||||
{Object.keys(RANGES).map(r => <ToggleButton key={r} value={r}>{r}</ToggleButton>)}
|
||||
</ToggleButtonGroup>
|
||||
<Box>
|
||||
<IconButton onClick={() => this.handleTimeNav(-1)}><ArrowBackIcon /></IconButton>
|
||||
<IconButton onClick={() => this.handleTimeNav(1)} disabled={!windowEnd}><ArrowForwardIcon /></IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Typography variant="h6">{dateDisplay}</Typography>
|
||||
|
||||
<Box>
|
||||
{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>}
|
||||
</Paper>
|
||||
|
||||
{/* View List */}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
{views.map(view => {
|
||||
const { channelConfig, axesData } = this.parseViewData(view);
|
||||
const { channels, axes } = this.parseViewData(view);
|
||||
return (
|
||||
<Paper key={view.id} sx={{ p: 2, display: 'flex', flexDirection: 'column' }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
|
||||
<Typography variant="h5">{view.name}</Typography>
|
||||
<Typography variant="h4">{view.name}</Typography>
|
||||
{isAdmin && (
|
||||
<Box>
|
||||
<IconButton onClick={(e) => this.handleOpenEdit(view, e)}><EditIcon /></IconButton>
|
||||
@@ -329,8 +347,13 @@ class ViewManager extends Component {
|
||||
</Box>
|
||||
<Box sx={{ height: '500px' }}>
|
||||
<Chart
|
||||
channelConfig={channelConfig}
|
||||
axisConfig={axesData}
|
||||
channelConfig={channels.map(c => ({
|
||||
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 && <Typography>No views available.</Typography>}
|
||||
</Box>
|
||||
|
||||
{/* Edit Dialog */}
|
||||
<Dialog open={open} onClose={() => this.setState({ open: false })} maxWidth="md" fullWidth>
|
||||
<DialogTitle>{editingId ? 'Edit View' : 'Create New View'}</DialogTitle>
|
||||
<DialogContent>
|
||||
<TextField
|
||||
autoFocus
|
||||
margin="dense"
|
||||
label="View Name"
|
||||
fullWidth
|
||||
value={viewName}
|
||||
onChange={(e) => 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 */}
|
||||
<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={{ flex: 1, display: 'flex', gap: 1, alignItems: 'center' }}>
|
||||
<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" label="Max" type="number" value={axisConfig.left.max} onChange={(e) => this.handleAxisChange('left', 'max', e.target.value)} />
|
||||
<TextField size="small" placeholder="Min" value={axisConfig.left.min} onChange={e => this.updateAxis('left', 'min', e.target.value)} />
|
||||
<TextField size="small" placeholder="Max" value={axisConfig.left.max} onChange={e => this.updateAxis('left', 'max', e.target.value)} />
|
||||
</Box>
|
||||
<Box sx={{ flex: 1, display: 'flex', gap: 1, alignItems: 'center' }}>
|
||||
<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" label="Max" type="number" value={axisConfig.right.max} onChange={(e) => this.handleAxisChange('right', 'max', e.target.value)} />
|
||||
<TextField size="small" placeholder="Min" value={axisConfig.right.min} onChange={e => this.updateAxis('right', 'min', 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 sx={{ mt: 2, p: 2, border: '1px solid #444', borderRadius: 1 }}>
|
||||
<Typography variant="subtitle2">Add Channels</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 1, mt: 1, alignItems: 'center' }}>
|
||||
<FormControl size="small" sx={{ minWidth: 150 }}>
|
||||
<InputLabel>Device</InputLabel>
|
||||
<Select value={selDevice} label="Device" onChange={(e) => this.setState({ selDevice: e.target.value })}>
|
||||
{uniqueDevices.map(d => <MenuItem key={d} value={d}>{d}</MenuItem>)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl size="small" sx={{ minWidth: 150 }}>
|
||||
<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>)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<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="right">Right</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<TextField
|
||||
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 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 sx={{ p: 2, border: '1px solid #444', borderRadius: 1 }}>
|
||||
<Typography variant="subtitle2">Channels</Typography>
|
||||
<List dense>
|
||||
{viewConfig.map((ch, idx) => (
|
||||
<ListItem key={idx} sx={{ pl: 0 }}>
|
||||
<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>)}
|
||||
</Select>
|
||||
<Select size="small" value={paramSelChannel} displayEmpty onChange={e => this.setState({ paramSelChannel: e.target.value })} sx={{ minWidth: 120 }}>
|
||||
<MenuItem value=""><em>Channel</em></MenuItem>
|
||||
{channelsForDevice.map(c => <MenuItem key={c} value={c}>{c}</MenuItem>)}
|
||||
</Select>
|
||||
<Select size="small" value={paramYAxis} onChange={e => this.setState({ paramYAxis: e.target.value })} sx={{ width: 80 }}>
|
||||
<MenuItem value="left">Left</MenuItem>
|
||||
<MenuItem value="right">Right</MenuItem>
|
||||
</Select>
|
||||
<TextField size="small" placeholder="Alias" value={paramAlias} onChange={e => this.setState({ paramAlias: e.target.value })} />
|
||||
<Button variant="contained" size="small" onClick={this.addChannel}>Add</Button>
|
||||
</Box>
|
||||
<Typography variant="caption" sx={{ mt: 1, display: 'block', color: 'text.secondary' }}>
|
||||
Click a chip to edit its settings.
|
||||
</Typography>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<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>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user