mirror of
https://git.rwth-aachen.de/acs/public/villas/web/
synced 2025-03-30 00:00:13 +01:00
597 lines
18 KiB
JavaScript
597 lines
18 KiB
JavaScript
/**
|
|
* 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 { Container } from 'flux/utils';
|
|
import Fullscreenable from 'react-fullscreenable';
|
|
import classNames from 'classnames';
|
|
|
|
import EditWidget from '../widget/edit-widget/edit-widget';
|
|
import EditFiles from '../file/edit-files';
|
|
import EditSignalMapping from "../signal/edit-signal-mapping";
|
|
import WidgetContextMenu from '../widget/widget-context-menu';
|
|
import WidgetToolbox from '../widget/widget-toolbox';
|
|
import WidgetArea from '../widget/widget-area';
|
|
import DashboardButtonGroup from './dashboard-button-group';
|
|
|
|
import DashboardStore from './dashboard-store';
|
|
import SignalStore from '../signal/signal-store'
|
|
import FileStore from '../file/file-store';
|
|
import WidgetStore from '../widget/widget-store';
|
|
import ICStore from '../ic/ic-store'
|
|
import ConfigStore from '../componentconfig/config-store'
|
|
import AppDispatcher from '../common/app-dispatcher';
|
|
|
|
import 'react-contexify/dist/ReactContexify.min.css';
|
|
import WidgetContainer from '../widget/widget-container';
|
|
import EditableWidgetContainer from '../widget/editable-widget-container';
|
|
|
|
class Dashboard extends Component {
|
|
|
|
static lastWidgetKey = 0;
|
|
static webSocketsOpened = false;
|
|
static getStores() {
|
|
return [DashboardStore, FileStore, WidgetStore, SignalStore, ConfigStore, ICStore];
|
|
}
|
|
|
|
static calculateState(prevState, props) {
|
|
if (prevState == null) {
|
|
prevState = {};
|
|
}
|
|
|
|
const sessionToken = localStorage.getItem("token");
|
|
|
|
let dashboard = DashboardStore.getState().find(d => d.id === parseInt(props.match.params.dashboard, 10));
|
|
if (dashboard == null) {
|
|
AppDispatcher.dispatch({
|
|
type: 'dashboards/start-load',
|
|
data: props.match.params.dashboard,
|
|
token: sessionToken
|
|
});
|
|
}
|
|
|
|
// obtain all widgets of this dashboard
|
|
let widgets = WidgetStore.getState().filter(w => w.dashboardID === parseInt(props.match.params.dashboard, 10));
|
|
|
|
// compute max y coordinate
|
|
let maxHeight = null;
|
|
maxHeight = Object.keys(widgets).reduce((maxHeightSoFar, widgetKey) => {
|
|
let thisWidget = widgets[widgetKey];
|
|
let thisWidgetHeight = thisWidget.y + thisWidget.height;
|
|
|
|
return thisWidgetHeight > maxHeightSoFar ? thisWidgetHeight : maxHeightSoFar;
|
|
}, 0);
|
|
|
|
|
|
// filter component configurations to the ones that belong to this scenario
|
|
let configs = [];
|
|
let files = [];
|
|
if (dashboard !== undefined) {
|
|
configs = ConfigStore.getState().filter(config => config.scenarioID === dashboard.scenarioID);
|
|
files = FileStore.getState().filter(file => file.scenarioID === dashboard.scenarioID);
|
|
if (dashboard.height === 0) {
|
|
dashboard.height = 400;
|
|
}
|
|
else if (maxHeight + 80 > dashboard.height) {
|
|
dashboard.height = maxHeight + 80;
|
|
}
|
|
}
|
|
|
|
// filter signals to the ones belonging to the scenario at hand
|
|
let signals = []
|
|
let allSignals = SignalStore.getState();
|
|
let sig, con;
|
|
for (sig of allSignals) {
|
|
for (con of configs) {
|
|
if (sig.configID === con.id) {
|
|
signals.push(sig);
|
|
}
|
|
}
|
|
}
|
|
|
|
// filter ICs to the ones used by this scenario
|
|
let ics = []
|
|
if (configs.length > 0) {
|
|
ics = ICStore.getState().filter(ic => {
|
|
let ICused = false;
|
|
for (let config of configs) {
|
|
if (ic.id === config.icID) {
|
|
ICused = true;
|
|
break;
|
|
}
|
|
}
|
|
return ICused;
|
|
});
|
|
}
|
|
|
|
let editOutputSignalsModal = prevState.editOutputSignalsModal;
|
|
let editInputSignalsModal = prevState.editInputSignalsModal;
|
|
|
|
return {
|
|
dashboard,
|
|
widgets,
|
|
signals,
|
|
sessionToken,
|
|
files,
|
|
configs,
|
|
ics,
|
|
|
|
editing: prevState.editing || false,
|
|
paused: prevState.paused || false,
|
|
|
|
editModal: prevState.editModal || false,
|
|
editOutputSignalsModal: editOutputSignalsModal,
|
|
editInputSignalsModal: editInputSignalsModal,
|
|
filesEditModal: prevState.filesEditModal || false,
|
|
filesEditSaveState: prevState.filesEditSaveState || [],
|
|
modalData: null,
|
|
modalIndex: null,
|
|
widgetChangeData: [],
|
|
widgetOrigIDs: prevState.widgetOrigIDs || [],
|
|
|
|
maxWidgetHeight: maxHeight || null,
|
|
};
|
|
|
|
}
|
|
|
|
static getNewWidgetKey() {
|
|
const widgetKey = this.lastWidgetKey;
|
|
this.lastWidgetKey++;
|
|
|
|
return widgetKey;
|
|
}
|
|
|
|
componentDidMount() {
|
|
|
|
Dashboard.webSocketsOpened = false;
|
|
|
|
// load widgets of dashboard
|
|
AppDispatcher.dispatch({
|
|
type: 'widgets/start-load',
|
|
token: this.state.sessionToken,
|
|
param: '?dashboardID=' + parseInt(this.props.match.params.dashboard, 10),
|
|
});
|
|
|
|
// load ICs to enable that component configs and dashboards work with them
|
|
AppDispatcher.dispatch({
|
|
type: 'ics/start-load',
|
|
token: this.state.sessionToken
|
|
});
|
|
|
|
}
|
|
|
|
componentDidUpdate(prevProps: Readonly<P>, prevState: Readonly<S>, snapshot: SS) {
|
|
// open web sockets if ICs are already known and sockets are not opened yet
|
|
if (this.state.ics !== undefined && !Dashboard.webSocketsOpened) {
|
|
if (this.state.ics.length > 0) {
|
|
console.log("Starting to open IC websockets:", this.state.ics);
|
|
AppDispatcher.dispatch({
|
|
type: 'ics/open-sockets',
|
|
data: this.state.ics
|
|
});
|
|
|
|
Dashboard.webSocketsOpened = true;
|
|
}
|
|
}
|
|
|
|
|
|
if (prevState.dashboard === undefined && this.state.dashboard !== undefined) {
|
|
// the dashboard was loaded, so that the scenarioID is available
|
|
|
|
// load configs of scenario
|
|
AppDispatcher.dispatch({
|
|
type: 'configs/start-load',
|
|
token: this.state.sessionToken,
|
|
param: '?scenarioID=' + this.state.dashboard.scenarioID
|
|
});
|
|
|
|
// load files of scenario
|
|
AppDispatcher.dispatch({
|
|
type: 'files/start-load',
|
|
param: '?scenarioID=' + this.state.dashboard.scenarioID,
|
|
token: this.state.sessionToken
|
|
});
|
|
}
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
// close web sockets of ICs
|
|
console.log("Starting to close all web sockets");
|
|
AppDispatcher.dispatch({
|
|
type: 'ics/close-sockets',
|
|
});
|
|
}
|
|
|
|
handleKeydown(e) {
|
|
switch (e.key) {
|
|
case ' ':
|
|
case 'p':
|
|
this.setState({ paused: !this.state.paused });
|
|
break;
|
|
case 'e':
|
|
this.setState({ editing: !this.state.editing });
|
|
break;
|
|
case 'f':
|
|
this.props.toggleFullscreen();
|
|
break;
|
|
default:
|
|
}
|
|
}
|
|
|
|
|
|
transformToWidgetsList(widgets) {
|
|
return Object.keys(widgets).map((key) => widgets[key]);
|
|
}
|
|
|
|
handleDrop(widget) {
|
|
widget.dashboardID = this.state.dashboard.id;
|
|
|
|
AppDispatcher.dispatch({
|
|
type: 'widgets/start-add',
|
|
token: this.state.sessionToken,
|
|
data: widget
|
|
});
|
|
|
|
|
|
};
|
|
|
|
widgetChange(widget, index, callback = null) {
|
|
let temp = this.state.widgetChangeData;
|
|
temp.push(widget);
|
|
this.setState({ widgetChangeData: temp });
|
|
|
|
}
|
|
|
|
|
|
|
|
editWidget(widget, index) {
|
|
this.setState({ editModal: true, modalData: widget, modalIndex: index });
|
|
};
|
|
|
|
duplicateWidget(widget) {
|
|
let widgetCopy = JSON.parse(JSON.stringify(widget));
|
|
delete widgetCopy.id;
|
|
widgetCopy.x = widgetCopy.x + 50;
|
|
widgetCopy.y = widgetCopy.y + 50;
|
|
|
|
AppDispatcher.dispatch({
|
|
type: 'widgets/start-add',
|
|
token: this.state.sessionToken,
|
|
data: widgetCopy
|
|
});
|
|
};
|
|
|
|
startEditFiles() {
|
|
let tempFiles = [];
|
|
this.state.files.forEach(file => {
|
|
tempFiles.push({
|
|
id: file.id,
|
|
name: file.name
|
|
});
|
|
})
|
|
this.setState({ filesEditModal: true, filesEditSaveState: tempFiles });
|
|
}
|
|
|
|
closeEditFiles() {
|
|
this.state.widgets.map(widget => {
|
|
if(widget.type === "Image"){
|
|
widget.customProperties.update = true;
|
|
}
|
|
})
|
|
this.setState({ filesEditModal: false });
|
|
}
|
|
|
|
closeEdit(data) {
|
|
|
|
if (data == null) {
|
|
|
|
AppDispatcher.dispatch({
|
|
type: 'widgets/start-load',
|
|
token: this.state.sessionToken,
|
|
param: '?dashboardID=' + this.state.dashboard.id
|
|
});
|
|
|
|
this.setState({ editModal: false });
|
|
|
|
return;
|
|
}
|
|
|
|
if(data.type === "Image")
|
|
{
|
|
data.customProperties.update = true;
|
|
}
|
|
|
|
AppDispatcher.dispatch({
|
|
type: 'widgets/start-edit',
|
|
token: this.state.sessionToken,
|
|
data: data
|
|
});
|
|
|
|
this.setState({ editModal: false });
|
|
};
|
|
|
|
|
|
deleteWidget(widget, index) {
|
|
|
|
AppDispatcher.dispatch({
|
|
type: 'widgets/start-remove',
|
|
data: widget,
|
|
token: this.state.sessionToken
|
|
});
|
|
};
|
|
|
|
|
|
startEditing() {
|
|
let originalIDs = [];
|
|
this.state.widgets.forEach(widget => {
|
|
originalIDs.push(widget.id);
|
|
if (widget.type === 'Slider' || widget.type === 'NumberInput' || widget.type === 'Button') {
|
|
AppDispatcher.dispatch({
|
|
type: 'widgets/start-edit',
|
|
token: this.state.sessionToken,
|
|
data: widget
|
|
});
|
|
}
|
|
else if (widget.type === 'Image'){
|
|
widget.customProperties.update = true;
|
|
}
|
|
});
|
|
this.setState({ editing: true, widgetOrigIDs: originalIDs });
|
|
};
|
|
|
|
saveEditing() {
|
|
this.state.widgets.forEach(widget => {
|
|
if (widget.type === 'Image'){
|
|
widget.customProperties.update = true;
|
|
}
|
|
});
|
|
// Provide the callback so it can be called when state change is applied
|
|
// TODO: Check if callback is needed
|
|
AppDispatcher.dispatch({
|
|
type: 'dashboards/start-edit',
|
|
data: this.state.dashboard,
|
|
token: this.state.sessionToken
|
|
});
|
|
|
|
this.state.widgetChangeData.forEach(widget => {
|
|
AppDispatcher.dispatch({
|
|
type: 'widgets/start-edit',
|
|
token: this.state.sessionToken,
|
|
data: widget
|
|
});
|
|
});
|
|
this.setState({ editing: false, widgetChangeData: [] });
|
|
};
|
|
|
|
cancelEditing() {
|
|
//raw widget has no id -> cannot be deleted in its original form
|
|
this.state.widgets.forEach(widget => {
|
|
if (widget.type === 'Image'){
|
|
widget.customProperties.update = true;
|
|
}
|
|
let tempID = this.state.widgetOrigIDs.find(element => element === widget.id);
|
|
if (typeof tempID === 'undefined') {
|
|
AppDispatcher.dispatch({
|
|
type: 'widgets/start-remove',
|
|
data: widget,
|
|
token: this.state.sessionToken
|
|
});
|
|
}
|
|
})
|
|
|
|
AppDispatcher.dispatch({
|
|
type: 'widgets/start-load',
|
|
token: this.state.sessionToken,
|
|
param: '?dashboardID=' + this.state.dashboard.id
|
|
});
|
|
|
|
AppDispatcher.dispatch({
|
|
type: 'dashboards/start-load',
|
|
data: this.props.match.params.dashboard,
|
|
token: this.state.sessionToken
|
|
});
|
|
this.setState({ editing: false, widgetChangeData: [] });
|
|
|
|
};
|
|
|
|
setGrid(value) {
|
|
|
|
let dashboard = this.state.dashboard;
|
|
dashboard.grid = value;
|
|
this.setState({ dashboard });
|
|
this.forceUpdate();
|
|
};
|
|
|
|
setDashboardSize(value) {
|
|
const maxHeight = Object.values(this.state.widgets).reduce((currentHeight, widget) => {
|
|
const absolutHeight = widget.y + widget.height;
|
|
|
|
return absolutHeight > currentHeight ? absolutHeight : currentHeight;
|
|
}, 0);
|
|
let dashboard = this.state.dashboard;
|
|
|
|
if (value === -1) {
|
|
|
|
let tempHeight = this.state.dashboard.height - 50;
|
|
|
|
if (tempHeight >= 400 && tempHeight >= (maxHeight + 80)) {
|
|
dashboard.height = tempHeight;
|
|
this.setState({ dashboard });
|
|
}
|
|
}
|
|
else {
|
|
dashboard.height = this.state.dashboard.height + 50;
|
|
this.setState({ dashboard });
|
|
}
|
|
|
|
this.forceUpdate();
|
|
}
|
|
|
|
pauseData() {
|
|
this.setState({ paused: true });
|
|
};
|
|
|
|
unpauseData() {
|
|
this.setState({ paused: false });
|
|
};
|
|
|
|
editInputSignals() {
|
|
this.setState({ editInputSignalsModal: true });
|
|
};
|
|
|
|
editOutputSignals() {
|
|
this.setState({ editOutputSignalsModal: true });
|
|
};
|
|
|
|
closeEditSignalsModal(direction) {
|
|
if (direction === "in") {
|
|
this.setState({ editInputSignalsModal: false });
|
|
} else if (direction === "out") {
|
|
this.setState({ editOutputSignalsModal: false });
|
|
}
|
|
}
|
|
|
|
|
|
render() {
|
|
if (this.state.dashboard === undefined) {
|
|
return <div className="section-title"> <span>{"Loading Dashboard..."}</span> </div>
|
|
}
|
|
|
|
const grid = this.state.dashboard.grid;
|
|
const boxClasses = classNames('section', 'box', { 'fullscreen-padding': this.props.isFullscreen });
|
|
let draggable = this.state.editing;
|
|
let dropZoneHeight = this.state.dashboard.height;
|
|
return <div className={boxClasses} >
|
|
<div className='section-header box-header'>
|
|
<div className="section-title">
|
|
<h2>{this.state.dashboard.name}</h2>
|
|
</div>
|
|
|
|
<DashboardButtonGroup
|
|
editing={this.state.editing}
|
|
onEdit={this.startEditing.bind(this)}
|
|
fullscreen={this.props.isFullscreen}
|
|
paused={this.state.paused}
|
|
onSave={this.saveEditing.bind(this)}
|
|
onCancel={this.cancelEditing.bind(this)}
|
|
onFullscreen={this.props.toggleFullscreen}
|
|
onPause={this.pauseData.bind(this)}
|
|
onUnpause={this.unpauseData.bind(this)}
|
|
onEditFiles={this.startEditFiles.bind(this)}
|
|
onEditOutputSignals={this.editOutputSignals.bind(this)}
|
|
onEditInputSignals={this.editInputSignals.bind(this)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="box box-content" onContextMenu={(e) => e.preventDefault()}>
|
|
{this.state.editing &&
|
|
<WidgetToolbox grid={grid} onGridChange={this.setGrid.bind(this)} dashboard={this.state.dashboard} onDashboardSizeChange={this.setDashboardSize.bind(this)} widgets={this.state.widgets} />
|
|
}
|
|
{!draggable ? (
|
|
<WidgetArea widgets={this.state.widgets} dropZoneHeight={dropZoneHeight} editing={this.state.editing} grid={grid} onWidgetAdded={this.handleDrop.bind(this)}>
|
|
{this.state.widgets != null && Object.keys(this.state.widgets).map(widgetKey => (
|
|
<WidgetContainer widget={this.state.widgets[widgetKey]} key={widgetKey}>
|
|
<WidgetContextMenu
|
|
key={widgetKey}
|
|
index={parseInt(widgetKey, 10)}
|
|
widget={this.state.widgets[widgetKey]}
|
|
onEdit={this.editWidget.bind(this)}
|
|
onDuplicate={this.duplicateWidget.bind(this)}
|
|
onDelete={this.deleteWidget.bind(this)}
|
|
onChange={this.widgetChange.bind(this)}
|
|
|
|
onWidgetChange={this.widgetChange.bind(this)}
|
|
editing={this.state.editing}
|
|
grid={grid}
|
|
paused={this.state.paused}
|
|
/>
|
|
</WidgetContainer>
|
|
))}
|
|
</WidgetArea>
|
|
) : (
|
|
<WidgetArea widgets={this.state.widgets} editing={this.state.editing} dropZoneHeight={dropZoneHeight} grid={grid} onWidgetAdded={this.handleDrop.bind(this)}>
|
|
{this.state.widgets != null && Object.keys(this.state.widgets).map(widgetKey => (
|
|
<EditableWidgetContainer
|
|
widget={this.state.widgets[widgetKey]}
|
|
key={widgetKey}
|
|
grid={grid}
|
|
index={parseInt(widgetKey, 10)}
|
|
onWidgetChange={this.widgetChange.bind(this)}>
|
|
<WidgetContextMenu
|
|
key={widgetKey}
|
|
index={parseInt(widgetKey, 10)}
|
|
widget={this.state.widgets[widgetKey]}
|
|
onEdit={this.editWidget.bind(this)}
|
|
onDuplicate={this.duplicateWidget.bind(this)}
|
|
onDelete={this.deleteWidget.bind(this)}
|
|
onChange={this.widgetChange.bind(this)}
|
|
|
|
onWidgetChange={this.widgetChange.bind(this)}
|
|
editing={this.state.editing}
|
|
paused={this.state.paused}
|
|
/>
|
|
</EditableWidgetContainer>
|
|
))}
|
|
</WidgetArea>
|
|
|
|
)}
|
|
|
|
<EditWidget
|
|
sessionToken={this.state.sessionToken}
|
|
show={this.state.editModal}
|
|
onClose={this.closeEdit.bind(this)}
|
|
widget={this.state.modalData}
|
|
signals={this.state.signals}
|
|
files={this.state.files}
|
|
ics={this.state.ics}
|
|
/>
|
|
|
|
<EditFiles
|
|
sessionToken={this.state.sessionToken}
|
|
show={this.state.filesEditModal}
|
|
onClose={this.closeEditFiles.bind(this)}
|
|
signals={this.state.signals}
|
|
files={this.state.files}
|
|
scenarioID={this.state.dashboard.scenarioID}
|
|
/>
|
|
|
|
<EditSignalMapping
|
|
show={this.state.editOutputSignalsModal}
|
|
onCloseEdit={(direction) => this.closeEditSignalsModal(direction)}
|
|
direction="Output"
|
|
signals={this.state.signals}
|
|
configID={null}
|
|
configs={this.state.configs}
|
|
sessionToken={this.state.sessionToken}
|
|
/>
|
|
<EditSignalMapping
|
|
show={this.state.editInputSignalsModal}
|
|
onCloseEdit={(direction) => this.closeEditSignalsModal(direction)}
|
|
direction="Input"
|
|
signals={this.state.signals}
|
|
configID={null}
|
|
configs={this.state.configs}
|
|
sessionToken={this.state.sessionToken}
|
|
/>
|
|
</div>
|
|
</div>;
|
|
}
|
|
}
|
|
|
|
|
|
let fluxContainerConverter = require('../common/FluxContainerConverter');
|
|
export default Fullscreenable()(Container.create(fluxContainerConverter.convert(Dashboard), { withProps: true }));
|