diff --git a/src/widget/edit-widget/edit-widget-aspect-control.js b/src/widget/edit-widget/edit-widget-aspect-control.js deleted file mode 100644 index e8a4616..0000000 --- a/src/widget/edit-widget/edit-widget-aspect-control.js +++ /dev/null @@ -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 . - ******************************************************************************/ - -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 ( - - this.props.handleChange(e)} - /> - - ); - } -} - -export default EditWidgetAspectControl; diff --git a/src/widget/edit-widget/edit-widget-aspect-control.jsx b/src/widget/edit-widget/edit-widget-aspect-control.jsx new file mode 100644 index 0000000..c02410a --- /dev/null +++ b/src/widget/edit-widget/edit-widget-aspect-control.jsx @@ -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 . + ******************************************************************************/ +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 ( + + + + ); +} + +export default EditWidgetAspectControl; diff --git a/src/widget/edit-widget/edit-widget-checkbox-control.js b/src/widget/edit-widget/edit-widget-checkbox-control.js deleted file mode 100644 index 89d5434..0000000 --- a/src/widget/edit-widget/edit-widget-checkbox-control.js +++ /dev/null @@ -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 . - ******************************************************************************/ - -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 - this.handleCheckboxChange(e)} - disabled={this.props.disabled !== 'undefined' ? this.props.disabled : true} - /> - ; - } -} - -export default EditWidgetCheckboxControl; diff --git a/src/widget/edit-widget/edit-widget-checkbox-control.jsx b/src/widget/edit-widget/edit-widget-checkbox-control.jsx new file mode 100644 index 0000000..7aa465a --- /dev/null +++ b/src/widget/edit-widget/edit-widget-checkbox-control.jsx @@ -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 . + ******************************************************************************/ +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 ( + + + + ); +} + +export default EditWidgetCheckboxControl; diff --git a/src/widget/edit-widget/edit-widget-checkbox-list.js b/src/widget/edit-widget/edit-widget-checkbox-list.js deleted file mode 100644 index 057711e..0000000 --- a/src/widget/edit-widget/edit-widget-checkbox-list.js +++ /dev/null @@ -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 . - ******************************************************************************/ - -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( this.handleCheckboxChange(e)} - />) - }) - } - - return - {this.props.label} - {checkboxList} - ; - } -} - -export default EditWidgetCheckboxList; diff --git a/src/widget/edit-widget/edit-widget-checkbox-list.jsx b/src/widget/edit-widget/edit-widget-checkbox-list.jsx new file mode 100644 index 0000000..b2d1a8d --- /dev/null +++ b/src/widget/edit-widget/edit-widget-checkbox-list.jsx @@ -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 . + ******************************************************************************/ +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) => ( + + )); + + return ( + + {props.label} + {checkboxList} + + ); +} + +export default EditWidgetCheckboxList; diff --git a/src/widget/widget-plot/plot.js b/src/widget/widget-plot/plot.js deleted file mode 100644 index 268382b..0000000 --- a/src/widget/widget-plot/plot.js +++ /dev/null @@ -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 . - ******************************************************************************/ - -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

