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

Merge branch 'time-offset-widget'

# Conflicts:
#	src/widget/edit-widget/edit-widget-control-creator.js
This commit is contained in:
Sonja Happ 2021-01-04 14:07:53 +01:00
commit fec5c0c68a
13 changed files with 1539 additions and 1528 deletions

2729
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -5,10 +5,10 @@
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.32",
"@fortawesome/free-solid-svg-icons": "^5.15.1",
"@fortawesome/react-fontawesome": "^0.1.12",
"@fortawesome/react-fontawesome": "^0.1.13",
"babel-runtime": "^6.26.0",
"bootstrap": "^4.5.3",
"bufferutil": "^4.0.1",
"bufferutil": "^4.0.2",
"canvas": "^2.6.1",
"classnames": "^2.2.6",
"d3-array": "^2.8.0",
@ -20,7 +20,7 @@
"d3-time-format": "^3.0.0",
"es6-promise": "^4.2.8",
"fibers": "^5.0.0",
"file-saver": "^2.0.2",
"file-saver": "^2.0.5",
"flux": "^3.1.3",
"gaugeJS": "^1.3.7",
"handlebars": "^4.7.6",
@ -29,15 +29,15 @@
"libcimsvg": "git+https://git.rwth-aachen.de/acs/public/cim/pintura-npm-package.git",
"lodash": "^4.17.20",
"moment": "^2.29.1",
"multiselect-react-dropdown": "^1.6.1",
"multiselect-react-dropdown": "^1.6.2",
"node-sass": "^4.14.1",
"popper.js": "^1.16.1",
"prop-types": "^15.7.2",
"rc-slider": "^9.6.0",
"rc-slider": "^9.6.4",
"react": "^16.14.0",
"react-bootstrap": "^1.4.0",
"react-bootstrap-time-picker": "^2.0.1",
"react-collapse": "^5.0.1",
"react-collapse": "^5.1.0",
"react-color": "^2.19.3",
"react-contexify": "^4.1.1",
"react-d3": "^0.4.0",
@ -51,17 +51,18 @@
"react-rnd": "^10.2.3",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0",
"react-scripts": "^4.0.0",
"react-scripts": "^4.0.1",
"react-svg-pan-zoom": "^3.8.1",
"sass": "^1.28.0",
"react-trafficlight": "^5.2.1",
"sass": "^1.29.0",
"superagent": "^6.1.0",
"ts-node": "^9.0.0",
"type-fest": "^0.13.1",
"typescript": "^4.0.5",
"utf-8-validate": "^5.0.2",
"typescript": "^4.1.2",
"utf-8-validate": "^5.0.3",
"validator": "^13.1.17",
"webpack-hot-middleware": "^2.25.0",
"webpack-plugin-serve": "^1.2.0"
"webpack-plugin-serve": "^1.2.1"
},
"devDependencies": {
"chai": "^4.2.0"

View file

@ -14,6 +14,8 @@
* You should have received a copy of the GNU General Public License
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import NotificationsDataManager from "../data-managers/notifications-data-manager";
import AppDispatcher from '../app-dispatcher';
class WebsocketAPI {
constructor(websocketurl, callbacks) {
@ -65,6 +67,10 @@ class WebsocketAPI {
}
onOpen = e => {
AppDispatcher.dispatch({
type: 'websocket/connected',
data: this.websocketurl,
});
this.wasConnected = true;
if ('onOpen' in this.callbacks)
@ -78,6 +84,16 @@ class WebsocketAPI {
}
else {
if (this.wasConnected) {
AppDispatcher.dispatch({
type: 'websocket/connection-error',
data: this.websocketurl,
});
const IC_WEBSOCKET_CONNECTION_ERROR = {
title: 'Websocket connection warning',
message: "Connection to " + this.websocketurl + " dropped. Attempt reconnect in 1 sec",
level: 'warning'
};
NotificationsDataManager.addNotification(IC_WEBSOCKET_CONNECTION_ERROR);
console.log("Connection to " + this.websocketurl + " dropped. Attempt reconnect in 1 sec");
window.setTimeout(() => { this.reconnect(); }, 1000);
}

View file

@ -557,6 +557,7 @@ class Dashboard extends Component {
widget={this.state.modalData}
signals={this.state.signals}
files={this.state.files}
ics={this.state.ics}
/>
<EditFiles

View file

@ -394,4 +394,19 @@ div[class*="-widget"] label {
height: 100%;
border: 2px solid lightgray;
}
/* End box widget */
/* End box widget */
/* Begin time offset widget */
.time-offset {
display: flex;
align-items: center;
justify-content: space-around;
flex-direction: column;
}
.time-offset span {
text-align: center;
font-size: 1em;
font-weight: 600;
}
/* End time offset widget */

View file

@ -31,10 +31,11 @@ import EditWidgetCheckboxControl from './edit-widget-checkbox-control';
import EditWidgetColorZonesControl from './edit-widget-color-zones-control';
import EditWidgetMinMaxControl from './edit-widget-min-max-control';
import EditWidgetParametersControl from './edit-widget-parameters-control';
import EditWidgetICControl from './edit-widget-ic-control';
import EditWidgetPlotColorsControl from './edit-widget-plot-colors-control';
//import EditWidgetHTMLContent from './edit-widget-html-content';
export default function CreateControls(widgetType = null, widget = null, sessionToken = null, files = null, signals, handleChange) {
export default function CreateControls(widgetType = null, widget = null, sessionToken = null, files = null,ics = null, signals, handleChange) {
// Use a list to concatenate the controls according to the widget type
var DialogControls = [];
@ -163,6 +164,16 @@ export default function CreateControls(widgetType = null, widget = null, session
);
break;
case 'TimeOffset':
DialogControls.push(
<EditWidgetICControl key={0} widget={widget} controlId={'customProperties.icID'} input ics={ics} handleChange={(e) => handleChange(e)}/>,
<EditWidgetNumberControl key={1} widget={widget} controlId={'customProperties.threshold_yellow'} label={'Threshold yellow'} defaultValue={0} handleChange={(e) => handleChange(e)} />,
<EditWidgetNumberControl key={2} widget={widget} controlId={'customProperties.threshold_red'} label={'Threshold red'} defaultValue={0} handleChange={(e) => handleChange(e)} />,
<EditWidgetCheckboxControl key={3} widget={widget} controlId={'customProperties.horizontal'} input text="Horizontal" handleChange={e => handleChange(e)} />,
<EditWidgetCheckboxControl key={4} widget={widget} controlId={'customProperties.showOffset'} input text="showOffset" handleChange={e => handleChange(e)} />,
);
break;
default:
console.log('Non-valid widget type: ' + widgetType);
}

View file

@ -0,0 +1,74 @@
/**
* 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 {FormGroup, FormControl, FormLabel} from 'react-bootstrap';
class EditWidgetICControl extends React.Component {
constructor(props) {
super(props);
this.state = {
ics: [],
};
}
static getDerivedStateFromProps(props, state){
return {
ics: props.ics
};
}
handleICChange(e) {
let value = e.target.value === "Select IC" ? (-1) : (e.target.value);
this.props.handleChange({ target: { id: this.props.controlId, value: value } });
}
render() {
let parts = this.props.controlId.split('.');
let isCustomProperty = true;
if(parts.length === 1){
isCustomProperty = false;
}
let icOptions = [];
if (this.state.ics !== null && this.state.ics.length > 0){
icOptions.push(
<option key = {0} default>Select IC</option>
)
icOptions.push(this.state.ics.map((ic, index) => (
<option key={index+1} value={ic.id}>{ic.name}</option>
)))
} else {
icOptions = <option style={{ display: 'none' }}>No ics found</option>
}
return <div>
<FormGroup controlId="ic">
<FormLabel>IC</FormLabel>
<FormControl
as="select"
value={isCustomProperty ? this.props.widget[parts[0]][parts[1]] : this.props.widget[this.props.controlId]}
onChange={(e) => this.handleICChange(e)}>{icOptions} </FormControl>
</FormGroup>
</div>;
}
}
export default EditWidgetICControl;

View file

@ -141,6 +141,11 @@ class EditWidgetDialog extends React.Component {
customProperty ? changeObject[parts[0]][parts[1]]= 'default' : changeObject[e.target.id] = 'default';
}
changeObject = this.setMaxWidth(changeObject);
} else if (parts[1] === 'horizontal'){
customProperty ? changeObject[parts[0]][parts[1]] = e.target.value : changeObject[e.target.id] = e.target.value ;
let tempWidth = changeObject.width;
changeObject.width = changeObject.height;
changeObject.height = tempWidth;
} else {
customProperty ? changeObject[parts[0]][parts[1]] = e.target.value : changeObject[e.target.id] = e.target.value ;
}
@ -163,6 +168,7 @@ class EditWidgetDialog extends React.Component {
this.state.temporal,
this.props.sessionToken,
this.props.files,
this.props.ics,
this.props.signals,
(e) => this.handleChange(e));
}

View file

@ -0,0 +1,59 @@
/**
* 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 ArrayStore from '../common/array-store';
class WebsocketStore extends ArrayStore {
updateSocketStatus(state, socket) {
let checkInclusion = false;
state.forEach((element) => {
if (element.url === socket.url) {
element.connected = socket.connected;
checkInclusion = true;
}
})
if (!checkInclusion) {
state.push(socket);
}
this.__emitChange();
return state;
}
reduce(state, action) {
let tempSocket = {};
switch (action.type) {
case 'websocket/connected':
tempSocket.url = action.data;
tempSocket.connected = true;
return this.updateSocketStatus(state, tempSocket);
case 'websocket/connection-error':
tempSocket.url = action.data;
tempSocket.connected = false;
return this.updateSocketStatus(state, tempSocket);
default:
return super.reduce(state, action);
}
}
}
export default new WebsocketStore();

View file

@ -198,6 +198,19 @@ class WidgetFactory {
widget.customProperties.lockAspect = true;
break;
case 'TimeOffset':
widget.minWidth = 20;
widget.minHeight = 20;
widget.width = 100;
widget.height = 40;
widget.customProperties.threshold_yellow = 1;
widget.customProperties.threshold_red = 2;
widget.customProperties.icID = -1;
widget.customProperties.horizontal = true;
widget.customProperties.showOffset = true;
widget.customProperties.lockAspect = true;
break;
default:
widget.width = 100;
widget.height = 100;

View file

@ -164,6 +164,7 @@ class WidgetToolbox extends React.Component {
<ToolboxItem name='Lamp' type='widget' icon = 'plus' />
<ToolboxItem name='Gauge' type='widget' icon = 'plus'/>
<ToolboxItem name='Topology' type='widget' disabled={thereIsTopologyWidget} title={topologyItemMsg} icon = 'plus'/>
<ToolboxItem name='TimeOffset' type='widget' icon = 'plus' />
<OverlayTrigger key={0} placement={'bottom'} overlay={<Tooltip id={`tooltip-${"?"}`}> Drag and drop widgets onto the dashboard </Tooltip>} >
<Button disabled={true} variant="light" size="sm" key={0} >
<Icon icon="question" />

View file

@ -23,6 +23,8 @@ import ICDataStore from '../ic/ic-data-store';
import ConfigsStore from '../componentconfig/config-store';
import FileStore from '../file/file-store';
import SignalStore from '../signal/signal-store'
import WebsocketStore from './websocket-store'
import ICStore from '../ic/ic-store';
import WidgetCustomAction from './widgets/custom-action';
import WidgetAction from './widgets/action';
@ -39,6 +41,7 @@ import WidgetGauge from './widgets/gauge';
import WidgetBox from './widgets/box';
import WidgetTopology from './widgets/topology';
import WidgetLine from './widgets/line';
import WidgetTimeOffset from './widgets/time-offset'
//import WidgetHTML from './widgets/html';
@ -46,11 +49,14 @@ import '../styles/widgets.css';
class Widget extends React.Component {
static getStores() {
return [ ICDataStore, ConfigsStore, FileStore, SignalStore];
return [ ICDataStore, ConfigsStore, FileStore, SignalStore, WebsocketStore, ICStore];
}
static calculateState(prevState, props) {
let websockets = WebsocketStore.getState();
let ics = ICStore.getState();
let icData = {};
if (props.paused) {
@ -78,6 +84,8 @@ class Widget extends React.Component {
}
return {
ics: ics,
websockets: websockets,
icData: icData,
signals: signals,
icIDs: icIDs,
@ -223,6 +231,13 @@ class Widget extends React.Component {
widget={widget}
editing={this.props.editing}
/>
} else if (widget.type === 'TimeOffset') {
return <WidgetTimeOffset
widget={widget}
data={this.state.icData}
websockets={this.state.websockets}
ics={this.state.ics}
/>
}
return null;

View file

@ -0,0 +1,98 @@
/**
* 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};
}
if (props.data == null
|| props.data[state.icID] == null
|| props.data[state.icID].output == null
|| props.data[state.icID].output.timestamp == null) {
return {timeOffset: -1};
}
let ic = props.ics.find(ic => ic.id === parseInt(state.icID, 10));
let websocket = props.websockets.find(ws => ws.url === ic.websocketurl);
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: ic.name};
}
render() {
let icSelected = " ";
if(!this.state.websocketOpen){
icSelected = "no connection";
} 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>)
}
<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} height={this.props.widget.height}
RedOn={(this.props.widget.customProperties.threshold_red <= this.state.timeOffset) || !this.state.websocketOpen}
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 < 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;