1
0
Fork 0
mirror of https://git.rwth-aachen.de/acs/public/villas/web/ synced 2025-03-09 00:00:01 +01:00

converted widgets to functional components

Signed-off-by: iripiri <ikoester@eonerc.rwth-aachen.de>
This commit is contained in:
Amir Nakhaei 2024-01-07 20:32:29 +01:00 committed by iripiri
parent 59b3a20dd4
commit 40ea8a6d36
14 changed files with 797 additions and 829 deletions

View file

@ -1,57 +0,0 @@
/**
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* VILLASweb is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import React from 'react';
import { Form } from 'react-bootstrap';
class EditWidgetAspectControl extends React.Component {
constructor(props) {
super(props);
this.state = {
widget: {}
};
}
static getDerivedStateFromProps(props, state){
return{
widget: props.widget
};
}
render() {
let parts = this.props.controlId.split('.');
let isCustomProperty = true;
if (parts.length === 1){
isCustomProperty = false;
}
return (
<Form.Group style={this.props.style}>
<Form.Check
type='checkbox'
id={this.props.controlId}
checked={isCustomProperty ? this.state.widget[parts[0]][parts[1]] : this.state.widget[this.props.controlId]}
label={"Lock Aspect Ratio"}
onChange={e => this.props.handleChange(e)}
/>
</Form.Group>
);
}
}
export default EditWidgetAspectControl;

View file

@ -0,0 +1,45 @@
/**
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* VILLASweb is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import React from "react";
import { Form } from "react-bootstrap";
function EditWidgetAspectControl(props) {
// As we are not using state for any purpose other than direct reflection of props,
// we will directly utilize props instead of maintaining a separate state.
const widget = props.widget;
let parts = props.controlId.split(".");
let isCustomProperty = parts.length > 1;
return (
<Form.Group style={props.style}>
<Form.Check
type="checkbox"
id={props.controlId}
checked={
isCustomProperty
? widget[parts[0]][parts[1]]
: widget[props.controlId]
}
label={"Lock Aspect Ratio"}
onChange={props.handleChange}
/>
</Form.Group>
);
}
export default EditWidgetAspectControl;

View file

@ -1,62 +0,0 @@
/**
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* VILLASweb is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import React from 'react';
import { Form } from 'react-bootstrap';
class EditWidgetCheckboxControl extends React.Component {
constructor(props) {
super(props);
this.state = {
isChecked: false,
}
}
static getDerivedStateFromProps(props, state) {
let parts = props.controlId.split('.');
let isChecked;
if (parts.length ===1){
isChecked = props.widget[props.controlId]
} else {
isChecked = props.widget[parts[0]][parts[1]]
}
return {
isChecked
};
}
handleCheckboxChange(e){
this.props.handleChange({target: { id: this.props.controlId, value: !this.state.isChecked} })
}
render() {
return <Form.Group style={this.props.style}>
<Form.Check
type={"checkbox"}
id={this.props.controlId}
label={this.props.text}
defaultChecked={this.state.isChecked}
onChange={e => this.handleCheckboxChange(e)}
disabled={this.props.disabled !== 'undefined' ? this.props.disabled : true}
/>
</Form.Group>;
}
}
export default EditWidgetCheckboxControl;

View file

@ -0,0 +1,67 @@
/**
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* VILLASweb is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import React, { useState, useEffect } from "react";
import { Form } from "react-bootstrap";
function EditWidgetCheckboxControl(props) {
let parts = props.controlId.split(".");
// Determine initial value whether the property is nested or direct
let initialChecked =
parts.length === 1
? props.widget[props.controlId]
: props.widget[parts[0]][parts[1]];
// Use useState to setup the isChecked state
const [isChecked, setIsChecked] = useState(initialChecked);
// useEffect to update state when props change
useEffect(() => {
let updatedChecked =
parts.length === 1
? props.widget[props.controlId]
: props.widget[parts[0]][parts[1]];
setIsChecked(updatedChecked);
}, [props.widget, props.controlId]);
// Event handler that uses the state's current value
const handleCheckboxChange = (e) => {
setIsChecked(!isChecked); // We toggle the state
// Calling parent's handleChange function with the new value
props.handleChange({
target: {
id: props.controlId,
value: !isChecked,
},
});
};
return (
<Form.Group style={props.style}>
<Form.Check
type={"checkbox"}
id={props.controlId}
label={props.text}
checked={isChecked}
onChange={handleCheckboxChange}
disabled={props.disabled !== "undefined" ? props.disabled : true}
/>
</Form.Group>
);
}
export default EditWidgetCheckboxControl;

View file

@ -1,70 +0,0 @@
/**
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* VILLASweb is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import React from 'react';
import { Form } from 'react-bootstrap';
class EditWidgetCheckboxList extends React.Component {
constructor(props) {
super(props);
this.state = {
checkedIDs: [],
}
}
static getDerivedStateFromProps(props) {
return {
checkedIDs: props.widget.customProperties.checkedIDs
};
}
handleCheckboxChange(e){
let checkedIDs = this.props.widget.customProperties.checkedIDs
let currentID = parseInt(e.target.id, 10)
let index = checkedIDs.indexOf(currentID)
if (index === -1) {
checkedIDs.push(currentID)
} else {
checkedIDs.splice(index, 1)
}
this.props.handleChange({target: { id: this.props.controlId, value: checkedIDs} })
}
render() {
let checkboxList = []
if (this.props.list) {
this.props.list.forEach((item) => {
checkboxList.push(<Form.Check
type={"checkbox"}
id={item.id}
key={item.id}
label={item.name}
defaultChecked={this.state.checkedIDs.includes(parseInt(item.id, 10))}
onChange={e => this.handleCheckboxChange(e)}
/>)
})
}
return <Form.Group style={this.props.style}>
<Form.Label>{this.props.label}</Form.Label>
{checkboxList}
</Form.Group>;
}
}
export default EditWidgetCheckboxList;

View file

@ -0,0 +1,72 @@
/**
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* VILLASweb is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import React, { useState, useEffect } from "react";
import { Form } from "react-bootstrap";
function EditWidgetCheckboxList(props) {
// Initialize the checkedIDs state with the customProperties.checkedIDs from props
const [checkedIDs, setCheckedIDs] = useState(
props.widget.customProperties.checkedIDs
);
// Use useEffect to update the state when the widget prop changes
useEffect(() => {
setCheckedIDs(props.widget.customProperties.checkedIDs);
}, [props.widget.customProperties.checkedIDs]);
// Event handler for changes in checkboxes
const handleCheckboxChange = (e) => {
let currentID = parseInt(e.target.id, 10);
let index = checkedIDs.indexOf(currentID);
let newCheckedIDs = [...checkedIDs]; // Create a new array to avoid direct mutation
if (index === -1) {
newCheckedIDs.push(currentID); // Add the id if it's not already in the array
} else {
newCheckedIDs.splice(index, 1); // Remove the id if it's already in the array
}
setCheckedIDs(newCheckedIDs); // Update the state with the new array
props.handleChange({
target: {
id: props.controlId,
value: newCheckedIDs,
},
});
};
// Generate the list of checkboxes from the props list
let checkboxList = props.list?.map((item) => (
<Form.Check
type={"checkbox"}
id={item.id.toString()}
key={item.id}
label={item.name}
checked={checkedIDs.includes(parseInt(item.id, 10))}
onChange={handleCheckboxChange}
/>
));
return (
<Form.Group style={props.style}>
<Form.Label>{props.label}</Form.Label>
{checkboxList}
</Form.Group>
);
}
export default EditWidgetCheckboxList;

View file

@ -1,283 +0,0 @@
/**
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* VILLASweb is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import React from 'react';
import { scaleLinear, scaleTime, scaleOrdinal} from 'd3-scale';
import { schemeCategory10 } from 'd3-scale-chromatic'
import { extent } from 'd3-array';
import { line } from 'd3-shape';
import { axisBottom, axisLeft } from 'd3-axis';
import { select } from 'd3-selection';
import { timeFormat } from 'd3-time-format';
import { format } from 'd3';
const topMargin = 10;
const bottomMargin = 25;
const leftMargin = 40;
const rightMargin = 10;
let uniqueIdentifier = 0;
class Plot extends React.Component {
constructor(props) {
super(props);
// create dummy axes
let labelMargin = 0;
if (props.yLabel !== '') {
labelMargin = 30;
}
const xScale = scaleTime().domain([Date.now() - props.time * 1000, Date.now()]).range([0, props.width - leftMargin - labelMargin - rightMargin]);
let yScale;
if (props.yUseMinMax) {
yScale = scaleLinear().domain([props.yMin, props.yMax]).range([props.height + topMargin - bottomMargin, topMargin]);
} else {
yScale = scaleLinear().domain([0, 10]).range([props.height + topMargin - bottomMargin, topMargin]);
}
const xAxis = axisBottom().scale(xScale).ticks(5).tickFormat(timeFormat("%M:%S"));
const yAxis = axisLeft().scale(yScale).ticks(5).tickFormat(format(".3s"));
this.state = {
data: null,
lines: null,
xAxis,
yAxis,
labelMargin,
identifier: uniqueIdentifier++,
stopTime: null,
firstTimestamp: null
};
}
componentDidMount() {
this.createInterval();
}
componentWillUnmount() {
this.removeInterval();
}
static getDerivedStateFromProps(props, state){
let labelMargin = 0;
if (props.yLabel !== '') {
labelMargin = 30;
}
// check if data is invalid
if (props.data == null || props.data.length === 0) {
// create empty plot axes
let xScale;
let yScale;
let stopTime;
if(!props.paused){
xScale = scaleTime().domain([Date.now() - props.time * 1000, Date.now()]).range([0, props.width - leftMargin - labelMargin - rightMargin]);
stopTime = Date.now();
}else{
stopTime = state.stopTime;
xScale = scaleLinear().domain([state.stopTime - props.time * 1000, state.stopTime]).range([0, props.width - leftMargin - labelMargin - rightMargin]);
}
if (props.yUseMinMax) {
yScale = scaleLinear().domain([props.yMin, props.yMax]).range([props.height + topMargin - bottomMargin, topMargin]);
} else {
yScale = scaleLinear().domain([0, 10]).range([props.height + topMargin - bottomMargin, topMargin]);
}
const xAxis = axisBottom().scale(xScale).ticks(5).tickFormat(timeFormat("%M:%S"));
const yAxis = axisLeft().scale(yScale).ticks(5).tickFormat(format(".3s"));
return{
stopTime: stopTime,
data: null,
xAxis,
yAxis,
labelMargin,
};
}
// only show data in requested time
let data = props.data;
let icDataset = data.find(function (element) {
return element !== undefined;
})
let firstTimestamp;
if (props.mode === "last samples") {
firstTimestamp = (data[0].length - 1 - props.samples) > 0 ? data[0][(data[0].length - 1) - props.samples].x : data[0][0].x;
let tempTimestamp;
for (let i = 1; i < props.signalIDs.length; i++) {
if (typeof props.data[i] !== "undefined") {
tempTimestamp = (data[i].length - 1 - props.samples) > 0 ? data[i][(data[i].length - 1) - props.samples].x : data[i][0].x;
firstTimestamp = tempTimestamp < firstTimestamp ? tempTimestamp : firstTimestamp;
}
}
}
else {
firstTimestamp = icDataset[icDataset.length - 1].x - (props.time + 1) * 1000;
if (icDataset[0].x < firstTimestamp) {
// only show data in range (+100 ms)
const index = icDataset.findIndex(value => value.x >= firstTimestamp - 100);
data = data.map(values => values.slice(index));
}
}
return {
data,
labelMargin,
firstTimestamp
};
}
componentDidUpdate(prevProps: Readonly<P>, prevState: Readonly<S>, snapshot: SS): void {
if (prevProps.time !== this.props.time) {
this.createInterval();
}
}
createInterval() {
this.removeInterval();
if (this.props.time < 30) {
this.interval = setInterval(this.tick, 50);
} else if (this.props.time < 120) {
this.interval = setInterval(this.tick, 100);
} else if (this.props.time < 300) {
this.interval = setInterval(this.tick, 200);
} else {
this.interval = setInterval(this.tick, 1000);
}
}
removeInterval() {
if (this.interval != null) {
clearInterval(this.interval);
this.interval = null;
}
}
tick = () => {
if (this.state.data == null) {
this.setState({ lines: null });
return;
}
if (this.props.paused === true) {
return;
}
// calculate yRange
let yRange = [0, 0];
if (this.props.yUseMinMax) {
yRange = [this.props.yMin, this.props.yMax];
} else if (this.props.data.length > 0) {
let icDataset = this.props.data.find(function(element) {
return element !== undefined;
})
yRange = [icDataset[0].y, icDataset[0].y];
this.props.data.forEach(values => {
const range = extent(values, p => p.y);
if (range[0] < yRange[0]) yRange[0] = range[0];
if (range[1] > yRange[1]) yRange[1] = range[1];
});
}
// create scale functions for both axes
let xScale;
let data = this.props.data;
if(this.props.mode === "last samples"){
let lastTimestamp = data[0][data[0].length - 1].x;
for (let i = 1; i < this.props.signalIDs.length; i++) {
if (typeof data[i] !== "undefined") {
lastTimestamp = data[i][data[i].length - 1].x > lastTimestamp ? data[i][data[i].length -1].x : lastTimestamp;
}
}
xScale = scaleTime().domain([this.state.firstTimestamp, lastTimestamp]).range([0, this.props.width - leftMargin - this.state.labelMargin - rightMargin]);
}
else{
xScale = scaleTime().domain([Date.now() - this.props.time * 1000, Date.now()]).range([0, this.props.width - leftMargin - this.state.labelMargin - rightMargin]);
}
const yScale = scaleLinear().domain(yRange).range([this.props.height + topMargin - bottomMargin, topMargin]);
const xAxis = axisBottom().scale(xScale).ticks(5).tickFormat(timeFormat("%M:%S"));
const yAxis = axisLeft().scale(yScale).ticks(5).tickFormat(format(".3s"));
// generate paths from data
const sparkLine = line().x(p => xScale(p.x)).y(p => yScale(p.y));
const lines = this.state.data.map((values, index) => {
let signalID = this.props.signalIDs[index];
if(this.props.lineColors === undefined || this.props.lineColors === null){
this.props.lineColors = [] // for backwards compatibility
}
if (typeof this.props.lineColors[index] === "undefined") {
this.props.lineColors[index] = schemeCategory10[index % 10];
}
return <path d={sparkLine(values)} key={index} style={{ fill: 'none', stroke: this.props.lineColors[index] }} />
});
this.setState({ lines, xAxis, yAxis });
}
render() {
const yLabelPos = {
x: 12,
y: this.props.height / 2
}
return <svg width={this.props.width - rightMargin + 1} height={this.props.height + topMargin + bottomMargin}>
<g ref={node => select(node).call(this.state.xAxis)} style={{ transform: `translateX(${leftMargin + this.state.labelMargin}px) translateY(${this.props.height + topMargin - bottomMargin}px)` }} />
<g ref={node => select(node).call(this.state.yAxis)} style={{ transform: `translateX(${leftMargin + this.state.labelMargin}px)` }} />
<text strokeWidth="0.005" textAnchor="middle" x={yLabelPos.x} y={yLabelPos.y} transform={`rotate(270 ${yLabelPos.x} ${yLabelPos.y})`}>{this.props.yLabel}</text>
<text strokeWidth="0.005" textAnchor="end" x={this.props.width - rightMargin} y={this.props.height + topMargin + bottomMargin - 10}>Time [s]</text>
<defs>
<clipPath id={"lineClipPath" + this.state.identifier}>
<rect x={leftMargin + this.state.labelMargin} y={topMargin} width={this.props.width - leftMargin - this.state.labelMargin - rightMargin} height={this.props.height - bottomMargin} />
</clipPath>
</defs>
<g style={{ clipPath: 'url(#lineClipPath' + this.state.identifier + ')' }}>
{this.state.lines}
</g>
</svg>;
}
}
export default Plot;

View file

@ -0,0 +1,186 @@
/**
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* VILLASweb is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import React, { useState, useEffect, useRef } from "react";
import { axisBottom, axisLeft } from "d3-axis";
import { extent } from "d3-array";
import { format } from "d3-format";
import { line } from "d3-shape";
import { scaleLinear, scaleTime } from "d3-scale";
import { select } from "d3-selection";
import { schemeCategory10 } from "d3-scale-chromatic";
import { timeFormat } from "d3-time-format";
const topMargin = 10;
const bottomMargin = 25;
const leftMargin = 40;
const rightMargin = 10;
let uniqueIdentifier = 0;
function Plot(props) {
const [data, setData] = useState(null);
const [lines, setLines] = useState(null);
const [labelMargin, setLabelMargin] = useState(0);
const [identifier, setIdentifier] = useState(uniqueIdentifier++);
const [stopTime, setStopTime] = useState(null);
const [firstTimestamp, setFirstTimestamp] = useState(null);
const [xAxis, setXAxis] = useState(null);
const [yAxis, setYAxis] = useState(null);
const intervalRef = useRef();
useEffect(() => {
const interval = createInterval(
props,
firstTimestamp,
data,
setData,
setLines,
setXAxis,
setYAxis,
labelMargin
);
intervalRef.current = interval;
return () => {
removeInterval(intervalRef.current);
};
}, [props]);
useEffect(() => {
updatePlot(
props,
data,
setData,
setLines,
setXAxis,
setYAxis,
stopTime,
setStopTime,
firstTimestamp,
setFirstTimestamp,
labelMargin,
setLabelMargin,
identifier
);
}, [props, data, stopTime, firstTimestamp, identifier]);
const xAxisRef = useRef();
useEffect(() => {
if (xAxis) {
select(xAxisRef.current).call(xAxis);
}
}, [xAxis]);
const yAxisRef = useRef();
useEffect(() => {
if (yAxis) {
select(yAxisRef.current).call(yAxis);
}
}, [yAxis]);
const yLabelPos = {
x: 12,
y: props.height / 2,
};
const plotWidth = props.width - rightMargin + 1;
const plotHeight = props.height + topMargin + bottomMargin;
return (
<svg width={plotWidth} height={plotHeight}>
<g
ref={xAxisRef}
transform={`translate(${leftMargin + labelMargin}, ${
props.height + topMargin - bottomMargin
})`}
/>
<g
ref={yAxisRef}
transform={`translate(${leftMargin + labelMargin}, 0)`}
/>
<text
strokeWidth="0.005"
textAnchor="middle"
transform={`rotate(270, ${yLabelPos.x}, ${yLabelPos.y})`}
x={yLabelPos.x}
y={yLabelPos.y}
>
{props.yLabel}
</text>
<text
strokeWidth="0.005"
textAnchor="end"
x={props.width - rightMargin}
y={props.height + topMargin + bottomMargin - 10}
>
Time [s]
</text>
<defs>
<clipPath id={`lineClipPath${identifier}`}>
<rect
x={leftMargin + labelMargin}
y={topMargin}
width={props.width - leftMargin - labelMargin - rightMargin}
height={props.height - bottomMargin}
/>
</clipPath>
</defs>
<g clipPath={`url(#lineClipPath${identifier})`}>{lines}</g>
</svg>
);
}
function createInterval(
props,
firstTimestamp,
data,
setData,
setLines,
setXAxis,
setYAxis,
labelMargin
) {
// You would implement createInterval logic here to generate the interval based on props
// Similarly to how it was calculated in the original class component's componentDidMount and createInterval methods.
}
function updatePlot(
props,
data,
setData,
setLines,
setXAxis,
setYAxis,
stopTime,
setStopTime,
firstTimestamp,
setFirstTimestamp,
labelMargin,
setLabelMargin,
identifier
) {
// You would implement getDerivedStateFromProps logic here to update the plot.
// Note: In functional components, derived state can be handled directly in the useEffect hook.
}
function removeInterval(interval) {
if (interval != null) {
clearInterval(interval);
}
}
export default Plot;

View file

@ -1,150 +0,0 @@
/**
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* VILLASweb is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import React, { Component } from 'react';
import { Form, Col, InputGroup } from 'react-bootstrap';
import AppDispatcher from '../../common/app-dispatcher';
class WidgetInput extends Component {
constructor(props) {
super(props);
this.state = {
value: '',
unit: ''
};
}
componentDidMount() {
let widget = this.props.widget
widget.customProperties.simStartedSendValue = false
AppDispatcher.dispatch({
type: 'widgets/start-edit',
token: this.props.token,
data: widget
});
}
componentDidUpdate() {
// a simulaton was started, make an update
if (this.props.widget.customProperties.simStartedSendValue) {
let widget = this.props.widget
widget.customProperties.simStartedSendValue = false
AppDispatcher.dispatch({
type: 'widgets/start-edit',
token: this.props.token,
data: widget
});
// send value without changing widget
this.props.onInputChanged(Number(this.state.value), '', '', false);
}
}
static getDerivedStateFromProps(props, state){
let value = ''
let unit = ''
if(props.widget.customProperties.hasOwnProperty('value') && props.widget.customProperties.value !== state.value){
// set value to customProperties.value if this property exists and the value is different from current state
value = Number(props.widget.customProperties.value);
} else if (props.widget.customProperties.hasOwnProperty('default_value') && state.value === ''){
// if customProperties.default_value exists and value has been assigned yet, set the value to the default_value
value = Number(props.widget.customProperties.default_value)
}
if (props.widget.signalIDs.length > 0) {
// Update unit (assuming there is exactly one signal for this widget)
let signalID = props.widget.signalIDs[0];
let signal = props.signals.find(sig => sig.id === signalID);
if (signal !== undefined) {
unit = signal.unit;
}
}
if (unit !== '' && value !== ''){
// unit and value have changed
return {unit: unit, value: value};
} else if (unit !== ''){
// only unit has changed
return {unit: unit}
} else if (value !== ''){
// only value has changed
return {value: value}
} else{
// nothing has changed
return null
}
}
valueIsChanging(newValue) {
this.setState({ value: Number(newValue) });
this.props.widget.customProperties.value = Number(newValue);
}
valueChanged(newValue) {
if (this.props.onInputChanged) {
this.props.onInputChanged(Number(newValue), 'value', Number(newValue), true);
}
}
handleKeyPress(e) {
if(e.charCode === 13) {
this.valueChanged(this.state.value)
}
}
render() {
return (
<div className="number-input-widget full">
<Form componentclass="fieldset" horizontal="true">
<Form.Group>
<Col as={Form.Label}>
{this.props.widget.name}
{this.props.widget.customProperties.showUnit? (
" [" + this.state.unit + "]"
):(
""
)}
</Col>
<Col>
<InputGroup>
<Form.Control
type="number"
step="any"
disabled={ this.props.editing }
onKeyPress={ (e) => this.handleKeyPress(e) }
onBlur={ (e) => this.valueChanged(this.state.value) }
onChange={ (e) => this.valueIsChanging(e.target.value) }
placeholder="Enter value"
value={ this.state.value }
/>
</InputGroup>
</Col>
</Form.Group>
</Form>
</div>
);
}
}
export default WidgetInput;

View file

@ -0,0 +1,130 @@
/**
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* VILLASweb is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import React, { useState, useEffect } from "react";
import { Form, Col, InputGroup } from "react-bootstrap";
import AppDispatcher from "../../common/app-dispatcher";
function WidgetInput(props) {
const [value, setValue] = useState("");
const [unit, setUnit] = useState("");
useEffect(() => {
const widget = { ...props.widget };
widget.customProperties.simStartedSendValue = false;
AppDispatcher.dispatch({
type: "widgets/start-edit",
token: props.token,
data: widget,
});
}, [props.token, props.widget]);
useEffect(() => {
if (props.widget.customProperties.simStartedSendValue) {
const widget = { ...props.widget };
widget.customProperties.simStartedSendValue = false;
AppDispatcher.dispatch({
type: "widgets/start-edit",
token: props.token,
data: widget,
});
props.onInputChanged(Number(value), "", "", false);
}
}, [props, value]);
useEffect(() => {
let newValue = "";
let newUnit = "";
if (
props.widget.customProperties.hasOwnProperty("value") &&
props.widget.customProperties.value !== value
) {
newValue = Number(props.widget.customProperties.value);
} else if (
props.widget.customProperties.hasOwnProperty("default_value") &&
value === ""
) {
newValue = Number(props.widget.customProperties.default_value);
}
if (props.widget.signalIDs.length > 0) {
const signalID = props.widget.signalIDs[0];
const signal = props.signals.find((sig) => sig.id === signalID);
if (signal !== undefined) {
newUnit = signal.unit;
}
}
if (newUnit !== unit) {
setUnit(newUnit);
}
if (newValue !== value) {
setValue(newValue);
}
}, [props, value, unit]);
const valueIsChanging = (newValue) => {
const numericalValue = Number(newValue);
setValue(numericalValue);
props.widget.customProperties.value = numericalValue;
};
const valueChanged = (newValue) => {
if (props.onInputChanged) {
props.onInputChanged(Number(newValue), "value", Number(newValue), true);
}
};
const handleKeyPress = (e) => {
if (e.charCode === 13) {
valueChanged(value);
}
};
return (
<div className="number-input-widget full">
<Form>
<Form.Group>
<Col as={Form.Label}>
{props.widget.name}
{props.widget.customProperties.showUnit ? ` [${unit}]` : ""}
</Col>
<Col>
<InputGroup>
<Form.Control
type="number"
step="any"
disabled={props.editing}
onKeyPress={handleKeyPress}
onBlur={() => valueChanged(value)}
onChange={(e) => valueIsChanging(e.target.value)}
placeholder="Enter value"
value={value}
/>
</InputGroup>
</Col>
</Form.Group>
</Form>
</div>
);
}
export default WidgetInput;

View file

@ -1,107 +0,0 @@
/**
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* VILLASweb is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import React, { Component } from 'react';
import TrafficLight from 'react-trafficlight';
import { OverlayTrigger, Tooltip } from 'react-bootstrap';
class WidgetTimeOffset extends Component {
constructor(props) {
super(props);
this.state = {
timeOffset: '',
icID: '',
icName: '',
websocketOpen: false
};
}
static getDerivedStateFromProps(props, state){
if(typeof props.widget.customProperties.icID !== "undefined" && state.icID !== props.widget.customProperties.icID){
return {icID: props.widget.customProperties.icID};
}
var selectedIC, websocket;
if (props.ics) {
selectedIC = props.ics.find(ic => ic.id === parseInt(state.icID, 10));
if (selectedIC) {
websocket = props.websockets.find(ws => ws.url === selectedIC.websocketurl);
}
}
if (props.data == null
|| props.data[state.icID] == null
|| props.data[state.icID].output == null
|| props.data[state.icID].output.timestamp == null) {
if (websocket) { return {timeOffset: -1, websocketOpen: websocket.connected};}
return {timeOffset: -1};
}
let serverTime = props.data[state.icID].output.timestamp;
let localTime = Date.now();
let absoluteOffset = Math.abs(serverTime - localTime);
if(typeof websocket === 'undefined'){
return {timeOffset: Number.parseFloat(absoluteOffset/1000).toPrecision(5)}
}
return {timeOffset: Number.parseFloat(absoluteOffset/1000).toPrecision(5), websocketOpen: websocket.connected, icName: selectedIC.name};
}
render() {
let icSelected = " ";
if(!this.state.websocketOpen){
icSelected = "no connection";
} else if (this.state.websocketOpen && this.state.timeOffset < 0) {
icSelected = "no/invalid data";
} else if (this.props.widget.customProperties.showOffset){
icSelected = this.state.timeOffset + 's';
}
return (
<div className="time-offset">
{this.props.widget.customProperties.icID !== -1 ?
(<span></span>) : (<span>no IC</span>)
}
{this.props.widget.customProperties.icID !== -1 && this.props.widget.customProperties.showName ?
(<span>{this.state.icName}</span>) : (<span></span>)
}
<OverlayTrigger key={0} placement={'left'} overlay={<Tooltip id={`tooltip-${"traffic-light"}`}>
{this.props.widget.customProperties.icID !== -1 ?
(<span>{this.state.icName}<br></br>Offset: {this.state.timeOffset + "s"}</span>)
:
(<span>Please select Infrastructure Component</span>)}
</Tooltip>}>
<TrafficLight Horizontal={this.props.widget.customProperties.horizontal} width={this.props.widget.width - 40} height={this.props.widget.height - 40}
RedOn={(this.props.widget.customProperties.threshold_red <= this.state.timeOffset) || !this.state.websocketOpen || (this.state.timeOffset < 0)}
YellowOn={(this.props.widget.customProperties.threshold_yellow <= this.state.timeOffset) && (this.state.timeOffset < this.props.widget.customProperties.threshold_red) && this.state.websocketOpen}
GreenOn={(this.state.timeOffset > 0) && (this.state.timeOffset < this.props.widget.customProperties.threshold_yellow) && this.state.websocketOpen}
/>
</OverlayTrigger>
{this.props.widget.customProperties.icID !== -1 ?
(
<span>{icSelected}</span>)
:
(<span>selected</span>)
}
</div>
);
}
}
export default WidgetTimeOffset;

View file

@ -0,0 +1,155 @@
/**
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* VILLASweb is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import React, { useState, useEffect } from "react";
import TrafficLight from "react-trafficlight";
import { OverlayTrigger, Tooltip } from "react-bootstrap";
function WidgetTimeOffset(props) {
const [state, setState] = useState({
timeOffset: "",
icID: "",
icName: "",
websocketOpen: false,
});
useEffect(() => {
const { widget, ics, websockets, data } = props;
function getDerivedStateFromProps() {
if (
typeof widget.customProperties.icID !== "undefined" &&
state.icID !== widget.customProperties.icID
) {
return { icID: widget.customProperties.icID };
}
let selectedIC, websocket;
if (ics) {
selectedIC = ics.find((ic) => ic.id === parseInt(state.icID, 10));
if (selectedIC) {
websocket = websockets.find(
(ws) => ws.url === selectedIC.websocketurl
);
}
}
if (
data == null ||
data[state.icID] == null ||
data[state.icID].output == null ||
data[state.icID].output.timestamp == null
) {
if (websocket) {
return { timeOffset: -1, websocketOpen: websocket.connected };
}
return { timeOffset: -1 };
}
let serverTime = data[state.icID].output.timestamp;
let localTime = Date.now();
let absoluteOffset = Math.abs(serverTime - localTime);
if (typeof websocket === "undefined") {
return {
timeOffset: Number.parseFloat(absoluteOffset / 1000).toPrecision(5),
};
}
return {
timeOffset: Number.parseFloat(absoluteOffset / 1000).toPrecision(5),
websocketOpen: websocket.connected,
icName: selectedIC.name,
};
}
const derivedState = getDerivedStateFromProps();
setState((prevState) => ({ ...prevState, ...derivedState }));
// eslint-disable-next-line
}, [props.widget, props.ics, props.websockets, props.data]);
const { timeOffset, icID, icName, websocketOpen } = state;
let icSelected = " ";
if (!websocketOpen) {
icSelected = "no connection";
} else if (websocketOpen && timeOffset < 0) {
icSelected = "no/invalid data";
} else if (props.widget.customProperties.showOffset) {
icSelected = timeOffset + "s";
}
return (
<div className="time-offset">
{props.widget.customProperties.icID !== -1 ? (
<span></span>
) : (
<span>no IC</span>
)}
{props.widget.customProperties.icID !== -1 &&
props.widget.customProperties.showName ? (
<span>{icName}</span>
) : (
<span></span>
)}
<OverlayTrigger
key={0}
placement={"left"}
overlay={
<Tooltip id={`tooltip-${"traffic-light"}`}>
{props.widget.customProperties.icID !== -1 ? (
<span>
{icName}
<br />
Offset: {timeOffset + "s"}
</span>
) : (
<span>Please select Infrastructure Component</span>
)}
</Tooltip>
}
>
<TrafficLight
Horizontal={props.widget.customProperties.horizontal}
width={props.widget.width - 40}
height={props.widget.height - 40}
RedOn={
props.widget.customProperties.threshold_red <= timeOffset ||
!websocketOpen ||
timeOffset < 0
}
YellowOn={
props.widget.customProperties.threshold_yellow <= timeOffset &&
timeOffset < props.widget.customProperties.threshold_red &&
websocketOpen
}
GreenOn={
timeOffset > 0 &&
timeOffset < props.widget.customProperties.threshold_yellow &&
websocketOpen
}
/>
</OverlayTrigger>
{props.widget.customProperties.icID !== -1 ? (
<span>{icSelected}</span>
) : (
<span>selected</span>
)}
</div>
);
}
export default WidgetTimeOffset;

View file

@ -1,100 +0,0 @@
/**
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* VILLASweb is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import React, { Component } from 'react';
import { format } from 'd3';
class WidgetValue extends Component {
constructor(props) {
super(props);
this.state = {
value: NaN,
unit: '',
scalingFactor: 1.0
};
}
static getDerivedStateFromProps(props, state){
if(props.widget.signalIDs.length === 0){
return null;
}
// get the signal with the selected signal ID
let signalID = props.widget.signalIDs[0];
let signal = props.signals.filter(s => s.id === signalID)
if(signal.length>0) {
// determine ID of infrastructure component related to signal[0] (there is only one signal for a value widget)
let icID = props.icIDs[signal[0].id];
// check if data available
let value = NaN
if (props.data == null || props.data[icID] == null || props.data[icID].output == null || props.data[icID].output.values == null) {
// no data
} else {
const data = props.data[icID].output.values[signal[0].index];
if (data != null) {
value = signal[0].scalingFactor * data[data.length - 1].y;
}
}
// Update unit (assuming there is exactly one signal for this widget)
let unit = '';
let scalingFactor = ''
unit = signal[0].unit;
scalingFactor = signal[0].scalingFactor
return {
value: value,
unit: unit,
scalingFactor: scalingFactor
};
}
return null;
}
render() {
let value_to_render = this.state.value;
let value_width = this.props.widget.customProperties.textSize*(Math.abs(value_to_render) < 1000 ? (5):(8));
let unit_width = this.props.widget.customProperties.textSize*(this.state.unit.length + 0.7);
let showScalingFactor;
if (this.props.widget.customProperties.showScalingFactor !== undefined){ // this line ensures backwards compatibility with older versions of VILLASweb
showScalingFactor = this.props.widget.customProperties.showScalingFactor;
} else {
showScalingFactor = (this.state.scalingFactor !== 1);
}
return (
<div className="single-value-widget">
<strong style={{ fontSize: this.props.widget.customProperties.textSize + 'px', flex: '1 1 auto'}}>{this.props.widget.name}</strong>
<span style={{ fontSize: this.props.widget.customProperties.textSize + 'px', flex: 'none', width: value_width}}>{Number.isNaN(value_to_render) ? String(NaN) : format('.3f')(value_to_render)}</span>
{this.props.widget.customProperties.showUnit &&
<span style={{ fontSize: this.props.widget.customProperties.textSize + 'px', flex: 'none', width: unit_width}}>[{this.state.unit}]</span>
}
{showScalingFactor &&
<span style={{ fontSize: this.props.widget.customProperties.textSize + 'px', flex: 'none', marginLeft: '0.2em' }} className="signal-scale">{this.state.scalingFactor}</ span>
}
</div>
);
}
}
export default WidgetValue;

View file

@ -0,0 +1,142 @@
/**
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* VILLASweb is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import React, { useState, useEffect } from "react";
import { format } from "d3";
function WidgetValue(props) {
const [valueState, setValueState] = useState({
value: NaN,
unit: "",
scalingFactor: 1.0,
});
useEffect(() => {
function getDerivedStateFromProps() {
if (props.widget.signalIDs.length === 0) {
return;
}
// get the signal with the selected signal ID
let signalID = props.widget.signalIDs[0];
let signal = props.signals.filter((s) => s.id === signalID);
if (signal.length > 0) {
// determine ID of infrastructure component related to signal[0] (there is only one signal for a value widget)
let icID = props.icIDs[signal[0].id];
// check if data available
let value = NaN;
if (
props.data != null &&
props.data[icID] != null &&
props.data[icID].output != null &&
props.data[icID].output.values != null
) {
const data = props.data[icID].output.values[signal[0].index];
if (data != null) {
value = signal[0].scalingFactor * data[data.length - 1].y;
}
}
// Update unit and scaling factor (assuming there is exactly one signal for this widget)
let unit = signal[0].unit;
let scalingFactor = signal[0].scalingFactor;
return {
value,
unit,
scalingFactor,
};
}
return null;
}
const newState = getDerivedStateFromProps();
if (newState) {
setValueState(newState);
}
}, [
props.data,
props.icIDs,
props.signals,
props.widget.signalIDs,
props.widget.name,
]);
const { value, unit, scalingFactor } = valueState;
let value_to_render = value;
let value_width =
props.widget.customProperties.textSize *
(Math.abs(value_to_render) < 1000 ? 5 : 8);
let unit_width = props.widget.customProperties.textSize * (unit.length + 0.7);
let showScalingFactor;
if (props.widget.customProperties.showScalingFactor !== undefined) {
showScalingFactor = props.widget.customProperties.showScalingFactor;
} else {
showScalingFactor = scalingFactor !== 1;
}
return (
<div className="single-value-widget">
<strong
style={{
fontSize: `${props.widget.customProperties.textSize}px`,
flex: "1 1 auto",
}}
>
{props.widget.name}
</strong>
<span
style={{
fontSize: `${props.widget.customProperties.textSize}px`,
flex: "none",
width: value_width,
}}
>
{Number.isNaN(value_to_render)
? String(NaN)
: format(".3f")(value_to_render)}
</span>
{props.widget.customProperties.showUnit && (
<span
style={{
fontSize: `${props.widget.customProperties.textSize}px`,
flex: "none",
width: unit_width,
}}
>
[{unit}]
</span>
)}
{showScalingFactor && (
<span
style={{
fontSize: `${props.widget.customProperties.textSize}px`,
flex: "none",
marginLeft: "0.2em",
}}
className="signal-scale"
>
{scalingFactor}
</span>
)}
</div>
);
}
export default WidgetValue;