u
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user