, prevState: Readonly, 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 - }); - - this.setState({ lines, xAxis, yAxis }); - } - - render() { - - const yLabelPos = { - x: 12, - y: this.props.height / 2 - } - - return - select(node).call(this.state.xAxis)} style={{ transform: `translateX(${leftMargin + this.state.labelMargin}px) translateY(${this.props.height + topMargin - bottomMargin}px)` }} /> - select(node).call(this.state.yAxis)} style={{ transform: `translateX(${leftMargin + this.state.labelMargin}px)` }} /> - - {this.props.yLabel} - Time [s] - - - - - - - - - {this.state.lines} - - ; - } -} - -export default Plot; diff --git a/src/widget/widget-plot/plot.jsx b/src/widget/widget-plot/plot.jsx new file mode 100644 index 0000000..4f48a6b --- /dev/null +++ b/src/widget/widget-plot/plot.jsx @@ -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 . + ******************************************************************************/ +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 ( + + + + + {props.yLabel} + + + Time [s] + + + + + + + {lines} + + ); +} + +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; diff --git a/src/widget/widgets/input.js b/src/widget/widgets/input.js deleted file mode 100644 index 6981236..0000000 --- a/src/widget/widgets/input.js +++ /dev/null @@ -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 . - ******************************************************************************/ - -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 ( -

-
- - - {this.props.widget.name} - {this.props.widget.customProperties.showUnit? ( - " [" + this.state.unit + "]" - - ):( - "" - )} - - - - - this.handleKeyPress(e) } - onBlur={ (e) => this.valueChanged(this.state.value) } - onChange={ (e) => this.valueIsChanging(e.target.value) } - placeholder="Enter value" - value={ this.state.value } - /> - - - - -
-
- ); - } -} - -export default WidgetInput; diff --git a/src/widget/widgets/input.jsx b/src/widget/widgets/input.jsx new file mode 100644 index 0000000..732c9f9 --- /dev/null +++ b/src/widget/widgets/input.jsx @@ -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 . + ******************************************************************************/ +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 ( +
+
+ + + {props.widget.name} + {props.widget.customProperties.showUnit ? ` [${unit}]` : ""} + + + + valueChanged(value)} + onChange={(e) => valueIsChanging(e.target.value)} + placeholder="Enter value" + value={value} + /> + + + +
+
+ ); +} + +export default WidgetInput; diff --git a/src/widget/widgets/time-offset.js b/src/widget/widgets/time-offset.js deleted file mode 100644 index 4eef6f8..0000000 --- a/src/widget/widgets/time-offset.js +++ /dev/null @@ -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 . - ******************************************************************************/ - -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 ( -
- {this.props.widget.customProperties.icID !== -1 ? - () : (no IC) - } - {this.props.widget.customProperties.icID !== -1 && this.props.widget.customProperties.showName ? - ({this.state.icName}) : () - } - - {this.props.widget.customProperties.icID !== -1 ? - ({this.state.icName}

Offset: {this.state.timeOffset + "s"}
) - : - (Please select Infrastructure Component)} - }> - 0) && (this.state.timeOffset < this.props.widget.customProperties.threshold_yellow) && this.state.websocketOpen} - /> -
- {this.props.widget.customProperties.icID !== -1 ? - ( - {icSelected}) - : - (selected) - } -
- ); - } -} - -export default WidgetTimeOffset; diff --git a/src/widget/widgets/time-offset.jsx b/src/widget/widgets/time-offset.jsx new file mode 100644 index 0000000..e5f3cd5 --- /dev/null +++ b/src/widget/widgets/time-offset.jsx @@ -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 . + ******************************************************************************/ +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 ( +
+ {props.widget.customProperties.icID !== -1 ? ( + + ) : ( + no IC + )} + {props.widget.customProperties.icID !== -1 && + props.widget.customProperties.showName ? ( + {icName} + ) : ( + + )} + + {props.widget.customProperties.icID !== -1 ? ( + + {icName} +
+ Offset: {timeOffset + "s"} +
+ ) : ( + Please select Infrastructure Component + )} + + } + > + 0 && + timeOffset < props.widget.customProperties.threshold_yellow && + websocketOpen + } + /> +
+ {props.widget.customProperties.icID !== -1 ? ( + {icSelected} + ) : ( + selected + )} +
+ ); +} + +export default WidgetTimeOffset; diff --git a/src/widget/widgets/value.js b/src/widget/widgets/value.js deleted file mode 100644 index 7b33ffe..0000000 --- a/src/widget/widgets/value.js +++ /dev/null @@ -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 . - ******************************************************************************/ - -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 ( -
- {this.props.widget.name} - {Number.isNaN(value_to_render) ? String(NaN) : format('.3f')(value_to_render)} - {this.props.widget.customProperties.showUnit && - [{this.state.unit}] - } - {showScalingFactor && - {this.state.scalingFactor} - } - -
- ); - } -} - -export default WidgetValue; diff --git a/src/widget/widgets/value.jsx b/src/widget/widgets/value.jsx new file mode 100644 index 0000000..f272afb --- /dev/null +++ b/src/widget/widgets/value.jsx @@ -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 . + ******************************************************************************/ +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 ( +
+ + {props.widget.name} + + + {Number.isNaN(value_to_render) + ? String(NaN) + : format(".3f")(value_to_render)} + + {props.widget.customProperties.showUnit && ( + + [{unit}] + + )} + {showScalingFactor && ( + + {scalingFactor} + + )} +
+ ); +} + +export default WidgetValue;