diff --git a/package-lock.json b/package-lock.json index 56c4421..225cb05 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8870,6 +8870,11 @@ "minimist": "^1.2.5" } }, + "moment": { + "version": "2.27.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.27.0.tgz", + "integrity": "sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ==" + }, "move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", @@ -8902,6 +8907,15 @@ "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz", "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=" }, + "multiselect-react-dropdown": { + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/multiselect-react-dropdown/-/multiselect-react-dropdown-1.5.7.tgz", + "integrity": "sha512-bDlXYEzpV/5p5G5nIFRrZ/Ndf8CSYWliZ62n/5imfjLy0K2/dNx6sFmk4W/Phq83bzPUDK/RI4553yCk6YzZwg==", + "requires": { + "react": "^16.7.0", + "react-dom": "^16.7.0" + } + }, "mute-stream": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", diff --git a/package.json b/package.json index 0f50af1..31cf318 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,8 @@ "jszip": "^3.5.0", "libcimsvg": "git+https://git.rwth-aachen.de/acs/public/cim/pintura-npm-package.git", "lodash": "^4.17.15", + "moment": "^2.27.0", + "multiselect-react-dropdown": "^1.5.7", "node-sass": "^4.14.1", "popper.js": "^1.16.1", "prop-types": "^15.7.2", diff --git a/src/common/array-store.js b/src/common/array-store.js index 0c536f0..1c045e7 100644 --- a/src/common/array-store.js +++ b/src/common/array-store.js @@ -128,7 +128,12 @@ class ArrayStore extends ReduceStore { return super.reduce(state, action); case this.type + '/start-edit': + if(action.id){ + this.dataManager.update(action.data, action.token,action.param,action.id); + } + else{ this.dataManager.update(action.data, action.token,action.param); + } return state; case this.type + '/edited': diff --git a/src/common/data-managers/rest-data-manager.js b/src/common/data-managers/rest-data-manager.js index fb23eff..7727a4e 100644 --- a/src/common/data-managers/rest-data-manager.js +++ b/src/common/data-managers/rest-data-manager.js @@ -68,7 +68,11 @@ class RestDataManager { } } case 'remove/update': - if(param === null){ + if(id !== null){ + return this.makeURL(this.url + '/' + id); + + } + else if(param === null){ return this.makeURL(this.url + '/' + object.id); } else{ @@ -201,11 +205,11 @@ class RestDataManager { }); } - update(object, token = null, param = null) { + update(object, token = null, param = null, id = null) { var obj = {}; obj[this.type] = this.filterKeys(object); - RestAPI.put(this.requestURL('remove/update',null,param,object), obj, token).then(response => { + RestAPI.put(this.requestURL('remove/update',id,param,object), obj, token).then(response => { AppDispatcher.dispatch({ type: this.type + 's/edited', data: response[this.type] diff --git a/src/common/dialogs/dialog.js b/src/common/dialogs/dialog.js index 9b3a2c8..4cc15af 100644 --- a/src/common/dialogs/dialog.js +++ b/src/common/dialogs/dialog.js @@ -46,7 +46,7 @@ class Dialog extends React.Component { render() { return ( - + {this.props.title} diff --git a/src/common/table.js b/src/common/table.js index d68191a..87a52be 100644 --- a/src/common/table.js +++ b/src/common/table.js @@ -17,7 +17,7 @@ import React, { Component } from 'react'; import _ from 'lodash'; -import { Table, Button, FormControl, FormLabel, FormCheck } from 'react-bootstrap'; +import { Table, Button, FormControl, FormLabel, FormCheck, Tooltip, OverlayTrigger } from 'react-bootstrap'; import { Link } from 'react-router-dom'; import Icon from './icon'; @@ -103,11 +103,13 @@ class CustomTable extends Component { // add buttons if (child.props.editButton) { - cell.push(); + cell.push( Edit } > + ); } if (child.props.deleteButton) { - cell.push(); + cell.push( Delete } > + ); } if (child.props.checkbox) { @@ -117,7 +119,8 @@ class CustomTable extends Component { } if (child.props.exportButton) { - cell.push(); + cell.push( Export } > + ); } return cell; diff --git a/src/componentconfig/edit-config.js b/src/componentconfig/edit-config.js index a69159d..2e277b1 100644 --- a/src/componentconfig/edit-config.js +++ b/src/componentconfig/edit-config.js @@ -17,9 +17,9 @@ import React from 'react'; import {FormGroup, FormControl, FormLabel} from 'react-bootstrap'; +import { Multiselect } from 'multiselect-react-dropdown' import Dialog from '../common/dialogs/dialog'; import ParametersEditor from '../common/parameters-editor'; -import SelectFile from "../file/select-file"; class EditConfigDialog extends React.Component { valid = false; @@ -28,12 +28,11 @@ class EditConfigDialog extends React.Component { super(props); this.state = { - selectedFile: null, name: '', icID: '', configuration: null, startParameters: this.props.config.startParameters, - selectedFileID: this.props.config.selectedFileID + fileIDs: this.props.config.fileIDs }; } @@ -52,9 +51,13 @@ class EditConfigDialog extends React.Component { if(this.state.startParameters !== {} && JSON.stringify(this.props.config.startParameters) !== JSON.stringify(this.state.startParameters)){ data.startParameters = this.state.startParameters; } - if (parseInt(this.state.selectedFileID, 10) !== 0 && - this.props.config.selectedFileID !== parseInt(this.state.selectedFileID)) { - data.selectedFileID = parseInt(this.state.selectedFileID, 10); + + let IDs = [] + for(let e of this.state.fileIDs){ + IDs.push(e.id) + } + if (JSON.stringify(IDs) !== JSON.stringify(this.props.config.fileIDs)){ + data.fileIDs = IDs; } //forward modified config to callback function @@ -79,10 +82,19 @@ class EditConfigDialog extends React.Component { this.valid = this.isValid() } - handleSelectedFileChange(event){ - //console.log("Config file change to: ", event.target.value); - this.setState({selectedFileID: event.target.value}) + onFileSelect(selectedList, selectedItem) { + this.setState({ + fileIDs: selectedList + }) + this.valid = this.isValid() + } + + onFileRemove(selectedList, removedItem) { + + this.setState({ + fileIDs: selectedList + }) this.valid = this.isValid() } @@ -91,9 +103,7 @@ class EditConfigDialog extends React.Component { return this.state.name !== '' || this.state.icID !== '' || this.state.startParameters !== {} - || this.state.selectedFile != null || this.state.configuration != null - || this.state.selectedFileID !== 0; } resetState() { @@ -105,6 +115,15 @@ class EditConfigDialog extends React.Component { ); + let configFileOptions = []; + for(let file of this.props.files) { + configFileOptions.push( + {name: file.name, id: file.id} + ); + } + + + return ( this.onClose(c)} onReset={() => this.resetState()} valid={this.valid}>
@@ -121,14 +140,14 @@ class EditConfigDialog extends React.Component { - this.handleSelectedFileChange(e)} - files={this.props.files} - value={this.state.selectedFileID} - scenarioID={this.props.config.scenarioID} - sessionToken={this.props.sessionToken} + this.onFileSelect(selectedList, selectedItem)} + onRemove={(selectedList, removedItem) => this.onFileRemove(selectedList, removedItem)} + displayValue={'name'} + placeholder={'Select file(s)...'} /> diff --git a/src/dashboard/dashboard-button-group.js b/src/dashboard/dashboard-button-group.js index 8baeece..89289df 100644 --- a/src/dashboard/dashboard-button-group.js +++ b/src/dashboard/dashboard-button-group.js @@ -17,15 +17,23 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Button } from 'react-bootstrap'; +import { Button,OverlayTrigger, Tooltip } from 'react-bootstrap'; import Icon from "../common/icon"; class DashboardButtonGroup extends React.Component { render() { const buttonStyle = { - marginLeft: '8px' + marginLeft: '12px', + height: '44px', + width : '35px' }; + const iconStyle = { + color: '#007bff', + height: '25px', + width : '25px' + } + const buttons = []; let key = 0; @@ -35,46 +43,60 @@ class DashboardButtonGroup extends React.Component { if (this.props.editing) { buttons.push( - , - + , + Discard changes } > + + ); } else { if (this.props.fullscreen !== true) { buttons.push( - + ); } if (this.props.paused) { buttons.push( - + ); } else { buttons.push( - + ); } buttons.push( - + ); buttons.push( - + ); } diff --git a/src/dashboard/dashboard.js b/src/dashboard/dashboard.js index 05f69e1..584a4f2 100644 --- a/src/dashboard/dashboard.js +++ b/src/dashboard/dashboard.js @@ -73,7 +73,10 @@ class Dashboard extends Component { return thisWidgetHeight > maxHeightSoFar? thisWidgetHeight : maxHeightSoFar; }, 0); - if(dashboard.height === 0 || maxHeight + 80 > dashboard.height) + if(dashboard.height === 0){ + dashboard.height = 400; + } + else if(maxHeight + 80 > dashboard.height) { dashboard.height = maxHeight + 80; } @@ -112,7 +115,7 @@ class Dashboard extends Component { return ICused; }); } - + return { dashboard, widgets, @@ -125,7 +128,7 @@ class Dashboard extends Component { editing: prevState.editing || false, paused: prevState.paused || false, - editModal: false, + editModal: prevState.editModal || false, filesEditModal: prevState.filesEditModal || false, filesEditSaveState: prevState.filesEditSaveState || [], modalData: null, @@ -240,7 +243,6 @@ class Dashboard extends Component { closeEditFiles(){ this.setState({ filesEditModal: false }); - // TODO do we need this if the dispatches happen in the dialog? } closeEdit(data){ @@ -368,11 +370,12 @@ class Dashboard extends Component { return absolutHeight > currentHeight ? absolutHeight : currentHeight; }, 0); let dashboard = this.state.dashboard; + if(value === -1){ let tempHeight = this.state.dashboard.height - 50; - if(tempHeight > (maxHeight + 80)){ + if(tempHeight >= 400 && tempHeight >= (maxHeight + 80)){ dashboard.height = tempHeight; this.setState({dashboard}); } @@ -421,7 +424,7 @@ class Dashboard extends Component {
e.preventDefault() }> {this.state.editing && - + } {!draggable?( diff --git a/src/dashboard/edit-dashboard.js b/src/dashboard/edit-dashboard.js index 7de6012..900a6c8 100644 --- a/src/dashboard/edit-dashboard.js +++ b/src/dashboard/edit-dashboard.js @@ -21,14 +21,14 @@ import { FormGroup, FormControl, FormLabel } from 'react-bootstrap'; import Dialog from '../common/dialogs/dialog'; class EditDashboardDialog extends React.Component { - valid: false; + valid = true; constructor(props) { super(props); this.state = { name: '', - _id: '' + id: '' } } @@ -49,7 +49,7 @@ class EditDashboardDialog extends React.Component { resetState() { this.setState({ name: this.props.dashboard.name, - _id: this.props.dashboard._id + id: this.props.dashboard.id }); } diff --git a/src/file/edit-file-name.js b/src/file/edit-file-name.js new file mode 100644 index 0000000..05481b3 --- /dev/null +++ b/src/file/edit-file-name.js @@ -0,0 +1,79 @@ +/** + * 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 {FormGroup, FormControl, FormLabel, Col} from 'react-bootstrap'; + +import Dialog from '../common/dialogs/dialog'; + +class EditFileName extends React.Component { + valid = true; + + constructor(props) { + super(props); + + this.state = { + file: {}, + + }; + } + + onClose = canceled => { + if (canceled) { + if (this.props.onClose != null) { + this.props.onClose(); + } + + return; + } + + if (this.valid && this.props.onClose != null) { + this.props.onClose(this.state.file); + } + }; + + handleChange = event => { + this.props.file.name = event.target.value; + this.setState({file: this.props.file}); + + let name = true; + + if (this.state.name === '') { + name = false; + } + + this.valid = name; + + }; + + + + render() { + return this.onClose(c)} valid={true}> + + + Name + + + + + + ; + } +} + +export default EditFileName; diff --git a/src/file/edit-files.js b/src/file/edit-files.js index 4f3c8a7..3e2e003 100644 --- a/src/file/edit-files.js +++ b/src/file/edit-files.js @@ -21,6 +21,7 @@ import Dialog from '../common/dialogs/dialog'; import AppDispatcher from "../common/app-dispatcher"; import Table from "../common/table"; import TableColumn from "../common/table-column"; +import EditFileName from "./edit-file-name"; class EditFilesDialog extends React.Component { @@ -32,7 +33,9 @@ class EditFilesDialog extends React.Component { this.state = { uploadFile: null, - uploadProgress: 0 + uploadProgress: 0, + editModal: false, + modalFile: {} }; } @@ -84,6 +87,22 @@ class EditFilesDialog extends React.Component { }; + closeEditModal(data){ + if(data !== {} || data !== "undefined"){ + const formData = new FormData(); + formData.append("object", data); + + AppDispatcher.dispatch({ + type: 'files/start-edit', + data: formData, + token: this.props.sessionToken, + id: data.id + }); + } + + this.setState({editModal: false}); + } + deleteFile(index){ let file = this.props.files[index] @@ -114,24 +133,24 @@ class EditFilesDialog extends React.Component { marginTop: '-40px' }; + return ( - this.onClose(c)} blendOutCancel = {true} valid={true}> + this.onClose(c)} blendOutCancel = {true} valid={true} size = 'lg'>
-
- - - - - +
+ + + + this.deleteFile(index)} + editButton + onEdit={index => this.setState({ editModal: true, modalFile: this.props.files[index] })} />
-
-
+
+ + this.closeEditModal(data)} file={this.state.modalFile} /> + +
+ + ); } } diff --git a/src/ic/edit-ic.js b/src/ic/edit-ic.js index e91c8ef..23cdfc1 100644 --- a/src/ic/edit-ic.js +++ b/src/ic/edit-ic.js @@ -78,6 +78,10 @@ class EditICDialog extends React.Component { this.setState({ [e.target.id]: e.target.value }); } + handlePropertiesChange(data) { + this.setState({ properties: data }); + } + resetState() { this.setState({ name: this.props.ic.name, @@ -91,8 +95,17 @@ class EditICDialog extends React.Component { render() { return ( - this.onClose(c)} onReset={() => this.resetState()} valid={this.valid}> + this.onClose(c)} + onReset={() => this.resetState()} + valid={this.valid} + size='lg' + >
+ UUID: {this.props.ic.uuid} Name this.handleChange(e)} /> @@ -120,7 +133,11 @@ class EditICDialog extends React.Component { Properties - + this.handlePropertiesChange(data)} + />
diff --git a/src/ic/ic-action.js b/src/ic/ic-action.js index 6a9e591..80c60e0 100644 --- a/src/ic/ic-action.js +++ b/src/ic/ic-action.js @@ -16,7 +16,7 @@ ******************************************************************************/ import React from 'react'; -import { Button, ButtonToolbar, DropdownButton, DropdownItem } from 'react-bootstrap'; +import { Button, ButtonToolbar, DropdownButton, Dropdown, Tooltip, OverlayTrigger } from 'react-bootstrap'; class ICAction extends React.Component { constructor(props) { @@ -48,21 +48,34 @@ class ICAction extends React.Component { }; render() { + let showTooltip = this.state.selectedAction.id === '-1'; + const actionList = this.props.actions.map(action => ( - + {action.title} - + )); return
- Send command to infrastructure component + {showTooltip ? + + Select command for infrastructure component } > + + {actionList} + + + Send command to infrastructure component } > + + + + : {actionList} - + }
; } } diff --git a/src/ic/ics.js b/src/ic/ics.js index 5cd7719..a7a0f2b 100644 --- a/src/ic/ics.js +++ b/src/ic/ics.js @@ -20,6 +20,7 @@ import { Container } from 'flux/utils'; import { Button } from 'react-bootstrap'; import FileSaver from 'file-saver'; import _ from 'lodash'; +import moment from 'moment' import AppDispatcher from '../common/app-dispatcher'; import InfrastructureComponentStore from './ic-store'; @@ -256,10 +257,10 @@ class InfrastructureComponents extends Component { return style.join(' ') } - static stateUpdateModifier(updatedAt) { - const date = new Date(updatedAt); - - return date.toLocaleString('de-DE'); + stateUpdateModifier(updatedAt) { + let dateFormat = 'ddd, DD MMM YYYY HH:mm:ss'; + let dateTime = moment.utc(updatedAt, dateFormat); + return dateTime.toLocaleString('de-DE'); } render() { @@ -279,9 +280,9 @@ class InfrastructureComponents extends Component { {/* */} - + - + this.stateUpdateModifier(stateUpdateAt)} /> { + tempFiles.push({ + id: file.id, + name: file.name + }); + }) + this.setState({filesEditModal: true, filesEditSaveState: tempFiles}); + } + + closeEditFiles(){ + this.setState({ filesEditModal: false }); + // TODO do we need this if the dispatches happen in the dialog? + } signalsAutoConf(index){ let componentConfig = this.state.configs[index]; @@ -438,12 +481,57 @@ class Scenario extends React.Component { * File modification methods ############################################## */ - getFileName(id) { - for (let file of this.state.files) { - if (file.id === id) { - return file.name; + getListOfFiles(fileIDs, types) { + + let fileList = ''; + + for (let id of fileIDs){ + for (let file of this.state.files) { + if (file.id === id && types.some(e => file.type.includes(e))) { + if (fileList === ''){ + fileList = file.name + } else { + fileList = fileList + ';' + file.name; + } + } } } + + + + + return fileList; + } + + startPintura(configIndex){ + let config = this.state.configs[configIndex]; + + // get xml / CIM file + let files = [] + for (let id of config.fileIDs){ + for (let file of this.state.files) { + if (file.id === id && ['xml'].some(e => file.type.includes(e))) { + files.push(file); + } + } + } + + if(files.length > 1){ + // more than one CIM file... + console.warn("There is more than one CIM file selected in this component configuration. I will open them all in a separate tab.") + } + + let base_host = 'aaa.bbb.ccc.ddd/api/v2/files/' + for (let file of files) { + // endpoint param serves for download and upload of CIM file, token is required for authentication + let params = { + token: this.state.sessionToken, + endpoint: base_host + String(file.id), + } + + // TODO start Pintura for editing CIM/ XML file from here + console.warn("Starting Pintura... and nothing happens so far :-) ", params) + } } /* ############################################## @@ -460,9 +548,31 @@ class Scenario extends React.Component { paddingTop: '30px' } + const iconStyle = { + color: '#007bff', + height: '25px', + width : '25px' + } + return
+
+ Add, edit or delete files of scenario } > + + +

{this.state.scenario.name}

+ + {/*Component Configurations table*/} @@ -470,7 +580,14 @@ class Scenario extends React.Component { this.onConfigChecked(index, event)} width='30' /> - this.getFileName(selectedFileID)} /> + this.getListOfFiles(fileIDs, ['json', 'JSON'])} /> + this.getListOfFiles(fileIDs, ['xml'])} + editButton + onEdit={(index) => this.startPintura(index)} + /> this.getICName(icID)} /> this.setState({ dashboardEditModal: true, modalDashboardData: this.state.dashboards[index] })} onDelete={(index) => this.setState({ deleteDashboardModal: true, modalDashboardData: this.state.dashboards[index], modalDashboardIndex: index })} onExport={index => this.exportDashboard(index)} /> @@ -573,6 +692,7 @@ class Scenario extends React.Component {
this.closeNewDashboardModal(data)} /> + this.closeEditDashboardModal(data)} /> this.closeImportDashboardModal(data)} /> this.closeDeleteDashboardModal(e)} /> @@ -595,7 +715,9 @@ class Scenario extends React.Component { this.userToAdd = e.target.value} + onChange={(e) => this.onUserInputChange(e)} + value={this.state.userToAdd} + type="text" /> +
@@ -65,13 +93,13 @@ class WidgetToolbox extends React.Component {
Increase dashboard height } > - Decrease dashboard height } > -
diff --git a/src/widget/widget.js b/src/widget/widget.js index e2faeb1..7b647db 100644 --- a/src/widget/widget.js +++ b/src/widget/widget.js @@ -43,6 +43,7 @@ import WidgetGauge from './widgets/gauge'; import WidgetBox from './widgets/box'; import WidgetHTML from './widgets/html'; import WidgetTopology from './widgets/topology'; +import WidgetLine from './widgets/line'; import '../styles/widgets.css'; @@ -221,6 +222,11 @@ class Widget extends React.Component { files={this.state.files} token={this.state.sessionToken} /> + } else if (widget.type === 'Line') { + return } return null; diff --git a/src/widget/widgets/gauge.js b/src/widget/widgets/gauge.js index 3aa3e3b..629784e 100644 --- a/src/widget/widgets/gauge.js +++ b/src/widget/widgets/gauge.js @@ -81,114 +81,116 @@ class WidgetGauge extends Component { // get the signal with the selected signal ID let signalID = props.widget.signalIDs[0]; let signal = props.signals.filter(s => s.id === signalID) - // determine ID of infrastructure component related to signal[0] (there is only one signal for a lamp widget) - let icID = props.icIDs[signal[0].id]; - let returnState = {} + if(signal.length > 0) { + // determine ID of infrastructure component related to signal[0] (there is only one signal for a lamp widget) + let icID = props.icIDs[signal[0].id]; - returnState["colorZones"] = props.widget.customProperties.zones; + let returnState = {} - if(signalID){ - returnState["signalID"] = signalID; - } - // Update unit (assuming there is exactly one signal for this widget) - if(signal !== undefined){ - returnState["unit"] = signal[0].unit; - } + returnState["colorZones"] = props.widget.customProperties.zones; - // update value + if (signalID) { + returnState["signalID"] = signalID; + } + // Update unit (assuming there is exactly one signal for this widget) + if (signal !== undefined) { + returnState["unit"] = signal[0].unit; + } - // check if data available - if (props.data == null - || props.data[icID] == null - || props.data[icID].output == null - || props.data[icID].output.values == null) { - return{ value: 0, minValue: 0, maxValue: 10}; - } + // update value - // memorize if min or max value is updated - let updateValue = false; - let updateMinValue = false; - let updateMaxValue = false; + // check if data available + if (props.data == null + || props.data[icID] == null + || props.data[icID].output == null + || props.data[icID].output.values == null) { + return {value: 0, minValue: 0, maxValue: 10}; + } - // check if value has changed - const data = props.data[icID].output.values[signal[0].index-1]; - // Take just 3 decimal positions - // Note: Favor this method over Number.toFixed(n) in order to avoid a type conversion, since it returns a String - if (data != null) { - const value = signal[0].scalingFactor * Math.round(data[data.length - 1].y * 1e3) / 1e3; - let minValue = null; - let maxValue = null; + // memorize if min or max value is updated + let updateValue = false; + let updateMinValue = false; + let updateMaxValue = false; - if ((state.value !== value && value != null) || props.widget.customProperties.valueUseMinMax || state.useMinMaxChange) { - //value has changed - updateValue = true; + // check if value has changed + const data = props.data[icID].output.values[signal[0].index - 1]; + // Take just 3 decimal positions + // Note: Favor this method over Number.toFixed(n) in order to avoid a type conversion, since it returns a String + if (data != null) { + const value = signal[0].scalingFactor * Math.round(data[data.length - 1].y * 1e3) / 1e3; + let minValue = null; + let maxValue = null; - // update min-max if needed - let updateLabels = false; + if ((state.value !== value && value != null) || props.widget.customProperties.valueUseMinMax || state.useMinMaxChange) { + //value has changed + updateValue = true; - minValue = state.minValue; - maxValue = state.maxValue; + // update min-max if needed + let updateLabels = false; - if (minValue == null || (!props.widget.customProperties.valueUseMinMax && (value < minValue || signalID !== state.signalID)) ||state.useMinMaxChange) { - minValue = value - 0.5; - updateLabels = true; - updateMinValue = true; - } + minValue = state.minValue; + maxValue = state.maxValue; - if (maxValue == null || (!props.widget.customProperties.valueUseMinMax && (value > maxValue || signalID !== state.signalID)) || state.useMinMaxChange) { - maxValue = value + 0.5; - updateLabels = true; - updateMaxValue = true; - returnState["useMinMaxChange"] = false; - } + if (minValue == null || (!props.widget.customProperties.valueUseMinMax && (value < minValue || signalID !== state.signalID)) || state.useMinMaxChange) { + minValue = value - 0.5; + updateLabels = true; + updateMinValue = true; + } - if (props.widget.customProperties.valueUseMinMax) { + if (maxValue == null || (!props.widget.customProperties.valueUseMinMax && (value > maxValue || signalID !== state.signalID)) || state.useMinMaxChange) { + maxValue = value + 0.5; + updateLabels = true; + updateMaxValue = true; + returnState["useMinMaxChange"] = false; + } + + if (props.widget.customProperties.valueUseMinMax) { minValue = props.widget.customProperties.valueMin; updateMinValue = true; maxValue = props.widget.customProperties.valueMax; updateMaxValue = true; updateLabels = true; - } - - if (updateLabels === false && state.gauge) { - // check if min/max changed - if (minValue > state.gauge.minValue) { - minValue = state.gauge.minValue; - updateMinValue = true; } - if (maxValue < state.gauge.maxValue) { - maxValue = state.gauge.maxValue; - updateMaxValue = true; + if (updateLabels === false && state.gauge) { + // check if min/max changed + if (minValue > state.gauge.minValue) { + minValue = state.gauge.minValue; + updateMinValue = true; + } + + if (maxValue < state.gauge.maxValue) { + maxValue = state.gauge.maxValue; + updateMaxValue = true; + } } } - } - if(props.widget.customProperties.valueUseMinMax !== state.useMinMax){ - returnState["useMinMax"] = props.widget.customProperties.valueUseMinMax; - } + if (props.widget.customProperties.valueUseMinMax !== state.useMinMax) { + returnState["useMinMax"] = props.widget.customProperties.valueUseMinMax; + } - // prepare returned state - if(updateValue === true){ - returnState["value"] = value; - } - if(updateMinValue === true){ - returnState["minValue"] = minValue; - } - if(updateMaxValue === true){ - returnState["maxValue"] = maxValue; - } - - if (returnState !== {}){ - return returnState; - } - else{ - return null; - } - } // if there is signal data + // prepare returned state + if (updateValue === true) { + returnState["value"] = value; + } + if (updateMinValue === true) { + returnState["minValue"] = minValue; + } + if (updateMaxValue === true) { + returnState["maxValue"] = maxValue; + } + if (returnState !== {}) { + return returnState; + } else { + return null; + } + } // if there is signal data + } + return null; } diff --git a/src/widget/widgets/input.js b/src/widget/widgets/input.js index 0b87617..142f4e2 100644 --- a/src/widget/widgets/input.js +++ b/src/widget/widgets/input.js @@ -42,11 +42,13 @@ class WidgetInput extends Component { value = Number(props.widget.customProperties.default_value) } - // 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 (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 !== ''){ diff --git a/src/widget/widgets/lamp.js b/src/widget/widgets/lamp.js index c0ca9a0..c6fd5e6 100644 --- a/src/widget/widgets/lamp.js +++ b/src/widget/widgets/lamp.js @@ -36,21 +36,24 @@ class WidgetLamp extends Component { // get the signal with the selected signal ID let signalID = props.widget.signalIDs[0]; let signal = props.signals.filter(s => s.id === signalID) - // determine ID of infrastructure component related to signal[0] (there is only one signal for a lamp widget) - let icID = props.icIDs[signal[0].id]; - // check if data available - if (props.data == null - || props.data[icID] == null - || props.data[icID].output == null - || props.data[icID].output.values == null) { - return{value:''}; - } + if(signal.length>0) { + // determine ID of infrastructure component related to signal[0] (there is only one signal for a lamp widget) + let icID = props.icIDs[signal[0].id]; - // check if value has changed - const data = props.data[icID].output.values[signal[0].index-1]; - if (data != null && Number(state.value) !== signal[0].scalingFactor * data[data.length - 1].y) { - return { value: signal[0].scalingFactor * data[data.length - 1].y }; + // check if data available + if (props.data == null + || props.data[icID] == null + || props.data[icID].output == null + || props.data[icID].output.values == null) { + return {value: ''}; + } + + // check if value has changed + const data = props.data[icID].output.values[signal[0].index - 1]; + if (data != null && Number(state.value) !== signal[0].scalingFactor * data[data.length - 1].y) { + return {value: signal[0].scalingFactor * data[data.length - 1].y}; + } } return null; diff --git a/src/widget/widgets/line.js b/src/widget/widgets/line.js new file mode 100644 index 0000000..680ca03 --- /dev/null +++ b/src/widget/widgets/line.js @@ -0,0 +1,38 @@ +/** + * 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 EditWidgetColorControl from '../edit-widget/edit-widget-color-control'; + +class WidgetLine extends Component { + render() { + const lineStyle = { + borderColor: EditWidgetColorControl.ColorPalette[this.props.widget.customProperties.border_color], + transform: 'rotate(' + this.props.widget.customProperties.rotation + 'deg)', + borderWidth: '' + this.props.widget.customProperties.border_width + 'px' + }; + + return ( +
+ { } +
+ ); + } +} + +export default WidgetLine; diff --git a/src/widget/widgets/slider.js b/src/widget/widgets/slider.js index 10bb055..8e7e2ff 100644 --- a/src/widget/widgets/slider.js +++ b/src/widget/widgets/slider.js @@ -53,10 +53,12 @@ class WidgetSlider extends Component { } // 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 (props.widget.signalIDs.length > 0) { + let signalID = props.widget.signalIDs[0]; + let signal = props.signals.find(sig => sig.id === signalID); + if (signal !== undefined) { + unit = signal.unit; + } } if (unit !== '' && value !== ''){ diff --git a/src/widget/widgets/topology.js b/src/widget/widgets/topology.js index 8aad399..1eb64fc 100644 --- a/src/widget/widgets/topology.js +++ b/src/widget/widgets/topology.js @@ -65,19 +65,12 @@ function show(element) { if(element !== undefined) { element.style.visibility = 'inherit'; } - else{ - console.log("MouseOver, show, element undefined.") - } } function hide(element) { if (element !== undefined) { element.style.visibility = 'hidden'; - } else { - console.log("MouseLeave, hide, element undefined.") } - - } // De-initialize functions @@ -91,25 +84,45 @@ class WidgetTopology extends React.Component { super(props); this.svgElem = React.createRef(); this.Viewer = null; - this.dashboardState = 'initial' - this.message = '' - let file = this.props.files.find(file => file.id === parseInt(this.props.widget.customProperties.file, 10)); this.state = { - file: file + file: this.props.files.find(file => file.id === parseInt(this.props.widget.customProperties.file, 10)), + dashboardState: 'initial', + message: '' }; } static getDerivedStateFromProps(props, state){ + // find the selected file of the widget, is undefined if no file is selected let file = props.files.find(file => file.id === parseInt(props.widget.customProperties.file, 10)); - if (state.file === undefined || state.file.id !== file.id) { - return{ - file: file - }; + let dashboardState = state.dashboardState; + let message = state.message; + + if(file === undefined){ + dashboardState = 'show_message'; + message = 'Select a topology model.' + } else if (!file.hasOwnProperty('data') && dashboardState === 'show_message'){ + // data of file is missing, start download + dashboardState = 'loading'; + message = ''; + AppDispatcher.dispatch({ + type: 'files/start-download', + data: file.id, + token: props.token + }); + } else if (file.hasOwnProperty('data') && (dashboardState === 'loading'|| dashboardState === 'show_message')){ + //file is available set state to ready + dashboardState = 'ready' + message = ''; } - return null + + return{ + file: file, + dashboardState:dashboardState, + message: message, + }; } componentDidMount() { @@ -124,13 +137,12 @@ class WidgetTopology extends React.Component { //this.Viewer.fitToViewer(); - // Query the file referenced by the widget - let widgetFile = parseInt(this.props.widget.customProperties.file, 10); - if (widgetFile !== -1 && this.state.file === undefined) { - this.dashboardState = 'loading'; + // Query the file referenced by the widget (if any) + if (this.state.file !== undefined) { + this.setState({dashboardState: 'loading'}); AppDispatcher.dispatch({ type: 'files/start-download', - data: widgetFile, + data: this.state.file.id, token: this.props.token }); } @@ -142,43 +154,25 @@ class WidgetTopology extends React.Component { componentDidUpdate(prevProps: Readonly

, prevState: Readonly, snapshot: SS): void { - if(this.state.file === undefined) { - // No file has been selected - this.dashboardState = 'show_message'; - this.message = 'Select a topology model first.'; - return; - } - - if((prevState.file === undefined && this.state.file !== undefined) - || (this.state.file.id !== prevState.file.id && this.state.file.id !== -1)) { - // if file has changed, download new file - this.dashboardState = 'loading'; - AppDispatcher.dispatch({ - type: 'files/start-download', - data: this.state.file.id, - token: this.props.token - }); - } else if (this.state.file.hasOwnProperty("data") && this.dashboardState === 'loading') { - // data of file has been newly downloaded (did not exist in previous state) - this.dashboardState = 'ready'; - - } else if(this.state.file.hasOwnProperty("data") && this.dashboardState === 'ready'){ + if(this.state.dashboardState === 'ready'){ + //Topology file incl data downloaded, init SVG (should happen only once!) if (this.svgElem) { let cimsvgInstance = new cimsvg(this.svgElem.current); cimsvg.setCimsvg(cimsvgInstance); cimsvgInstance.setFileCount(1); // transform data blob into string format this.state.file.data.text().then(function(content) { - cimsvgInstance.loadFile(content); - cimsvgInstance.fit(); - attachComponentEvents(); + cimsvgInstance.loadFile(content); + cimsvgInstance.fit(); + attachComponentEvents(); }); + this.setState({dashboardState: 'loaded'}); } else { console.error("The svgElem variable is not initialized before the attempt to create the cimsvg instance."); } - } + } } render() { @@ -192,11 +186,11 @@ class WidgetTopology extends React.Component { position: "right" } - switch(this.dashboardState) { + switch(this.state.dashboardState) { case 'loading': markup =

; break; case 'show_message': - markup =
{ this.message }
; break; + markup =
{ this.state.message }
; break; default: markup = (
s.id === signalID) - // 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]; + 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 = '' - if (props.data == null || props.data[icID] == null || props.data[icID].output == null || props.data[icID].output.values == null) { - value = ''; - } else { - // check if value has changed - const data = props.data[icID].output.values[signal[0].index - 1]; - if (data != null && Number(state.value) !== data[data.length - 1].y) { - value = signal[0].scalingFactor * data[data.length - 1].y; + // check if data available + let value = '' + if (props.data == null || props.data[icID] == null || props.data[icID].output == null || props.data[icID].output.values == null) { + value = ''; + } else { + // check if value has changed + const data = props.data[icID].output.values[signal[0].index - 1]; + if (data != null && Number(state.value) !== data[data.length - 1].y) { + value = signal[0].scalingFactor * data[data.length - 1].y; + } } + + // Update unit (assuming there is exactly one signal for this widget) + let unit = ''; + if (signal !== undefined) { + unit = signal[0].unit; + } + + return { + value: value, + unit: unit, + }; } - // Update unit (assuming there is exactly one signal for this widget) - let unit = ''; - if(signal !== undefined){ - unit = signal[0].unit; - } - - return { - value: value, - unit: unit, - }; + return null; }