u
This commit is contained in:
25
uiserver/src/components/Chart.js
vendored
25
uiserver/src/components/Chart.js
vendored
@@ -99,7 +99,9 @@ export default class Chart extends Component {
|
|||||||
|
|
||||||
for (let i = 0; i < points.length; i++) {
|
for (let i = 0; i < points.length; i++) {
|
||||||
const [tsStr, rawVal, untilStr] = points[i];
|
const [tsStr, rawVal, untilStr] = points[i];
|
||||||
const val = Number(rawVal);
|
const numVal = Number(rawVal);
|
||||||
|
// MUI-X charts only accepts numbers and null - NaN causes errors
|
||||||
|
const val = Number.isNaN(numVal) ? null : numVal;
|
||||||
|
|
||||||
let start = new Date(tsStr).getTime();
|
let start = new Date(tsStr).getTime();
|
||||||
let explicitEnd = untilStr ? new Date(untilStr).getTime() : null;
|
let explicitEnd = untilStr ? new Date(untilStr).getTime() : null;
|
||||||
@@ -230,12 +232,14 @@ export default class Chart extends Component {
|
|||||||
let label = id;
|
let label = id;
|
||||||
let yAxisKey = 'left';
|
let yAxisKey = 'left';
|
||||||
let color = undefined;
|
let color = undefined;
|
||||||
|
let fillColor = undefined;
|
||||||
if (channelConfig) {
|
if (channelConfig) {
|
||||||
const item = channelConfig.find(c => c.id === id);
|
const item = channelConfig.find(c => c.id === id);
|
||||||
if (item) {
|
if (item) {
|
||||||
if (item.alias) label = item.alias;
|
if (item.alias) label = item.alias;
|
||||||
if (item.yAxis) yAxisKey = item.yAxis;
|
if (item.yAxis) yAxisKey = item.yAxis;
|
||||||
if (item.color) color = item.color;
|
if (item.color) color = item.color;
|
||||||
|
if (item.fillColor) fillColor = item.fillColor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,6 +251,16 @@ export default class Chart extends Component {
|
|||||||
yAxisKey: yAxisKey,
|
yAxisKey: yAxisKey,
|
||||||
};
|
};
|
||||||
if (color) sObj.color = color;
|
if (color) sObj.color = color;
|
||||||
|
// Enable area fill if fillColor is set (with 50% transparency)
|
||||||
|
if (fillColor) {
|
||||||
|
sObj.area = true;
|
||||||
|
// Convert hex to rgba with 50% opacity
|
||||||
|
const hex = fillColor.replace('#', '');
|
||||||
|
const r = parseInt(hex.substring(0, 2), 16);
|
||||||
|
const g = parseInt(hex.substring(2, 4), 16);
|
||||||
|
const b = parseInt(hex.substring(4, 6), 16);
|
||||||
|
sObj.areaColor = `rgba(${r}, ${g}, ${b}, 0.5)`;
|
||||||
|
}
|
||||||
return sObj;
|
return sObj;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -289,6 +303,15 @@ export default class Chart extends Component {
|
|||||||
position: { vertical: 'top', horizontal: 'middle' },
|
position: { vertical: 'top', horizontal: 'middle' },
|
||||||
padding: 0,
|
padding: 0,
|
||||||
},
|
},
|
||||||
|
lineHighlight: { strokeWidth: 3 },
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
'& .MuiLineElement-root': {
|
||||||
|
strokeWidth: 3,
|
||||||
|
},
|
||||||
|
'& .MuiAreaElement-root': {
|
||||||
|
fillOpacity: 0.5,
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ class ViewManager extends Component {
|
|||||||
views: [],
|
views: [],
|
||||||
open: false,
|
open: false,
|
||||||
colorPickerOpen: false,
|
colorPickerOpen: false,
|
||||||
|
colorPickerMode: 'line',
|
||||||
editingId: null,
|
editingId: null,
|
||||||
viewName: '',
|
viewName: '',
|
||||||
availableDevices: [],
|
availableDevices: [],
|
||||||
@@ -183,7 +184,7 @@ class ViewManager extends Component {
|
|||||||
const { user } = this.props;
|
const { user } = this.props;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fetch('/api/views/reorder', {
|
const res = await fetch('/api/views/reorder', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -191,6 +192,10 @@ class ViewManager extends Component {
|
|||||||
},
|
},
|
||||||
body: JSON.stringify({ order })
|
body: JSON.stringify({ order })
|
||||||
});
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const err = await res.json();
|
||||||
|
console.error("Failed to save order:", err);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to save order", err);
|
console.error("Failed to save order", err);
|
||||||
}
|
}
|
||||||
@@ -276,19 +281,33 @@ class ViewManager extends Component {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
openColorPicker = (idx) => {
|
openColorPicker = (idx, mode = 'line') => {
|
||||||
this.setState({ colorPickerOpen: true, pickerTargetIndex: idx });
|
this.setState({ colorPickerOpen: true, pickerTargetIndex: idx, colorPickerMode: mode });
|
||||||
};
|
};
|
||||||
|
|
||||||
selectColor = (color) => {
|
selectColor = (color) => {
|
||||||
const { pickerTargetIndex, viewConfig } = this.state;
|
const { pickerTargetIndex, viewConfig, colorPickerMode } = this.state;
|
||||||
if (pickerTargetIndex !== null) {
|
if (pickerTargetIndex !== null) {
|
||||||
const newConfig = [...viewConfig];
|
const newConfig = viewConfig.map((ch, i) => {
|
||||||
newConfig[pickerTargetIndex].color = color;
|
if (i === pickerTargetIndex) {
|
||||||
|
if (colorPickerMode === 'fill') {
|
||||||
|
return { ...ch, fillColor: color };
|
||||||
|
} else {
|
||||||
|
return { ...ch, color: color };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ch;
|
||||||
|
});
|
||||||
this.setState({ viewConfig: newConfig, colorPickerOpen: false, pickerTargetIndex: null });
|
this.setState({ viewConfig: newConfig, colorPickerOpen: false, pickerTargetIndex: null });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
clearFillColor = (idx) => {
|
||||||
|
const newConfig = [...this.state.viewConfig];
|
||||||
|
delete newConfig[idx].fillColor;
|
||||||
|
this.setState({ viewConfig: newConfig });
|
||||||
|
};
|
||||||
|
|
||||||
handleRangeChange = (e, newVal) => {
|
handleRangeChange = (e, newVal) => {
|
||||||
if (newVal) this.setState({ rangeLabel: newVal });
|
if (newVal) this.setState({ rangeLabel: newVal });
|
||||||
};
|
};
|
||||||
@@ -371,7 +390,8 @@ class ViewManager extends Component {
|
|||||||
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,
|
||||||
|
fillColor: c.fillColor
|
||||||
}))}
|
}))}
|
||||||
axisConfig={axes}
|
axisConfig={axes}
|
||||||
windowEnd={windowEnd}
|
windowEnd={windowEnd}
|
||||||
@@ -413,9 +433,17 @@ class ViewManager extends Component {
|
|||||||
<List dense>
|
<List dense>
|
||||||
{viewConfig.map((ch, idx) => (
|
{viewConfig.map((ch, idx) => (
|
||||||
<ListItem key={idx} sx={{ pl: 0 }}>
|
<ListItem key={idx} sx={{ pl: 0 }}>
|
||||||
<IconButton size="small" onClick={() => this.openColorPicker(idx)}>
|
<IconButton size="small" onClick={() => this.openColorPicker(idx, 'line')} title="Line color">
|
||||||
<Box sx={{ width: 20, height: 20, bgcolor: ch.color || '#fff', borderRadius: '50%' }} />
|
<Box sx={{ width: 20, height: 20, bgcolor: ch.color || '#fff', borderRadius: '50%', border: '2px solid #fff' }} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
<IconButton size="small" onClick={() => this.openColorPicker(idx, 'fill')} title="Fill color (area)">
|
||||||
|
<Box sx={{ width: 20, height: 20, bgcolor: ch.fillColor || 'transparent', borderRadius: '50%', border: ch.fillColor ? '2px solid #fff' : '2px dashed #666' }} />
|
||||||
|
</IconButton>
|
||||||
|
{ch.fillColor && (
|
||||||
|
<IconButton size="small" onClick={() => this.clearFillColor(idx)} title="Remove fill" sx={{ ml: -0.5 }}>
|
||||||
|
<DeleteIcon sx={{ fontSize: 14 }} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={ch.alias}
|
primary={ch.alias}
|
||||||
secondary={`${ch.device}:${ch.channel} (${ch.yAxis})`}
|
secondary={`${ch.device}:${ch.channel} (${ch.yAxis})`}
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ module.exports = {
|
|||||||
// Publicly list views
|
// Publicly list views
|
||||||
app.get('/api/views', (req, res) => {
|
app.get('/api/views', (req, res) => {
|
||||||
try {
|
try {
|
||||||
const stmt = db.prepare('SELECT * FROM views ORDER BY name');
|
const stmt = db.prepare('SELECT * FROM views ORDER BY position ASC, id ASC');
|
||||||
const rows = stmt.all();
|
const rows = stmt.all();
|
||||||
const views = rows.map(row => {
|
const views = rows.map(row => {
|
||||||
try {
|
try {
|
||||||
@@ -148,7 +148,7 @@ module.exports = {
|
|||||||
|
|
||||||
app.get('/api/views/:id', (req, res) => {
|
app.get('/api/views/:id', (req, res) => {
|
||||||
try {
|
try {
|
||||||
const stmt = db.prepare('SELECT * FROM views ORDER BY position ASC, id ASC');
|
const stmt = db.prepare('SELECT * FROM views WHERE id = ?');
|
||||||
const view = stmt.get(req.params.id);
|
const view = stmt.get(req.params.id);
|
||||||
if (view) {
|
if (view) {
|
||||||
view.config = JSON.parse(view.config);
|
view.config = JSON.parse(view.config);
|
||||||
@@ -195,17 +195,23 @@ module.exports = {
|
|||||||
// Reorder Views
|
// Reorder Views
|
||||||
app.post('/api/views/reorder', requireAdmin, (req, res) => {
|
app.post('/api/views/reorder', requireAdmin, (req, res) => {
|
||||||
const { order } = req.body;
|
const { order } = req.body;
|
||||||
|
console.log('[API] Reorder request:', order);
|
||||||
if (!Array.isArray(order)) return res.status(400).json({ error: 'Invalid format' });
|
if (!Array.isArray(order)) return res.status(400).json({ error: 'Invalid format' });
|
||||||
|
|
||||||
const updateStmt = db.prepare('UPDATE views SET position = ? WHERE id = ?');
|
const updateStmt = db.prepare('UPDATE views SET position = ? WHERE id = ?');
|
||||||
const updateMany = db.transaction((items) => {
|
const updateMany = db.transaction((items) => {
|
||||||
for (const item of items) updateStmt.run(item.position, item.id);
|
for (const item of items) {
|
||||||
|
console.log('[API] Updating view', item.id, 'to position', item.position);
|
||||||
|
updateStmt.run(item.position, item.id);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
updateMany(order);
|
updateMany(order);
|
||||||
|
console.log('[API] Reorder successful');
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
console.error('[API] Reorder error:', err);
|
||||||
res.status(500).json({ error: err.message });
|
res.status(500).json({ error: err.message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user