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

implemented websocket logic for redux. Updated widgets and dashboard

Signed-off-by: Andrii Podriez <andrey5577990@gmail.com>
This commit is contained in:
Andrii Podriez 2024-08-28 20:36:42 +02:00 committed by iripiri
parent 4d97b629cc
commit bfcc11ebae
81 changed files with 2114 additions and 1231 deletions

View file

@ -42,7 +42,6 @@ import { useSelector } from 'react-redux';
const App = () => {
const isTokenExpired = (token) => {
console.log("decoded, ", jwt.decode(token))
let decodedToken = jwt.decode(token);
let timeNow = (new Date().getTime() + 1) / 1000;
return decodedToken.exp < timeNow;

View file

@ -14,101 +14,87 @@
* 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 NotificationsFactory from "../data-managers/notifications-factory";
import AppDispatcher from '../app-dispatcher';
class WebsocketAPI {
constructor(websocketurl, callbacks) {
this.websocketurl = websocketurl;
this.callbacks = callbacks;
const OFFSET_TYPE = 2;
const OFFSET_VERSION = 4;
this.wasConnected = false;
this.isClosing = false;
this.connect(websocketurl, callbacks);
class WebSocketManager {
constructor() {
this.socket = null;
}
connect(websocketurl, callbacks) {
// create web socket client
this.socket = new WebSocket(WebsocketAPI.getURL(websocketurl), 'live');
this.socket.binaryType = 'arraybuffer';
this.socket.onclose = this.onClose;
this.socket.onopen = this.onOpen;
this.socket.onerror = this.onError;
id = null;
// register callbacks
if (callbacks.onMessage)
this.socket.onmessage = callbacks.onMessage;
}
reconnect() {
//console.log("Reconnecting: " + this.websocketurl);
this.connect(this.websocketurl, this.callbacks);
}
get url() {
return WebsocketAPI.getURL(this.websocketurl);
}
send(data) {
this.socket.send(data);
}
close(code, reason) {
this.isClosing = true;
this.socket.close(code, reason);
}
onError = e => {
console.error('Error on WebSocket connection to: ' + this.websocketurl + ':', e);
if ('onError' in this.callbacks)
this.callbacks.onError(e);
}
onOpen = e => {
AppDispatcher.dispatch({
type: 'websocket/connected',
data: this.websocketurl,
});
this.wasConnected = true;
if ('onOpen' in this.callbacks)
this.callbacks.onOpen(e);
}
onClose = e => {
if (this.isClosing) {
if ('onClose' in this.callbacks)
this.callbacks.onClose(e);
connect(url, onMessage, onOpen, onClose) {
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
console.log('Already connected to:', url);
return;
}
else {
if (this.wasConnected) {
AppDispatcher.dispatch({
type: 'websocket/connection-error',
data: this.websocketurl,
});
NotificationsDataManager.addNotification(NotificationsFactory.WEBSOCKET_CONNECTION_WARN(this.websocketurl));
console.log("Connection to " + this.websocketurl + " dropped. Attempt reconnect in 1 sec");
window.setTimeout(() => { this.reconnect(); }, 1000);
if (this.socket) {
this.socket.close();
}
this.socket = new WebSocket(url, 'live');
this.socket.binaryType = 'arraybuffer';
this.socket.onopen = onOpen;
this.socket.onmessage = (event) => {
const msgs = this.bufferToMessageArray(event.data);
onMessage(msgs);
};
this.socket.onclose = onClose;
}
disconnect() {
if (this.socket) {
this.socket.close();
this.socket = null;
console.log('WebSocket connection closed');
}
}
bufferToMessageArray(blob) {
/* some local variables for parsing */
let offset = 0;
const msgs = [];
/* for every msg in vector */
while (offset < blob.byteLength) {
const msg = this.bufferToMessage(new DataView(blob, offset));
if (msg !== undefined) {
msgs.push(msg);
offset += msg.blob.byteLength;
}
}
}
static getURL(websocketurl) {
// create an anchor element (note: no need to append this element to the document)
var link = document.createElement('a');
link.href = websocketurl;
if (link.protocol === 'https:')
link.protocol = 'wss:';
else
link.protocol = 'ws:';
return link.href;
}
return msgs;
}
export default WebsocketAPI;
bufferToMessage(data) {
// parse incoming message into usable data
if (data.byteLength === 0) {
return null;
}
const source_index = data.getUint8(1);
const bits = data.getUint8(0);
const length = data.getUint16(0x02, 1);
const bytes = length * 4 + 16;
return {
version: (bits >> OFFSET_VERSION) & 0xF,
type: (bits >> OFFSET_TYPE) & 0x3,
source_index: source_index,
length: length,
sequence: data.getUint32(0x04, 1),
timestamp: data.getUint32(0x08, 1) * 1e3 + data.getUint32(0x0C, 1) * 1e-6,
values: new Float32Array(data.buffer, data.byteOffset + 0x10, length),
blob: new DataView(data.buffer, data.byteOffset + 0x00, bytes),
// id: id
};
}
}
export const wsManager = new WebSocketManager();

View file

@ -28,7 +28,7 @@ const buttonStyle = {
const iconStyle = {
height: '25px',
width: '25px'
}
};
let buttonkey = 0;

View file

@ -1,447 +1,521 @@
// import React, { useState, useEffect, useCallback } from 'react';
// import { useParams } from 'react-router-dom';
// import Fullscreenable from 'react-fullscreenable';
// import classNames from 'classnames';
// import 'react-contexify/dist/ReactContexify.min.css';
// import EditWidget from '../../widget/edit-widget/edit-widget';
// import EditFilesDialog from '../../file/edit-files';
// import EditSignalMappingDialog from '../scenarios/dialogs/edit-signal-mapping'
// import WidgetToolbox from '../../widget/widget-toolbox';
// import WidgetArea from './widget-area';
// import DashboardButtonGroup from './dashboard-button-group';
// import IconToggleButton from '../../common/buttons/icon-toggle-button';
// import WidgetContainer from '../../widget/widget-container';
// import Widget from "../../widget/widget";
// import {
// useGetDashboardQuery,
// useLazyGetWidgetsQuery,
// useLazyGetConfigsQuery,
// useAddWidgetMutation,
// useUpdateWidgetMutation,
// useDeleteWidgetMutation
// } from '../../store/apiSlice';
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
import Fullscreenable from 'react-fullscreenable';
import classNames from 'classnames';
import 'react-contexify/dist/ReactContexify.min.css';
import EditWidget from './widget/edit-widget/edit-widget';
import EditSignalMappingDialog from '../scenarios/dialogs/edit-signal-mapping'
import WidgetToolbox from './widget/widget-toolbox';
import WidgetArea from './widget-area';
import DashboardButtonGroup from './dashboard-button-group';
import IconToggleButton from '../../common/buttons/icon-toggle-button';
import WidgetContainer from "./widget/widget-container";
import Widget from "./widget/widget";
// const startUpdaterWidgets = new Set(['Slider', 'Button', 'NumberInput']);
import { connectWebSocket, disconnect } from '../../store/websocketSlice';
// const Dashboard = ({ isFullscreen, toggleFullscreen }) => {
// const params = useParams();
// const { data: dashboardRes, error: dashboardError, isLoading: isDashboardLoading } = useGetDashboardQuery(params.dashboard);
// const dashboard = dashboardRes ? dashboardRes.dashboard : {};
import {
useGetDashboardQuery,
useLazyGetWidgetsQuery,
useLazyGetConfigsQuery,
useAddWidgetMutation,
useUpdateWidgetMutation,
useDeleteWidgetMutation,
useLazyGetFilesQuery,
useUpdateDashboardMutation,
useGetICSQuery,
useLazyGetSignalsQuery
} from '../../store/apiSlice';
// const [triggerGetWidgets] = useLazyGetWidgetsQuery();
// const [triggerGetConfigs] = useLazyGetConfigsQuery();
// const [addWidget] = useAddWidgetMutation();
// const [updateWidget] = useUpdateWidgetMutation();
// const [deleteWidgetMutation] = useDeleteWidgetMutation();
const startUpdaterWidgets = new Set(['Slider', 'Button', 'NumberInput']);
// const [widgets, setWidgets] = useState([]);
// const [configs, setConfigs] = useState([]);
// const [signals, setSignals] = useState([]);
// const [sessionToken, setSessionToken] = useState(localStorage.getItem("token"));
// const [files, setFiles] = useState([]);
// const [ics, setIcs] = useState([]);
// const [editing, setEditing] = useState(false);
// const [paused, setPaused] = useState(false);
// const [editModal, setEditModal] = useState(false);
// const [editOutputSignalsModal, setEditOutputSignalsModal] = useState(false);
// const [editInputSignalsModal, setEditInputSignalsModal] = useState(false);
// const [filesEditModal, setFilesEditModal] = useState(false);
// const [filesEditSaveState, setFilesEditSaveState] = useState([]);
// const [modalData, setModalData] = useState(null);
// const [modalIndex, setModalIndex] = useState(null);
// const [widgetChangeData, setWidgetChangeData] = useState([]);
// const [widgetOrigIDs, setWidgetOrigIDs] = useState([]);
// const [maxWidgetHeight, setMaxWidgetHeight] = useState(null);
// const [locked, setLocked] = useState(false);
const Dashboard = ({ isFullscreen, toggleFullscreen }) => {
const dispatch = useDispatch();
const params = useParams();
const { data: dashboardRes, error: dashboardError, isLoading: isDashboardLoading } = useGetDashboardQuery(params.dashboard);
const dashboard = dashboardRes ? dashboardRes.dashboard : {};
const {data: icsRes} = useGetICSQuery();
const ics = icsRes ? icsRes.ics : [];
// useEffect(() => {
// if (dashboard.id) {
// fetchWidgets(dashboard.id);
// fetchConfigs(dashboard.scenarioID);
// }
// }, [dashboard]);
const [triggerGetWidgets] = useLazyGetWidgetsQuery();
const [triggerGetConfigs] = useLazyGetConfigsQuery();
const [triggerGetFiles] = useLazyGetFilesQuery();
const [triggerGetSignals] = useLazyGetSignalsQuery();
const [addWidget] = useAddWidgetMutation();
const [updateWidget] = useUpdateWidgetMutation();
const [deleteWidgetMutation] = useDeleteWidgetMutation();
const [updateDashboard] = useUpdateDashboardMutation();
// const fetchWidgets = async (dashboardID) => {
// try {
// const res = await triggerGetWidgets(dashboardID).unwrap();
// if (res.widgets) {
// setWidgets(res.widgets);
// }
// } catch (err) {
// console.log('error', err);
// }
// };
const [widgets, setWidgets] = useState([]);
const [widgetsToUpdate, setWidgetsToUpdate] = useState([]);
const [configs, setConfigs] = useState([]);
const [signals, setSignals] = useState([]);
const [sessionToken, setSessionToken] = useState(localStorage.getItem("token"));
const [files, setFiles] = useState([]);
const [editing, setEditing] = useState(false);
const [paused, setPaused] = useState(false);
const [editModal, setEditModal] = useState(false);
const [editOutputSignalsModal, setEditOutputSignalsModal] = useState(false);
const [editInputSignalsModal, setEditInputSignalsModal] = useState(false);
const [filesEditModal, setFilesEditModal] = useState(false);
const [filesEditSaveState, setFilesEditSaveState] = useState([]);
const [modalData, setModalData] = useState(null);
const [modalIndex, setModalIndex] = useState(null);
const [widgetChangeData, setWidgetChangeData] = useState([]);
const [widgetOrigIDs, setWidgetOrigIDs] = useState([]);
const [maxWidgetHeight, setMaxWidgetHeight] = useState(null);
const [locked, setLocked] = useState(false);
// const fetchConfigs = async (scenarioID) => {
// try {
// const res = await triggerGetConfigs(scenarioID).unwrap();
// if (res.configs) {
// setConfigs(res.configs);
// }
// } catch (err) {
// console.log('error', err);
// }
// };
const [height, setHeight] = useState(10);
const [grid, setGrid] = useState(50);
const [newHeightValue, setNewHeightValue] = useState(0);
// const handleKeydown = useCallback((e) => {
// switch (e.key) {
// case ' ':
// case 'p':
// setPaused(prevPaused => !prevPaused);
// break;
// case 'e':
// setEditing(prevEditing => !prevEditing);
// break;
// case 'f':
// toggleFullscreen();
// break;
// default:
// }
// }, [toggleFullscreen]);
// useEffect(() => {
// window.addEventListener('keydown', handleKeydown);
// return () => {
// window.removeEventListener('keydown', handleKeydown);
// };
// }, [handleKeydown]);
useEffect(() => {
const wsUrl = 'wss://villas.k8s.eonerc.rwth-aachen.de/ws/ws_sig';
dispatch(connectWebSocket({ url: wsUrl, id: 547627 }));
// const handleDrop = async (widget) => {
// widget.dashboardID = dashboard.id;
return () => {
dispatch(disconnect());
};
}, [dispatch]);
// if (widget.type === 'ICstatus') {
// let allICids = ics.map(ic => ic.id);
// widget.customProperties.checkedIDs = allICids;
// }
// try {
// const res = await addWidget(widget).unwrap();
// if (res) {
// fetchWidgets(dashboard.id);
// }
// } catch (err) {
// console.log('error', err);
// }
// };
useEffect(() => {
if (dashboard.id) {
//as soon as dashboard is loaded, load widgets, configs, signals and files for this dashboard
fetchWidgets(dashboard.id);
fetchWidgetData(dashboard.scenarioID);
setHeight(dashboard.height);
setGrid(dashboard.grid);
}
}, [dashboard]);
// const widgetChange = async (widget) => {
// setWidgetChangeData(prevWidgetChangeData => [...prevWidgetChangeData, widget]);
const fetchWidgets = async (dashboardID) => {
try {
const widgetsRes = await triggerGetWidgets(dashboardID).unwrap();
if (widgetsRes.widgets) {
setWidgets(widgetsRes.widgets);
}
} catch (err) {
console.log('error fetching data', err);
}
}
// try {
// await updateWidget({ widgetID: widget.id, updatedWidget: { widget } }).unwrap();
// fetchWidgets(dashboard.id);
// } catch (err) {
// console.log('error', err);
// }
// };
const fetchWidgetData = async (scenarioID) => {
try {
const filesRes = await triggerGetFiles(scenarioID).unwrap();
if (filesRes.files) {
setFiles(filesRes.files);
}
const configsRes = await triggerGetConfigs(scenarioID).unwrap();
if (configsRes.configs) {
setConfigs(configsRes.configs);
//load signals if there are any configs
// const onChange = async (widget) => {
// try {
// await updateWidget({ widgetID: widget.id, updatedWidget: { widget } }).unwrap();
// fetchWidgets(dashboard.id);
// } catch (err) {
// console.log('error', err);
// }
// };
if(configsRes.configs.length > 0){
for(const config of configsRes.configs){
const signalsInRes = await triggerGetSignals({configID: config.id, direction: "in"}).unwrap();
const signalsOutRes = await triggerGetSignals({configID: config.id, direction: "out"}).unwrap();
setSignals(prevState => ([...signalsInRes.signals, ...signalsOutRes.signals, ...prevState]));
}
}
}
} catch (err) {
console.log('error fetching data', err);
}
}
// const onSimulationStarted = () => {
// widgets.forEach(async (widget) => {
// if (startUpdaterWidgets.has(widget.type)) {
// widget.customProperties.simStartedSendValue = true;
// try {
// await updateWidget({ widgetID: widget.id, updatedWidget: { widget } }).unwrap();
// } catch (err) {
// console.log('error', err);
// }
// }
// });
// };
const handleKeydown = useCallback((e) => {
switch (e.key) {
case ' ':
case 'p':
setPaused(prevPaused => !prevPaused);
break;
case 'e':
setEditing(prevEditing => !prevEditing);
break;
case 'f':
toggleFullscreen();
break;
default:
}
}, [toggleFullscreen]);
// const editWidget = (widget, index) => {
// setEditModal(true);
// setModalData(widget);
// setModalIndex(index);
// };
useEffect(() => {
window.addEventListener('keydown', handleKeydown);
return () => {
window.removeEventListener('keydown', handleKeydown);
};
}, [handleKeydown]);
// const duplicateWidget = async (widget) => {
// let widgetCopy = { ...widget, id: undefined, x: widget.x + 50, y: widget.y + 50 };
// try {
// const res = await addWidget({ widget: widgetCopy }).unwrap();
// if (res) {
// fetchWidgets(dashboard.id);
// }
// } catch (err) {
// console.log('error', err);
// }
// };
const handleDrop = async (widget) => {
widget.dashboardID = dashboard.id;
// const startEditFiles = () => {
// let tempFiles = files.map(file => ({ id: file.id, name: file.name }));
// setFilesEditModal(true);
// setFilesEditSaveState(tempFiles);
// };
if (widget.type === 'ICstatus') {
let allICids = ics.map(ic => ic.id);
widget.customProperties.checkedIDs = allICids;
}
// const closeEditFiles = () => {
// widgets.forEach(widget => {
// if (widget.type === "Image") {
// widget.customProperties.update = true;
// }
// });
// setFilesEditModal(false);
// };
try {
const res = await addWidget(widget).unwrap();
if (res) {
fetchWidgets(dashboard.id);
}
} catch (err) {
console.log('error', err);
}
};
// const closeEdit = async (data) => {
// if (!data) {
// setEditModal(false);
// setModalData(null);
// setModalIndex(null);
// return;
// }
const widgetChange = async (widget) => {
setWidgetsToUpdate(prevWidgetsToUpdate => [...prevWidgetsToUpdate, widget.id]);
setWidgets(prevWidgets => prevWidgets.map(w => w.id === widget.id ? {...widget} : w));
// if (data.type === "Image") {
// data.customProperties.update = true;
// }
// try {
// await updateWidget({ widgetID: widget.id, updatedWidget: { widget } }).unwrap();
// fetchWidgets(dashboard.id);
// } catch (err) {
// console.log('error', err);
// }
};
// try {
// await updateWidget({ widgetID: data.id, updatedWidget: { widget: data } }).unwrap();
// fetchWidgets(dashboard.id);
// } catch (err) {
// console.log('error', err);
// }
const onChange = async (widget) => {
try {
await updateWidget({ widgetID: widget.id, updatedWidget: { widget: widget } }).unwrap();
fetchWidgets(dashboard.id);
} catch (err) {
console.log('error', err);
}
};
// setEditModal(false);
// setModalData(null);
// setModalIndex(null);
// };
const onSimulationStarted = () => {
widgets.forEach(async (widget) => {
if (startUpdaterWidgets.has(widget.type)) {
widget.customProperties.simStartedSendValue = true;
try {
await updateWidget({ widgetID: widget.id, updatedWidget: { widget } }).unwrap();
} catch (err) {
console.log('error', err);
}
}
});
};
// const deleteWidget = async (widgetID) => {
// try {
// await deleteWidgetMutation(widgetID).unwrap();
// fetchWidgets(dashboard.id);
// } catch (err) {
// console.log('error', err);
// }
// };
const editWidget = (widget, index) => {
setEditModal(true);
setModalData({...widget});
setModalIndex(index);
};
// const startEditing = () => {
// let originalIDs = widgets.map(widget => widget.id);
// widgets.forEach(async (widget) => {
// if (widget.type === 'Slider' || widget.type === 'NumberInput' || widget.type === 'Button') {
// try {
// await updateWidget({ widgetID: widget.id, updatedWidget: { widget } }).unwrap();
// } catch (err) {
// console.log('error', err);
// }
// } else if (widget.type === 'Image') {
// widget.customProperties.update = true;
// }
// });
// setEditing(true);
// setWidgetOrigIDs(originalIDs);
// };
const duplicateWidget = async (widget) => {
let widgetCopy = { ...widget, id: undefined, x: widget.x + 50, y: widget.y + 50 };
try {
const res = await addWidget({ widget: widgetCopy }).unwrap();
if (res) {
fetchWidgets(dashboard.id);
}
} catch (err) {
console.log('error', err);
}
};
// const saveEditing = () => {
// widgets.forEach(async (widget) => {
// if (widget.type === 'Image') {
// widget.customProperties.update = true;
// }
// try {
// await updateWidget({ widgetID: widget.id, updatedWidget: { widget } }).unwrap();
// } catch (err) {
// console.log('error', err);
// }
// });
// setEditing(false);
// setWidgetChangeData([]);
// };
const startEditFiles = () => {
let tempFiles = files.map(file => ({ id: file.id, name: file.name }));
setFilesEditModal(true);
setFilesEditSaveState(tempFiles);
};
// const cancelEditing = () => {
// widgets.forEach(async (widget) => {
// if (widget.type === 'Image') {
// widget.customProperties.update = true;
// }
// if (!widgetOrigIDs.includes(widget.id)) {
// try {
// await deleteWidget(widget.id).unwrap();
// } catch (err) {
// console.log('error', err);
// }
// }
// });
// setEditing(false);
// setWidgetChangeData([]);
// };
const closeEditFiles = () => {
widgets.forEach(widget => {
if (widget.type === "Image") {
//widget.customProperties.update = true;
}
});
setFilesEditModal(false);
};
// const setGrid = (value) => {
// setState(prevState => ({ ...prevState, dashboard: { ...dashboard, grid: value } }));
// };
const closeEdit = async (data) => {
if (!data) {
setEditModal(false);
setModalData(null);
setModalIndex(null);
return;
}
// const setDashboardSize = (value) => {
// const maxHeight = Object.values(widgets).reduce((currentHeight, widget) => {
// const absolutHeight = widget.y + widget.height;
// return absolutHeight > currentHeight ? absolutHeight : currentHeight;
// }, 0);
if (data.type === "Image") {
data.customProperties.update = true;
}
// if (value === -1) {
// if (dashboard.height >= 450 && dashboard.height >= (maxHeight + 80)) {
// setState(prevState => ({ ...prevState, dashboard: { ...dashboard, height: dashboard.height - 50 } }));
// }
// } else {
// setState(prevState => ({ ...prevState, dashboard: { ...dashboard, height: dashboard.height + 50 } }));
// }
// };
try {
await updateWidget({ widgetID: data.id, updatedWidget: { widget: data } }).unwrap();
fetchWidgets(dashboard.id);
} catch (err) {
console.log('error', err);
}
// const pauseData = () => setPaused(true);
// const unpauseData = () => setPaused(false);
// const editInputSignals = () => setEditInputSignalsModal(true);
// const editOutputSignals = () => setEditOutputSignalsModal(true);
setEditModal(false);
setModalData(null);
setModalIndex(null);
};
// const closeEditSignalsModal = (direction) => {
// if (direction === "in") {
// setEditInputSignalsModal(false);
// } else if (direction === "out") {
// setEditOutputSignalsModal(false);
// }
// };
const deleteWidget = async (widgetID) => {
try {
await deleteWidgetMutation(widgetID).unwrap();
fetchWidgets(dashboard.id);
} catch (err) {
console.log('error', err);
}
};
// const buttonStyle = { marginLeft: '10px' };
// const iconStyle = { height: '25px', width: '25px' };
// const grid = dashboard.grid;
// const boxClasses = classNames('section', 'box', { 'fullscreen-padding': isFullscreen });
// let dropZoneHeight = dashboard.height;
const startEditing = () => {
let originalIDs = widgets.map(widget => widget.id);
widgets.forEach(async (widget) => {
if (widget.type === 'Slider' || widget.type === 'NumberInput' || widget.type === 'Button') {
try {
await updateWidget({ widgetID: widget.id, updatedWidget: { widget } }).unwrap();
} catch (err) {
console.log('error', err);
}
} else if (widget.type === 'Image') {
//widget.customProperties.update = true;
}
});
setEditing(true);
setWidgetOrigIDs(originalIDs);
};
// if (isDashboardLoading) {
// return <div>Loading...</div>;
// }
const saveEditing = async () => {
// widgets.forEach(async (widget) => {
// if (widget.type === 'Image') {
// widget.customProperties.update = true;
// }
// try {
// await updateWidget({ widgetID: widget.id, updatedWidget: { widget } }).unwrap();
// } catch (err) {
// console.log('error', err);
// }
// });
// if (dashboardError) {
// return <div>Error. Dashboard not found</div>;
// }
// return (
// <div className={boxClasses}>
// <div key={"header-box"} className='section-header box-header'>
// <div key={"title"} className="section-title">
// <h2>
// {dashboard.name}
// <span key={"toggle-lock-button"} className='icon-button'>
// <IconToggleButton
// childKey={0}
// checked={locked}
// index={dashboard.id}
// checkedIcon='lock'
// uncheckedIcon='lock-open'
// tooltipChecked='Dashboard is locked, cannot be edited'
// tooltipUnchecked='Dashboard is unlocked, can be edited'
// disabled={true}
// buttonStyle={buttonStyle}
// iconStyle={iconStyle}
// />
// </span>
// </h2>
// </div>
if(height !== dashboard.height || grid !== dashboard.grid) {
try {
const {height: oldHeight, grid: oldGrid, ...rest} = dashboard;
await updateDashboard({dashboardID: dashboard.id, dashboard:{height: height, grid: grid, ...rest}}).unwrap();
} catch (err) {
console.log('error', err);
}
}
// <DashboardButtonGroup
// key={"dashboard-buttons"}
// locked={locked}
// editing={editing}
// onEdit={startEditing}
// fullscreen={isFullscreen}
// paused={paused}
// onSave={saveEditing}
// onCancel={cancelEditing}
// onFullscreen={toggleFullscreen}
// onPause={pauseData}
// onUnpause={unpauseData}
// onEditFiles={startEditFiles}
// onEditOutputSignals={editOutputSignals}
// onEditInputSignals={editInputSignals}
// />
// </div>
// <div key={"dashboard-area"} className="box box-content" onContextMenu={(e) => e.preventDefault()}>
// {editing &&
// <WidgetToolbox
// key={"widget-toolbox"}
// grid={grid}
// onGridChange={setGrid}
// dashboard={dashboard}
// onDashboardSizeChange={setDashboardSize}
// widgets={widgets}
// />
// }
if(widgetsToUpdate.length > 0){
try {
for(const index in widgetsToUpdate){
await updateWidget({ widgetID: widgetsToUpdate[index], updatedWidget: { widget: {...widgets.find(w => w.id == widgetsToUpdate[index])} } }).unwrap();
}
fetchWidgets(dashboard.id);
} catch (err) {
console.log('error', err);
}
}
// <WidgetArea
// key={"widget-area"}
// widgets={widgets}
// editing={editing}
// dropZoneHeight={dropZoneHeight}
// grid={grid}
// onWidgetAdded={handleDrop}
// >
// {widgets != null && Object.keys(widgets).map(widgetKey => (
// <div key={"widget-container-wrapper" + widgetKey}>
// <WidgetContainer
// widget={widgets[widgetKey]}
// key={"widget-container" + widgetKey}
// index={parseInt(widgetKey, 10)}
// grid={grid}
// onWidgetChange={widgetChange}
// editing={editing}
// paused={paused}
// onEdit={editWidget}
// onDuplicate={duplicateWidget}
// onDelete={(widget, index) => deleteWidget(widget.id)}
// onChange={editing ? widgetChange : onChange}
// >
// <Widget
// key={"widget" + widgetKey}
// data={widgets[widgetKey]}
// editing={editing}
// index={parseInt(widgetKey, 10)}
// paused={paused}
// onSimulationStarted={onSimulationStarted}
// ics={ics}
// configs={configs}
// scenarioID={dashboard.scenarioID}
// />
// </WidgetContainer>
// </div>
// ))}
// </WidgetArea>
setEditing(false);
setWidgetChangeData([]);
};
// <EditWidget
// key={"edit-widget"}
// sessionToken={sessionToken}
// show={editModal}
// onClose={closeEdit}
// widget={modalData}
// signals={signals}
// files={files}
// ics={ics}
// configs={configs}
// scenarioID={dashboard.scenarioID}
// />
const cancelEditing = () => {
// widgets.forEach(async (widget) => {
// if (widget.type === 'Image') {
// widget.customProperties.update = true;
// }
// if (!widgetOrigIDs.includes(widget.id)) {
// try {
// await deleteWidget(widget.id).unwrap();
// } catch (err) {
// console.log('error', err);
// }
// }
// });
fetchWidgets(dashboard.id);
setEditing(false);
setWidgetChangeData([]);
setHeight(dashboard.height);
setGrid(dashboard.grid);
};
// <EditSignalMappingDialog
// key={"edit-signal-mapping-input-dialog"}
// show={editInputSignalsModal}
// onCloseEdit={closeEditSignalsModal}
// direction="Input"
// signals={signals}
// configID={null}
// configs={configs}
// sessionToken={sessionToken}
// />
// </div>
// </div>
// );
// };
const updateGrid = (value) => {
setGrid(value);
};
// export default Fullscreenable()(Dashboard);
const updateHeight = (value) => {
const maxHeight = Object.values(widgets).reduce((currentHeight, widget) => {
const absolutHeight = widget.y + widget.height;
return absolutHeight > currentHeight ? absolutHeight : currentHeight;
}, 0);
const Dashboard = (props) => {
return <div></div>
}
if (value === -1) {
if (dashboard.height >= 450 && dashboard.height >= (maxHeight + 80)) {
setHeight(prevState => (prevState - 50));
}
} else {
setHeight( prevState => ( prevState + 50));
}
};
export default Dashboard;
const pauseData = () => setPaused(true);
const unpauseData = () => setPaused(false);
const editInputSignals = () => setEditInputSignalsModal(true);
const editOutputSignals = () => setEditOutputSignalsModal(true);
const closeEditSignalsModal = (direction) => {
if (direction === "in") {
setEditInputSignalsModal(false);
} else if (direction === "out") {
setEditOutputSignalsModal(false);
}
};
const buttonStyle = { marginLeft: '10px' };
const iconStyle = { height: '25px', width: '25px' };
const boxClasses = classNames('section', 'box', { 'fullscreen-padding': isFullscreen });
if (isDashboardLoading) {
return <div>Loading...</div>;
}
if (dashboardError) {
return <div>Error. Dashboard not found</div>;
}
return (
<div className={boxClasses}>
<div key={"header-box"} className='section-header box-header'>
<div key={"title"} className="section-title">
<h2>
{dashboard.name}
<span key={"toggle-lock-button"} className='icon-button'>
<IconToggleButton
childKey={0}
checked={locked}
index={dashboard.id}
checkedIcon='lock'
uncheckedIcon='lock-open'
tooltipChecked='Dashboard is locked, cannot be edited'
tooltipUnchecked='Dashboard is unlocked, can be edited'
disabled={true}
buttonStyle={buttonStyle}
iconStyle={iconStyle}
/>
</span>
</h2>
</div>
<DashboardButtonGroup
key={"dashboard-buttons"}
locked={locked}
editing={editing}
onEdit={startEditing}
fullscreen={isFullscreen}
paused={paused}
onSave={saveEditing}
onCancel={cancelEditing}
onFullscreen={toggleFullscreen}
onPause={pauseData}
onUnpause={unpauseData}
onEditFiles={startEditFiles}
onEditOutputSignals={editOutputSignals}
onEditInputSignals={editInputSignals}
/>
</div>
<div key={"dashboard-area"} className="box box-content" onContextMenu={(e) => e.preventDefault()}>
{editing &&
<WidgetToolbox
key={"widget-toolbox"}
grid={grid}
onGridChange={updateGrid}
dashboard={dashboard}
onDashboardSizeChange={updateHeight}
widgets={widgets}
/>
}
<WidgetArea
key={"widget-area"}
widgets={widgets}
editing={editing}
dropZoneHeight={height}
grid={grid}
onWidgetAdded={handleDrop}
>
{widgets != null && Object.keys(widgets).map(widgetKey => (
<div key={"widget-container-wrapper" + widgetKey}>
<WidgetContainer
widget={JSON.parse(JSON.stringify(widgets[widgetKey]))}
key={"widget-container" + widgetKey}
index={parseInt(widgetKey, 10)}
grid={grid}
onWidgetChange={widgetChange}
editing={editing}
paused={paused}
onEdit={editWidget}
onDuplicate={duplicateWidget}
onDelete={(widget, index) => deleteWidget(widget.id)}
onChange={editing ? widgetChange : onChange}
>
<Widget
widget={JSON.parse(JSON.stringify(widgets[widgetKey]))}
editing={editing}
files={files}
configs={configs}
signals={signals}
paused={paused}
ics={ics}
icData={[]}
/>
</WidgetContainer>
</div>
))}
</WidgetArea>
<EditWidget
key={"edit-widget"}
sessionToken={sessionToken}
show={editModal}
onClose={closeEdit}
widget={modalData}
signals={signals}
files={files}
ics={ics}
configs={configs}
scenarioID={dashboard.scenarioID}
/>
{/* <EditFilesDialog
key={"edit-files-dialog"}
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}
locked={this.state.locked}
/> */}
<EditSignalMappingDialog
key={"edit-signal-mapping-input-dialog"}
show={editInputSignalsModal}
onCloseEdit={closeEditSignalsModal}
direction="Input"
signals={signals}
configID={null}
configs={configs}
sessionToken={sessionToken}
/>
</div>
</div>
);
};
export default Fullscreenable()(Dashboard);

View file

@ -0,0 +1,82 @@
/**
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* VILLASweb is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import React from 'react';
import { Form, Button, Col } from 'react-bootstrap';
import Dialog from '../../../common/dialogs/dialog';
class EditFileContent extends React.Component {
valid = true;
constructor(props) {
super(props);
this.state = {
uploadFile: null,
};
}
selectUploadFile(event) {
this.setState({ uploadFile: event.target.files[0] });
};
startEditContent(){
const formData = new FormData();
formData.append("file", this.state.uploadFile);
AppDispatcher.dispatch({
type: 'files/start-edit',
data: formData,
token: this.props.sessionToken,
id: this.props.file.id
});
this.setState({ uploadFile: null });
};
onClose = () => {
this.props.onClose();
};
render() {
return <Dialog
show={this.props.show}
title='Edit File Content'
buttonTitle='Close'
onClose={() => this.onClose()}
blendOutCancel = {true}
valid={true}
>
<Form.Group as={Col} >
<Form.Control
disabled={false}
type='file'
onChange={(event) => this.selectUploadFile(event)} />
</Form.Group>
<Form.Group as={Col} >
<Button
disabled={this.state.uploadFile === null}
onClick={() => this.startEditContent()}>
Upload
</Button>
</Form.Group>
</Dialog>;
}
}
export default EditFileContent;

View file

@ -0,0 +1,192 @@
/**
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* VILLASweb is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import React from 'react';
import {Form, Button, Col, ProgressBar, Row} from 'react-bootstrap';
import Dialog from '../../../common/dialogs/dialog';
import { Table, ButtonColumn, DataColumn } from "../../../common/table";
import EditFileContent from "./edit-file-content";
class EditFilesDialog extends React.Component {
valid = true;
constructor(props) {
super(props);
this.state = {
uploadFile: null,
uploadProgress: 0,
editModal: false,
modalFile: {}
};
}
onClose() {
this.props.onClose();
}
selectUploadFile(event) {
this.setState({ uploadFile: event.target.files[0] });
};
startFileUpload(){
// upload file
const formData = new FormData();
formData.append("file", this.state.uploadFile);
AppDispatcher.dispatch({
type: 'files/start-upload',
data: formData,
token: this.props.sessionToken,
progressCallback: this.updateUploadProgress,
finishedCallback: this.clearProgress,
scenarioID: this.props.scenarioID,
});
this.setState({ uploadFile: null });
};
updateUploadProgress = (event) => {
if (event.hasOwnProperty("percent")){
this.setState({ uploadProgress: parseInt(event.percent.toFixed(), 10) });
} else {
this.setState({ uploadProgress: 0 });
}
};
clearProgress = (newFileID) => {
this.setState({ uploadProgress: 0 });
};
closeEditModal() {
this.setState({editModal: false});
}
deleteFile(index){
let file = this.props.files[index]
AppDispatcher.dispatch({
type: 'files/start-remove',
data: file,
token: this.props.sessionToken
});
}
render() {
let fileOptions = [];
if (this.props.files.length > 0){
fileOptions.push(
<option key = {0} default>Select image file</option>
)
fileOptions.push(this.props.files.map((file, index) => (
<option key={index+1} value={file.id}>{file.name}</option>
)))
} else {
fileOptions = <option disabled value style={{ display: 'none' }}>No files found, please upload one first.</option>
}
const progressBarStyle = {
marginLeft: '100px',
marginTop: '-40px'
};
let title = this.props.locked ? "View files of scenario" : "Edit Files of Scenario";
return (
<Dialog
show={this.props.show}
title={title}
buttonTitle="Close"
onClose={() => this.onClose()}
blendOutCancel = {true}
valid={true}
>
<Table breakWord={true} data={this.props.files}>
<DataColumn
title='ID'
dataKey='id'
width={50}
/>
<DataColumn
title='Name'
dataKey='name'
/>
<DataColumn
title='Size (bytes)'
dataKey='size'
/>
<DataColumn
title='Type'
dataKey='type'
/>
<ButtonColumn
align='right'
deleteButton
onDelete={(index) => this.deleteFile(index)}
editButton
onEdit={index => this.setState({ editModal: true, modalFile: this.props.files[index] })}
locked={this.props.locked}
/>
</Table>
<div style={{ float: 'center' }}>
<h5>Add file</h5>
<Row>
<Col xs lg="4">
<Form.Control
type='file'
onChange={(event) => this.selectUploadFile(event)}
disabled={this.props.locked}
/>
</Col>
<Col xs lg="2">
<span className='solid-button'>
<Button
variant='secondary'
disabled={this.state.uploadFile === null || this.props.locked}
onClick={() => this.startFileUpload()}>
Upload
</Button>
</span>
</Col>
</Row>
</div>
<br />
<Form.Group as={Col} >
<ProgressBar
striped={true}
animated={true}
now={this.state.uploadProgress}
label={this.state.uploadProgress + '%'}
/>
</Form.Group>
<div style={{ clear: 'both' }} />
<EditFileContent
show={this.state.editModal}
onClose={(data) => this.closeEditModal(data)}
sessionToken={this.props.sessionToken}
file={this.state.modalFile}
/>
</Dialog>
);
}
}
export default EditFilesDialog;

View file

@ -17,11 +17,9 @@
import React from 'react';
import PropTypes from 'prop-types';
import Dropzone from './dropzone';
import Grid from './grid';
import WidgetFactory from '../../widget/widget-factory';
import WidgetFactory from './widget/widget-factory';
class WidgetArea extends React.Component {
snapToGrid(value) {

View file

@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react';
import { Form, Container, Row, Col, OverlayTrigger, Tooltip, Button} from 'react-bootstrap';
import ColorPicker from '../../common/color-picker';
import Icon from "../../common/icon";
import ColorPicker from '../../../../common/color-picker';
import Icon from "../../../../common/icon";
const EditWidgetColorControl = (props) => {
const [color, setColor] = useState(null);

View file

@ -17,8 +17,8 @@
import React from 'react';
import { Form, Table, Button, Tooltip, OverlayTrigger } from 'react-bootstrap';
import ColorPicker from '../../common/color-picker'
import Icon from '../../common/icon';
import ColorPicker from '../../../../common/color-picker'
import Icon from '../../../../common/icon';
import { Collapse } from 'react-collapse';
class EditWidgetColorZonesControl extends React.Component {

View file

@ -17,7 +17,6 @@
import React, { Component } from 'react';
import { Col, Row, Form } from 'react-bootstrap';
import WidgetSlider from '../widgets/slider';
class EditWidgetOrientation extends Component {

View file

@ -17,7 +17,7 @@
import React, { Component } from 'react';
import { Form } from 'react-bootstrap';
import ParametersEditor from '../../common/parameters-editor';
import ParametersEditor from '../../../../common/parameters-editor';
class EditWidgetParametersControl extends Component {
constructor(props) {

View file

@ -17,8 +17,8 @@
import React, { Component } from 'react';
import { OverlayTrigger, Tooltip , Button, Form } from 'react-bootstrap';
import ColorPicker from '../../common/color-picker'
import Icon from "../../common/icon";
import ColorPicker from '../../../../common/color-picker'
import Icon from "../../../../common/icon";
import {schemeCategory10} from "d3-scale-chromatic";
class EditWidgetPlotColorsControl extends Component {

View file

@ -17,7 +17,7 @@
import React from 'react';
import { Form } from 'react-bootstrap';
import Dialog from '../../common/dialogs/dialog';
import Dialog from '../../../../common/dialogs/dialog';
import CreateControls from './edit-widget-control-creator';
class EditWidgetDialog extends React.Component {
@ -92,6 +92,8 @@ class EditWidgetDialog extends React.Component {
handleChange(e) {
// TODO: check what we really need in this function. Can we reduce its complexity?
let parts = e.target.id.split('.');
// creating a deep copy of an object to be updated
//let changeObject = JSON.parse(JSON.stringify(this.state.temporal));;
let changeObject = this.state.temporal;
let customProperty = true;
if (parts.length === 1) {
@ -146,9 +148,10 @@ class EditWidgetDialog extends React.Component {
} else {
customProperty ? changeObject[parts[0]][parts[1]] = e.target.value : changeObject[e.target.id] = e.target.value ;
}
console.log(changeObject)
this.setState({ temporal: changeObject});
}
resetState() {

View file

@ -18,7 +18,7 @@
import React from "react";
import { DragSource } from "react-dnd";
import classNames from "classnames";
import Icon from "../common/icon";
import Icon from "../../../common/icon";
// Drag source specification
const toolboxItemSource = {

View file

@ -47,7 +47,6 @@ class WidgetContainer extends React.Component {
widget.x = this.snapToGrid(data.x);
widget.y = this.snapToGrid(data.y);
if (widget.x !== data.x || widget.y !== data.y) {
this.rnd.updatePosition({ x: widget.x, y: widget.y });
}

View file

@ -145,4 +145,4 @@ WidgetContextMenu.propTypes = {
onChange: PropTypes.func.isRequired
};
export default WidgetContextMenu
export default WidgetContextMenu;

View file

@ -15,7 +15,7 @@
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import WidgetSlider from './widgets/slider';
//import WidgetSlider from './widgets/slider';
class WidgetFactory {
@ -148,23 +148,23 @@ class WidgetFactory {
widget.customProperties.value = '';
widget.customProperties.simStartedSendValue = false;
break;
case 'Slider':
widget.minWidth = 380;
widget.minHeight = 30;
widget.width = 400;
widget.height = 50;
widget.customProperties.orientation = WidgetSlider.OrientationTypes.HORIZONTAL.value; // Assign default orientation
widget.customProperties.rangeMin = 0;
widget.customProperties.rangeMax = 200;
widget.customProperties.rangeUseMinMax = true;
widget.customProperties.showUnit = false;
widget.customProperties.continous_update = false;
widget.customProperties.value = '';
widget.customProperties.resizeLeftRightLock = false;
widget.customProperties.resizeTopBottomLock = true;
widget.customProperties.simStartedSendValue = false;
// case 'Slider':
// widget.minWidth = 380;
// widget.minHeight = 30;
// widget.width = 400;
// widget.height = 50;
// widget.customProperties.orientation = WidgetSlider.OrientationTypes.HORIZONTAL.value; // Assign default orientation
// widget.customProperties.rangeMin = 0;
// widget.customProperties.rangeMax = 200;
// widget.customProperties.rangeUseMinMax = true;
// widget.customProperties.showUnit = false;
// widget.customProperties.continous_update = false;
// widget.customProperties.value = '';
// widget.customProperties.resizeLeftRightLock = false;
// widget.customProperties.resizeTopBottomLock = true;
// widget.customProperties.simStartedSendValue = false;
break;
// break;
case 'Gauge':
widget.minWidth = 100;
widget.minHeight = 150;

View file

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

View file

@ -20,7 +20,7 @@ import { Collapse } from 'react-collapse';
import PropTypes from 'prop-types';
import Slider from 'rc-slider';
import { Button, OverlayTrigger, Tooltip } from 'react-bootstrap';
import Icon from "../common/icon";
import Icon from '../../../common/icon';
import ToolboxItem from './toolbox-item';
let hasPintura = true;

View file

@ -0,0 +1,452 @@
/**
* 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 { useState, useEffect } from 'react';
import { useSelector } from 'react-redux';
import WidgetLabel from './widgets/label';
import WidgetLine from './widgets/line';
import WidgetBox from './widgets/box';
import WidgetImage from './widgets/image';
import WidgetPlot from './widgets/plot';
import WidgetTable from './widgets/table';
import WidgetValue from './widgets/value';
import WidgetLamp from './widgets/lamp';
import WidgetGauge from './widgets/gauge';
import WidgetTimeOffset from './widgets/time-offset';
import WidgetICstatus from './widgets/icstatus';
// import WidgetCustomAction from './widgets/custom-action';
// import WidgetAction from './widgets/action';
// import WidgetButton from './widgets/button';
// import WidgetInput from './widgets/input';
// import WidgetSlider from './widgets/slider';
// import WidgetTopology from './widgets/topology';
// import WidgetPlayer from './widgets/player';
//import WidgetHTML from './widgets/html';
import '../../../styles/widgets.css';
import { useGetICSQuery, useGetSignalsQuery, useGetConfigsQuery } from '../../../store/apiSlice';
const Widget = ({widget, editing, files, configs, signals, paused, ics, icData}) => {
const { token: sessionToken } = useSelector((state) => state.auth);
const [icIDs, setICIDs] = useState([]);
const icdata = useSelector((state) => state.websocket.icdata);
useEffect(() => {
if(signals.length > 0){
let ids = [];
for (let id of widget.signalIDs){
let signal = signals.find(s => s.id === id);
if (signal !== undefined) {
let config = configs.find(m => m.id === signal.configID);
if (config !== undefined){
ids[signal.id] = config.icID;
}
}
}
setICIDs(ids);
}
}, [signals])
// const {data: signals, isLoading: signalsLoading} = useGetSignalsQuery({})
switch(widget.type){
//Cosmetic widgets
case 'Line':
return <WidgetLine widget={widget} editing={editing} />
case 'Box':
return <WidgetBox widget={widget} editing={editing} />
case 'Label':
return <WidgetLabel widget={widget}/>;
case 'Image':
return <WidgetImage widget={widget} files={files} token={sessionToken} />
//Displaying widgets
case 'Plot':
return <WidgetPlot widget={widget} data={icdata} signals={signals} icIDs={icIDs} paused={paused} />
case 'Table':
return <WidgetTable widget={widget} data={icdata} signals={signals} icIDs={icIDs} />
case 'Value':
return <WidgetValue widget={widget} data={icdata} signals={signals} icIDs={icIDs} />
case 'Lamp':
return <WidgetLamp widget={widget} data={icdata} signals={signals} icIDs={icIDs} />
case 'Gauge':
return <WidgetGauge widget={widget} data={icdata} signals={signals} icIDs={icIDs} editing={editing} />
case 'TimeOffset':
return <WidgetTimeOffset widget={widget} data={icdata} signals={signals} icIDs={icIDs} editing={editing} />
case 'ICstatus':
return <WidgetICstatus widget={widget} ics={ics} />
//Manipulation widgets
default:
return <div>Error: Widget not found!</div>
}
// if (widget.type === 'CustomAction') {
// return <WidgetCustomAction
// widget={widget}
// data={this.state.icData}
// signals={this.state.signals}
// icIDs={this.state.icIDs}
// />
// } else if (widget.type === 'Action') {
// return <WidgetAction
// widget={widget}
// data={this.state.icData}
// />
// } else if (widget.type === 'Lamp') {
// return <WidgetLamp
// widget={widget}
// data={this.state.icData}
// signals={this.state.signals}
// icIDs={this.state.icIDs}
// />
// } else if (widget.type === 'Value') {
// return <WidgetValue
// widget={widget}
// data={this.state.icData}
// signals={this.state.signals}
// icIDs={this.state.icIDs}
// />
// } else if (widget.type === 'Plot') {
// return <WidgetPlot
// widget={widget}
// data={this.state.icData}
// signals={this.state.signals}
// icIDs={this.state.icIDs}
// paused={this.props.paused}
// />
// } else if (widget.type === 'Table') {
// return <WidgetTable
// widget={widget}
// data={this.state.icData}
// signals={this.state.signals}
// icIDs={this.state.icIDs}
// />
// } else if (widget.type === 'Label') {
// return <WidgetLabel
// widget={widget}
// />
// } else if (widget.type === 'Image') {
// return <WidgetImage
// widget={widget}
// files={this.state.files}
// token={this.state.sessionToken}
// />
// } else if (widget.type === 'Button') {
// return <WidgetButton
// widget={widget}
// editing={this.props.editing}
// onInputChanged={(value, controlID, controlValue, isFinalChange) => this.inputDataChanged(widget, value, controlID, controlValue, isFinalChange)}
// signals={this.state.signals}
// token={this.state.sessionToken}
// />
// } else if (widget.type === 'NumberInput') {
// return <WidgetInput
// widget={widget}
// editing={this.props.editing}
// onInputChanged={(value, controlID, controlValue, isFinalChange) => this.inputDataChanged(widget, value, controlID, controlValue, isFinalChange)}
// signals={this.state.signals}
// token={this.state.sessionToken}
// />
// } else if (widget.type === 'Slider') {
// return <WidgetSlider
// widget={widget}
// editing={this.props.editing}
// onInputChanged={(value, controlID, controlValue, isFinalChange) => this.inputDataChanged(widget, value, controlID, controlValue, isFinalChange)}
// signals={this.state.signals}
// token={this.state.sessionToken}
// />
// } else if (widget.type === 'Gauge') {
// return <WidgetGauge
// widget={widget}
// data={this.state.icData}
// editing={this.props.editing}
// signals={this.state.signals}
// icIDs={this.state.icIDs}
// />
// //} else if (widget.type === 'HTML') {
// //return <WidgetHTML
// // widget={widget}
// // editing={this.props.editing}
// ///>
// } else if (widget.type === 'Topology') {
// return <WidgetTopology
// widget={widget}
// files={this.state.files}
// token={this.state.sessionToken}
// />
// } else if (widget.type === 'TimeOffset') {
// return <WidgetTimeOffset
// widget={widget}
// data={this.state.icData}
// websockets={this.state.websockets}
// ics={this.props.ics}
// />
// } else if (widget.type === 'ICstatus') {
// return <WidgetICstatus
// widget={widget}
// ics={this.props.ics}
// />
// } else if (widget.type === 'Player') {
// return <WidgetPlayer
// widget={widget}
// editing={this.props.editing}
// configs={this.props.configs}
// onStarted={this.props.onSimulationStarted}
// ics={this.props.ics}
// results={this.state.results}
// files={this.state.files}
// scenarioID={this.props.scenarioID}
// />
// }
return null;
}
// class Widget extends React.Component {
// static getStores() {
// return [ ICDataStore, ConfigsStore, FileStore, SignalStore, WebsocketStore, ResultStore];
// }
// static calculateState(prevState, props) {
// let websockets = WebsocketStore.getState();
// let icData = {};
// if (props.paused) {
// if (prevState && prevState.icData) {
// icData = JSON.parse(JSON.stringify(prevState.icData));
// }
// } else {
// icData = ICDataStore.getState();
// }
// // Get the IC IDs and signal indexes for all signals of the widget
// let configs = ConfigsStore.getState().filter(c => c.scenarioID === parseInt(props.scenarioID, 10));
// // TODO make sure that the signals are only the signals that belong to the scenario at hand
// let signals = SignalStore.getState();
// let icIDs = [];
// for (let id of props.data.signalIDs){
// let signal = signals.find(s => s.id === id);
// if (signal !== undefined) {
// let config = configs.find(m => m.id === signal.configID);
// if (config !== undefined){
// icIDs[signal.id] = config.icID;
// }
// }
// }
// let results = ResultStore.getState().filter(r => r.scenarioID === parseInt(props.scenarioID, 10));
// let files = FileStore.getState().filter(f => f.scenarioID === parseInt(props.scenarioID, 10));
// return {
// websockets: websockets,
// icData: icData,
// signals: signals,
// icIDs: icIDs,
// files: files,
// sessionToken: localStorage.getItem("token"),
// results: results,
// };
// }
// inputDataChanged(widget, data, controlID, controlValue, isFinalChange) {
// // controlID is the path to the widget customProperty that is changed (for example 'value')
// // modify the widget customProperty
// if (controlID !== '' && isFinalChange) {
// let updatedWidget = JSON.parse(JSON.stringify(widget));
// updatedWidget.customProperties[controlID] = controlValue;
// AppDispatcher.dispatch({
// type: 'widgets/start-edit',
// token: this.state.sessionToken,
// data: updatedWidget
// });
// }
// // The following assumes that a widget modifies/ uses exactly one signal
// // get the signal with the selected signal ID
// let signalID = widget.signalIDs[0];
// let signal = this.state.signals.filter(s => s.id === signalID)
// if (signal.length === 0){
// console.warn("Unable to send signal for signal ID", signalID, ". Signal not found.");
// return;
// }
// // determine ID of infrastructure component related to signal[0]
// // Remark: there is only one selected signal for an input type widget
// let icID = this.state.icIDs[signal[0].id];
// AppDispatcher.dispatch({
// type: 'icData/inputChanged',
// ic: icID,
// signalID: signal[0].id,
// signalIndex: signal[0].index,
// data: signal[0].scalingFactor * data
// });
// }
// createWidget(widget) {
// if (widget.type === 'CustomAction') {
// return <WidgetCustomAction
// widget={widget}
// data={this.state.icData}
// signals={this.state.signals}
// icIDs={this.state.icIDs}
// />
// } else if (widget.type === 'Action') {
// return <WidgetAction
// widget={widget}
// data={this.state.icData}
// />
// } else if (widget.type === 'Lamp') {
// return <WidgetLamp
// widget={widget}
// data={this.state.icData}
// signals={this.state.signals}
// icIDs={this.state.icIDs}
// />
// } else if (widget.type === 'Value') {
// return <WidgetValue
// widget={widget}
// data={this.state.icData}
// signals={this.state.signals}
// icIDs={this.state.icIDs}
// />
// } else if (widget.type === 'Plot') {
// return <WidgetPlot
// widget={widget}
// data={this.state.icData}
// signals={this.state.signals}
// icIDs={this.state.icIDs}
// paused={this.props.paused}
// />
// } else if (widget.type === 'Table') {
// return <WidgetTable
// widget={widget}
// data={this.state.icData}
// signals={this.state.signals}
// icIDs={this.state.icIDs}
// />
// } else if (widget.type === 'Label') {
// return <WidgetLabel
// widget={widget}
// />
// } else if (widget.type === 'Image') {
// return <WidgetImage
// widget={widget}
// files={this.state.files}
// token={this.state.sessionToken}
// />
// } else if (widget.type === 'Button') {
// return <WidgetButton
// widget={widget}
// editing={this.props.editing}
// onInputChanged={(value, controlID, controlValue, isFinalChange) => this.inputDataChanged(widget, value, controlID, controlValue, isFinalChange)}
// signals={this.state.signals}
// token={this.state.sessionToken}
// />
// } else if (widget.type === 'NumberInput') {
// return <WidgetInput
// widget={widget}
// editing={this.props.editing}
// onInputChanged={(value, controlID, controlValue, isFinalChange) => this.inputDataChanged(widget, value, controlID, controlValue, isFinalChange)}
// signals={this.state.signals}
// token={this.state.sessionToken}
// />
// } else if (widget.type === 'Slider') {
// return <WidgetSlider
// widget={widget}
// editing={this.props.editing}
// onInputChanged={(value, controlID, controlValue, isFinalChange) => this.inputDataChanged(widget, value, controlID, controlValue, isFinalChange)}
// signals={this.state.signals}
// token={this.state.sessionToken}
// />
// } else if (widget.type === 'Gauge') {
// return <WidgetGauge
// widget={widget}
// data={this.state.icData}
// editing={this.props.editing}
// signals={this.state.signals}
// icIDs={this.state.icIDs}
// />
// } else if (widget.type === 'Box') {
// return <WidgetBox
// widget={widget}
// editing={this.props.editing}
// />
// //} else if (widget.type === 'HTML') {
// //return <WidgetHTML
// // widget={widget}
// // editing={this.props.editing}
// ///>
// } else if (widget.type === 'Topology') {
// return <WidgetTopology
// widget={widget}
// files={this.state.files}
// token={this.state.sessionToken}
// />
// } else if (widget.type === 'Line') {
// return <WidgetLine
// 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.props.ics}
// />
// } else if (widget.type === 'ICstatus') {
// return <WidgetICstatus
// widget={widget}
// ics={this.props.ics}
// />
// } else if (widget.type === 'Player') {
// return <WidgetPlayer
// widget={widget}
// editing={this.props.editing}
// configs={this.props.configs}
// onStarted={this.props.onSimulationStarted}
// ics={this.props.ics}
// results={this.state.results}
// files={this.state.files}
// scenarioID={this.props.scenarioID}
// />
// }
// return null;
// }
// render() {
// return this.createWidget(this.props.data);
// }
// }
// let fluxContainerConverter = require('../common/FluxContainerConverter');
// export default Container.create(fluxContainerConverter.convert(Widget), { withProps: true });
export default Widget;

View file

@ -17,8 +17,7 @@
import React, { useState, useEffect } from "react";
import { Badge } from "react-bootstrap";
import { stateLabelStyle } from "../../ic/ics";
import AppDispatcher from "../../common/app-dispatcher";
import {stateLabelStyle} from "../../../infrastructure/styles";
const WidgetICstatus = (props) => {
const [sessionToken, setSessionToken] = useState(
@ -31,11 +30,6 @@ const WidgetICstatus = (props) => {
if (props.ics) {
props.ics.forEach((ic) => {
let icID = parseInt(ic.id, 10);
AppDispatcher.dispatch({
type: "ics/start-load",
data: icID,
token: sessionToken,
});
});
}
};

View file

@ -16,41 +16,43 @@
******************************************************************************/
import React, { useState, useEffect } from "react";
import AppDispatcher from "../../common/app-dispatcher";
const WidgetImage = (props) => {
const [file, setFile] = useState(undefined);
const [file, setFile] = useState(null);
const widget = JSON.parse(JSON.stringify(props.widget));
useEffect(() => {
let widgetFile = props.widget.customProperties.file;
if (widgetFile !== -1 && file === undefined) {
AppDispatcher.dispatch({
type: "files/start-download",
data: widgetFile,
token: props.token,
});
let widgetFile = widget.customProperties.file;
if (widgetFile !== -1 && file === null) {
// AppDispatcher.dispatch({
// type: "files/start-download",
// data: widgetFile,
// token: props.token,
// });
}
}, [file, props.token, props.widget.customProperties.file]);
}, [file, props.token, widget.customProperties.file]);
useEffect(() => {
if (props.widget.customProperties.file === -1) {
props.widget.customProperties.update = false;
if (file !== undefined) setFile(undefined);
if (widget.customProperties.file === -1) {
widget.customProperties.update = false;
if (file !== null) setFile(null);
} else {
console.log("looking in", props.files)
let foundFile = props.files.find(
(f) => f.id === parseInt(props.widget.customProperties.file, 10)
(f) => f.id === parseInt(widget.customProperties.file, 10)
);
if (foundFile && props.widget.customProperties.update) {
props.widget.customProperties.update = false;
AppDispatcher.dispatch({
type: "files/start-download",
data: foundFile.id,
token: props.token,
});
if (foundFile && widget.customProperties.update) {
widget.customProperties.update = false;
// AppDispatcher.dispatch({
// type: "files/start-download",
// data: foundFile.id,
// token: props.token,
// });
setFile(foundFile);
}
}
}, [props.widget.customProperties, props.files, props.token, file]);
}, [widget.customProperties, props.files, props.token, file]);
const imageError = (e) => {
console.error("Image error:", e);

View file

@ -18,18 +18,15 @@
import React, { Component } from 'react';
import { Container, Row, Col } from 'react-bootstrap';
import JSZip from 'jszip';
import IconButton from '../../common/buttons/icon-button';
import IconTextButton from '../../common/buttons/icon-text-button';
import ParametersEditor from '../../common/parameters-editor';
import ICAction from '../../ic/ic-action';
import ResultPythonDialog from "../../pages/scenarios/dialogs/result-python-dialog";
import AppDispatcher from "../../common/app-dispatcher";
import { playerMachine } from '../widget-player/player-machine';
import { interpret } from 'xstate';
const playerService = interpret(playerMachine);
function transitionState(currentState, playerEvent) {

View file

@ -0,0 +1,120 @@
/**
* 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 Plot from '../widget-plot/plot';
import PlotLegend from '../widget-plot/plot-legend';
class WidgetPlot extends React.Component {
constructor(props) {
super(props);
this.state = {
data: [],
signals: []
};
}
static getDerivedStateFromProps(props, state){
let intersection = []
let data = [];
let signalID, sig;
for (signalID of props.widget.signalIDs) {
for (sig of props.signals) {
if (signalID === sig.id) {
intersection.push(sig);
// sig is a selected signal, get data
// determine ID of infrastructure component related to signal (via config)
let icID = props.icIDs[sig.id]
// distinguish between input and output signals
if (sig.direction === "out") {
if (props.data[icID] != null && props.data[icID].output != null && props.data[icID].output.values != null) {
if (props.data[icID].output.values[sig.index] !== undefined) {
let values = props.data[icID].output.values[sig.index];
if(sig.scalingFactor !== 1) {
let scaledValues = JSON.parse(JSON.stringify(values));
for (let i=0; i< scaledValues.length; i++){
scaledValues[i].y = scaledValues[i].y * sig.scalingFactor;
}
data.push(scaledValues);
} else {
data.push(values);
}
}
}
} else if (sig.direction === "in") {
if (props.data[icID] != null && props.data[icID].input != null && props.data[icID].input.values != null) {
if (props.data[icID].input.values[sig.index] !== undefined) {
let values = props.data[icID].output.values[sig.index];
if(sig.scalingFactor !== 1) {
let scaledValues = JSON.parse(JSON.stringify(values));
for (let i=0; i< scaledValues.length; i++){
scaledValues[i].y = scaledValues[i].y * sig.scalingFactor;
}
data.push(scaledValues);
} else {
data.push(values);
}
}
}
}
} // sig is selected signal
} // loop over props.signals
} // loop over selected signals
return {signals: intersection, data: data}
}
//do we need this function?
scaleData(data, scaleFactor){
// data is an array of value pairs x,y
}
render() {
return <div className="plot-widget" ref="wrapper">
<div className="widget-plot">
<Plot
data={this.state.data}
mode={this.props.widget.customProperties.mode || "auto time-scrolling"}
height={this.props.widget.height - 55}
width={this.props.widget.width - 20}
time={this.props.widget.customProperties.time}
samples={this.props.widget.customProperties.nbrSamples || 100}
yMin={this.props.widget.customProperties.yMin}
yMax={this.props.widget.customProperties.yMax}
yUseMinMax={this.props.widget.customProperties.yUseMinMax}
paused={this.props.paused}
yLabel={this.props.widget.customProperties.ylabel}
lineColors={this.props.widget.customProperties.lineColors}
signalIDs={this.props.widget.signalIDs}
/>
</div>
<PlotLegend
signals={this.state.signals}
lineColors={this.props.widget.customProperties.lineColors}
showUnit={this.props.widget.customProperties.showUnit} />
</div>;
}
}
export default WidgetPlot;

View file

@ -20,7 +20,6 @@ import { format } from "d3";
import classNames from "classnames";
import Slider from "rc-slider";
import "rc-slider/assets/index.css";
import AppDispatcher from "../../common/app-dispatcher";
const WidgetSlider = (props) => {
const [value, setValue] = useState("");
@ -29,11 +28,11 @@ const WidgetSlider = (props) => {
useEffect(() => {
let widget = { ...props.widget };
widget.customProperties.simStartedSendValue = false;
AppDispatcher.dispatch({
type: "widgets/start-edit",
token: props.token,
data: widget,
});
// AppDispatcher.dispatch({
// type: "widgets/start-edit",
// token: props.token,
// data: widget,
// });
}, [props.token, props.widget]);
useEffect(() => {

View file

@ -16,7 +16,7 @@
******************************************************************************/
import React, { useState, useEffect } from "react";
import { format } from "d3";
import { Table, DataColumn } from "../../common/table";
import { Table, DataColumn } from "../../../../common/table";
const WidgetTable = (props) => {
const [rows, setRows] = useState([]);

View file

@ -22,10 +22,12 @@ import classNames from 'classnames';
import { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { sessionToken } from '../../localStorage';
import { clearCheckedICs, deleteIC, loadAllICs, sendActionToIC } from '../../store/icSlice';
import { clearCheckedICs, deleteIC, sendActionToIC } from '../../store/icSlice';
import { useGetICSQuery } from '../../store/apiSlice';
const ICActionBoard = (props) => {
const dispatch = useDispatch();
const {refetch: refetchICs} = useGetICSQuery();
const checkedICsIds = useSelector(state => state.infrastructure.checkedICsIds);
let pickedTime = new Date();
@ -60,7 +62,7 @@ const ICActionBoard = (props) => {
});
dispatch(clearCheckedICs());
dispatch(loadAllICs({token: sessionToken}));
refetchICs();
}
const onShutdown = () => {

View file

@ -26,12 +26,14 @@ import moment from 'moment'
import IconToggleButton from "../../common/buttons/icon-toggle-button";
import { updateCheckedICs, openDeleteModal, openEditModal } from "../../store/icSlice";
import { stateLabelStyle } from "./styles";
import { useGetICSQuery } from "../../store/apiSlice";
//a Table of IC components of specific category from props.category
//titled with props.title
const ICCategoryTable = (props) => {
const dispatch = useDispatch();
const ics = useSelector(state => state.infrastructure.ICsArray);
const {data: icsRes, isLoading, refetch: refetchICs} = useGetICSQuery();
const ics = icsRes ? icsRes.ics : [];
const [isGenericDisplayed, setIsGenericDisplayed] = useState(false);
const { user: currentUser } = useSelector((state) => state.auth);

View file

@ -22,6 +22,7 @@ import IconButton from '../../../common/buttons/icon-button';
import ManagedICsTable from "./managed-ics-table";
import { useDispatch } from 'react-redux';
import { loadICbyId } from '../../../store/icSlice';
import { useGetICSQuery } from '../../../store/apiSlice';
import ICParamsTable from '../ic-params-table';
import RawDataTable from '../../../common/rawDataTable';
@ -31,7 +32,8 @@ import { iconStyle, buttonStyle } from "../styles";
const DefaultManagerPage = (props) => {
const ic = props.ic;
const ics = useSelector((state) => state.infrastructure.ICsArray);
const {data: icsRes, isLoading, refetch: refetchICs} = useGetICSQuery();
const ics = icsRes ? icsRes.ics : [];
const { user: currentUser, token: sessionToken } = useSelector((state) => state.auth);

View file

@ -20,6 +20,7 @@ import IconButton from "../../../common/buttons/icon-button";
import RawDataTable from "../../../common/rawDataTable";
import { useDispatch, useSelector } from "react-redux";
import { loadICbyId } from "../../../store/icSlice";
import { useGetICSQuery } from "../../../store/apiSlice";
import ICParamsTable from "../ic-params-table";
@ -33,7 +34,8 @@ const KubernetesICPage = (props) => {
const { user: currentUser, token: sessionToken } = useSelector((state) => state.auth);
const ic = props.ic;
const ics = useSelector((state) => state.infrastructure.ICsArray);
const {data: icsRes, isLoading, refetch: refetchICs} = useGetICSQuery();
const ics = icsRes ? icsRes.ics : [];
const config = useSelector((state) => state.config.config);
//const managedICs = ics.filter(managedIC => managedIC.category !== "manager" && managedIC.manager === ic.uuid);

View file

@ -22,6 +22,7 @@ import ManagedICsTable from "./managed-ics-table";
import RawDataTable from "../../../common/rawDataTable";
import { downloadGraph } from "../../../utils/icUtils";
import { loadICbyId } from "../../../store/icSlice";
import { useGetICSQuery } from "../../../store/apiSlice";
import ICParamsTable from "../ic-params-table";
@ -35,7 +36,8 @@ const ManagerVillasNode = (props) => {
const ic = props.ic;
const ics = useSelector((state) => state.infrastructure.ICsArray);
const {data: icsRes, isLoading, refetch: refetchICs} = useGetICSQuery();
const ics = icsRes ? icsRes.ics : [];
const managedICs = ics.filter(managedIC => managedIC.category !== "manager" && managedIC.manager === ic.uuid);
const graphURL = ic.apiurl !== "" ? ic.apiurl + "/graph.svg" : "";

View file

@ -20,7 +20,8 @@ import IconButton from "../../../common/buttons/icon-button";
import ManagedICsTable from "./managed-ics-table";
import RawDataTable from "../../../common/rawDataTable";
import { useDispatch, useSelector } from "react-redux";
import { loadAllICs, loadICbyId } from "../../../store/icSlice";
import { loadICbyId } from "../../../store/icSlice";
import { useGetICSQuery } from "../../../store/apiSlice";
import ICParamsTable from "../ic-params-table";
@ -34,7 +35,9 @@ const ManagerVillasRelay = (props) => {
const ic = props.ic;
const ics = useSelector((state) => state.infrastructure.ICsArray);
const {data: icsRes, isLoading, refetch: refetchICs} = useGetICSQuery();
const ics = icsRes ? icsRes.ics : [];
const managedICs = ics.filter(managedIC => managedIC.category !== "manager" && managedIC.manager === ic.uuid);
const refresh = () => {

View file

@ -18,7 +18,7 @@
import { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useParams } from 'react-router-dom';
import { loadAllICs, loadICbyId } from "../../store/icSlice";
import { loadICbyId } from "../../store/icSlice";
import { loadConfig } from "../../store/configSlice";
import DefaultManagerPage from "./ic-pages/default-manager-page";
@ -36,10 +36,8 @@ const InfrastructureComponent = (props) => {
const { token: sessionToken } = useSelector((state) => state.auth);
const ic = useSelector(state => state.infrastructure.currentIC);
const isICLoading = useSelector(state => state.infrastructure.isCurrentICLoading);
useEffect(() => {
dispatch(loadAllICs({token: sessionToken}));
dispatch(loadICbyId({token: sessionToken, id: id}));
dispatch(loadConfig({token: sessionToken}));
}, []);

View file

@ -17,7 +17,7 @@
import { useEffect, useState } from "react"
import { useDispatch, useSelector } from "react-redux";
import { loadAllICs, loadICbyId, addIC, sendActionToIC, closeDeleteModal, closeEditModal, editIC, deleteIC } from "../../store/icSlice";
import { addIC, sendActionToIC, closeDeleteModal, closeEditModal, editIC, deleteIC } from "../../store/icSlice";
import IconButton from "../../common/buttons/icon-button";
import ICCategoryTable from "./ic-category-table";
import ICActionBoard from "./ic-action-board";
@ -28,14 +28,16 @@ import EditICDialog from "./dialogs/edit-ic-dialog";
import DeleteDialog from "../../common/dialogs/delete-dialog";
import NotificationsDataManager from "../../common/data-managers/notifications-data-manager";
import NotificationsFactory from "../../common/data-managers/notifications-factory";
import {useGetICSQuery} from '../../store/apiSlice';
const Infrastructure = (props) => {
const Infrastructure = () => {
const dispatch = useDispatch();
const { user: currentUser, token: sessionToken } = useSelector((state) => state.auth);
const ics = useSelector(state => state.infrastructure.ICsArray);
const {data: icsRes, isLoading, refetch: refetchICs} = useGetICSQuery();
const ics = icsRes ? icsRes.ics : [];
const externalICs = ics.filter(ic => ic.managedexternally === true);
//track status of the modals
@ -45,22 +47,14 @@ const Infrastructure = (props) => {
const [checkedICs, setCheckedICs] = useState([]);
useEffect(() => {
//load array of ics and start a timer for periodic refresh
dispatch(loadAllICs({token: sessionToken}));
let timer = window.setInterval(() => refresh(), 10000);
//start a timer for periodic refresh
let timer = window.setInterval(() => refetchICs(), 10000);
return () => {
window.clearInterval(timer);
}
}, []);
const refresh = () => {
//if none of the modals are currently opened, we reload ics array
if(!(isEditModalOpened || isDeleteModalOpened || isICModalOpened)){
dispatch(loadAllICs({token: sessionToken}));
}
}
//modal actions and selectors
const isEditModalOpened = useSelector(state => state.infrastructure.isEditModalOpened);
@ -75,7 +69,7 @@ const Infrastructure = (props) => {
if(data){
if(!data.managedexternally){
dispatch(addIC({token: sessionToken, ic: data})).then(res => dispatch(loadAllICs({token: sessionToken})));
dispatch(addIC({token: sessionToken, ic: data}))
}else {
// externally managed IC: dispatch create action to selected manager
let newAction = {};
@ -91,7 +85,7 @@ const Infrastructure = (props) => {
return;
}
dispatch(sendActionToIC({token: sessionToken, id: managerIC.id, actions: newAction})).then(res => dispatch(loadAllICs({token: sessionToken})));
dispatch(sendActionToIC({token: sessionToken, id: managerIC.id, actions: newAction}))
}
}
}
@ -99,20 +93,20 @@ const Infrastructure = (props) => {
const onImportModalClose = (data) => {
setIsImportModalOpened(false);
dispatch(addIC({token: sessionToken, ic: data})).then(res => dispatch(loadAllICs({token: sessionToken})));
dispatch(addIC({token: sessionToken, ic: data}))
}
const onEditModalClose = (data) => {
if(data){
//some changes where done
dispatch(editIC({token: sessionToken, ic: data})).then(res => dispatch(loadAllICs({token: sessionToken})));
dispatch(editIC({token: sessionToken, ic: data}))
}
dispatch(closeEditModal(data));
}
const onCloseDeleteModal = (isDeleteConfirmed) => {
if(isDeleteConfirmed){
dispatch(deleteIC({token: sessionToken, id:deleteModalIC.id})).then(res => dispatch(loadAllICs({token: sessionToken})));
dispatch(deleteIC({token: sessionToken, id:deleteModalIC.id}))
}
dispatch(closeDeleteModal());
}

View file

@ -1,10 +1,10 @@
import { useState } from "react";
import { useState, useEffect } from "react";
import { Button, Form, OverlayTrigger, Tooltip } from "react-bootstrap";
import { Table, ButtonColumn, CheckboxColumn, DataColumn } from '../../../common/table';
import { dialogWarningLabel, signalDialogCheckButton, buttonStyle } from "../styles";
import Dialog from "../../../common/dialogs/dialog";
import Icon from "../../../common/icon";
import { useGetSignalsQuery, useAddSignalMutation, useDeleteSignalMutation } from "../../../store/apiSlice";
import { useGetSignalsQuery, useAddSignalMutation, useDeleteSignalMutation, useUpdateSignalMutation } from "../../../store/apiSlice";
import { Collapse } from 'react-collapse';
const ExportSignalMappingDialog = ({isShown, direction, onClose, configID}) => {
@ -14,10 +14,44 @@ const ExportSignalMappingDialog = ({isShown, direction, onClose, configID}) => {
const {data, refetch: refetchSignals } = useGetSignalsQuery({configID: configID, direction: direction});
const [addSignalToConfig] = useAddSignalMutation();
const [deleteSignal] = useDeleteSignalMutation();
const [updateSignal] = useUpdateSignalMutation();
const signals = data ? data.signals : [];
const [updatedSignals, setUpdatedSignals] = useState([]);
const [updatedSignalsIDs, setUpdatedSignalsIDs] = useState([]);
useEffect(() => {
if (signals.length > 0) {
setUpdatedSignals([...signals]);
}
}, [signals]);
const handleMappingChange = (e, row, column) => {
console.log(e.target.value, row, column);
const signalToUpdate = {...updatedSignals[row]};
switch (column) {
case 1:
signalToUpdate.index = e.target.value;
break;
case 2:
signalToUpdate.name = e.target.value;
break;
case 3:
signalToUpdate.unit = e.target.value;
break;
case 4:
signalToUpdate.scalingFactor = e.target.value;
break;
default:
break;
}
setUpdatedSignals(prevState =>
prevState.map((signal, index) =>
index === row ? signalToUpdate : signal
)
);
setUpdatedSignalsIDs(prevState => ([signalToUpdate.id, ...prevState]));
}
const handleAdd = async () => {
@ -58,6 +92,24 @@ const ExportSignalMappingDialog = ({isShown, direction, onClose, configID}) => {
refetchSignals();
}
const handleUpdate = async () => {
try {
for (let id of updatedSignalsIDs) {
const signalToUpdate = updatedSignals.find(signal => signal.id === id);
if (signalToUpdate) {
await updateSignal({ signalID: id, updatedSignal: signalToUpdate }).unwrap();
}
}
refetchSignals();
setUpdatedSignalsIDs([]);
} catch (error) {
console.error("Error updating signals:", error);
}
}
const onSignalChecked = (signal, event) => {
if(!checkedSignalsIDs.includes(signal.id)){
setCheckedSignalsIDs(prevState => ([...prevState, signal.id]));
@ -102,14 +154,17 @@ const ExportSignalMappingDialog = ({isShown, direction, onClose, configID}) => {
title={"Edit Signal " + direction +" Mapping"}
buttonTitle="Close"
blendOutCancel = {true}
onClose={(c) => onClose(c)}
onClose={(c) => {
handleUpdate();
onClose(c)
}}
onReset={() => {}}
valid={true}
>
<Form.Group>
<Form.Label style={dialogWarningLabel}>IMPORTANT: Signal configurations that were created before January 2022 have to be fixed manually. Signal indices have to start at 0 and not 1.</Form.Label>
<Form.Label> <i>Click in table cell to edit</i></Form.Label>
<Table breakWord={true} checkbox onChecked={(signal) => onSignalChecked(signal)} data={signals}>
<Table breakWord={true} checkbox onChecked={(signal) => onSignalChecked(signal)} data={updatedSignals}>
<CheckboxColumn
onChecked={(index, event) => onSignalChecked(index, event)}
checked={(signal) => isSignalChecked(signal)}

View file

@ -156,42 +156,40 @@ const ConfigsTable = ({scenario, ics}) => {
const copyConfig = async (configToCopy) => {
let copiedConfig = JSON.parse(JSON.stringify(configToCopy));
try {
const signalsInRes = await triggerGetSignals({configID: configToCopy.id, direction: "in"}, ).unwrap();
const signalsOutRes = await triggerGetSignals({configID: configToCopy.id, direction: "out"}, ).unwrap();
const signalsInRes = await triggerGetSignals({configID: configToCopy.id, direction: "in"}).unwrap();
const signalsOutRes = await triggerGetSignals({configID: configToCopy.id, direction: "out"}).unwrap();
let parsedInSignals = [];
let parsedOutSignals = [];
if(signalsInRes.signals.length > 0){
for(let signal of signalsInRes.signals){
delete signal.configID;
delete signal.id;
parsedInSignals.push(signal);
if (signalsInRes.signals.length > 0) {
for (let signal of signalsInRes.signals) {
const { configID, id, ...rest } = signal;
parsedInSignals.push(rest);
}
}
if(signalsOutRes.signals.length > 0){
for(let signal of signalsOutRes.signals){
delete signal.configID;
delete signal.id;
parsedOutSignals.push(signal);
if (signalsOutRes.signals.length > 0) {
for (let signal of signalsOutRes.signals) {
const { configID, id, ...rest } = signal;
parsedOutSignals.push(rest);
}
}
copiedConfig["inputMapping"] = parsedInSignals;
copiedConfig["outputMapping"] = parsedOutSignals;
delete copiedConfig.id;
delete copiedConfig.scenarioID;
return copiedConfig;
const { id, scenarioID, ...finalConfig } = copiedConfig;
return finalConfig;
} catch (err) {
console.log(err);
return null;
}
}
const handleConfigExport = async (config) => {
try {
@ -234,7 +232,11 @@ const ConfigsTable = ({scenario, ics}) => {
}
}
} catch (err) {
notificationsDataManager.addNotification(NotificationsFactory.UPDATE_ERROR(err.data.message));
if(err.data){
notificationsDataManager.addNotification(NotificationsFactory.UPDATE_ERROR(err.data.message));
} else {
console.log(err)
}
}
refetchConfigs();

View file

@ -10,6 +10,7 @@ import { fileEndpoints } from './endpoints/file-endpoints';
import { signalEndpoints } from './endpoints/signal-endpoints';
import { resultEndpoints } from './endpoints/result-endpoints';
import { authEndpoints } from './endpoints/auth-endpoints';
import { websocketEndpoints } from './endpoints/websocket-endpoints';
export const apiSlice = createApi({
reducerPath: 'api',
@ -34,6 +35,7 @@ export const apiSlice = createApi({
...resultEndpoints(builder),
...signalEndpoints(builder),
...authEndpoints(builder),
...websocketEndpoints(builder),
}),
});
@ -80,4 +82,7 @@ export const {
useDeleteWidgetMutation,
useGetConfigQuery,
useAuthenticateUserMutation,
useLazyGetFilesQuery,
useUpdateSignalMutation,
useGetIcDataQuery
} = apiSlice;

View file

@ -51,4 +51,4 @@ export const loadConfig = createAsyncThunk(
}
);
export default configSlice.reducer;
export default configSlice.reducer;

View file

@ -29,6 +29,13 @@ export const signalEndpoints = (builder) => ({
body: { signal },
}),
}),
updateSignal: builder.mutation({
query: ({ signalID, updatedSignal }) => ({
url: `signals/${signalID}`,
method: 'PUT',
body: { signal: updatedSignal },
}),
}),
deleteSignal: builder.mutation({
query: (signalID) => ({
url: `signals/${signalID}`,

View file

@ -0,0 +1,103 @@
/**
* 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/>.
******************************************************************************/
function setupWebSocket(WS_URL, onMessage, protocol) {
const socket = new WebSocket(WS_URL, protocol); // Include the protocol here
socket.binaryType = 'arraybuffer'; // Set binary type
socket.onmessage = (event) => {
onMessage(event.data);
};
return socket;
}
const sendMessage = (message) => {
if (socket && socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify(message));
}
};
export const websocketEndpoints = (builder) => ({
getIcData: builder.query({
query: () => ({data: []}),
async onCacheEntryAdded(
arg,
{ updateCachedData, cacheDataLoaded, cacheEntryRemoved }
) {
// create a websocket connection when the cache subscription starts
const socket = new WebSocket('wss://villas.k8s.eonerc.rwth-aachen.de/ws/ws_sig', 'live');
socket.binaryType = 'arraybuffer';
try {
// wait for the initial query to resolve before proceeding
await cacheDataLoaded;
// when data is received from the socket connection to the server,
// if it is a message and for the appropriate channel,
// update our query result with the received message
const listener = (event) => {
console.log(event.data)
}
socket.addEventListener('message', listener)
} catch {
// no-op in case `cacheEntryRemoved` resolves before `cacheDataLoaded`,
// in which case `cacheDataLoaded` will throw
}
// cacheEntryRemoved will resolve when the cache subscription is no longer active
await cacheEntryRemoved
// perform cleanup steps once the `cacheEntryRemoved` promise resolves
ws.close()
},
}),
});
function bufferToMessageArray(blob) {
let offset = 0;
const msgs = [];
while (offset < blob.byteLength) {
const msg = bufferToMessage(new DataView(blob, offset));
if (msg !== undefined) {
msgs.push(msg);
offset += msg.blob.byteLength;
}
}
return msgs;
}
function bufferToMessage(data) {
if (data.byteLength === 0) {
return null;
}
const source_index = data.getUint8(1);
const bits = data.getUint8(0);
const length = data.getUint16(0x02, 1);
const bytes = length * 4 + 16;
return {
version: (bits >> OFFSET_VERSION) & 0xF,
type: (bits >> OFFSET_TYPE) & 0x3,
source_index: source_index,
length: length,
sequence: data.getUint32(0x04, 1),
timestamp: data.getUint32(0x08, 1) * 1e3 + data.getUint32(0x0C, 1) * 1e-6,
values: new Float32Array(data.buffer, data.byteOffset + 0x10, length),
blob: new DataView(data.buffer, data.byteOffset + 0x00, bytes),
};
}

View file

@ -77,18 +77,10 @@ const icSlice = createSlice({
closeDeleteModal: (state, args) => {
state.deleteModalIC = null;
state.isDeleteModalOpened = false;
}
},
extraReducers: builder => {
builder
.addCase(loadAllICs.pending, (state, action) => {
state.isLoading = true
})
.addCase(loadAllICs.fulfilled, (state, action) => {
state.ICsArray = action.payload;
console.log("fetched ICs")
})
.addCase(loadICbyId.pending, (state, action) => {
state.isCurrentICLoading = true
})
@ -122,19 +114,6 @@ const icSlice = createSlice({
}
});
//loads all ICs and saves them in the store
export const loadAllICs = createAsyncThunk(
'infrastructure/loadAllICs',
async (data) => {
try {
const res = await RestAPI.get('/api/v2/ic', data.token);
return res.ics;
} catch (error) {
console.log("Error loading ICs data: ", error);
}
}
);
//loads one IC by its id
export const loadICbyId = createAsyncThunk(
'infrastructure/loadICbyId',

View file

@ -20,12 +20,14 @@ import icReducer from './icSlice';
import configReducer from './configSlice'
import { apiSlice } from "./apiSlice";
import authReducer from './authSlice';
import websocketReducer from './websocketSlice';
export const store = configureStore({
reducer: {
auth: authReducer,
infrastructure: icReducer,
config: configReducer,
websocket: websocketReducer,
[apiSlice.reducerPath]: apiSlice.reducer,
},
middleware: (getDefaultMiddleware) =>

134
src/store/websocketSlice.js Normal file
View file

@ -0,0 +1,134 @@
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import { wsManager } from "../common/api/websocket-api";
import { current } from "@reduxjs/toolkit";
export const connectWebSocket = createAsyncThunk(
'websocket/connect',
async ({ url, id, length }, { dispatch }) => {
return new Promise((resolve, reject) => {
wsManager.connect(
url,
(msgs) => {
const icdata = {
input: {
sequence: -1,
length: length,
version: 2,
type: 0,
timestamp: Date.now(),
values: new Array(length).fill(0),
},
output: {
values: [],
},
};
const MAX_VALUES = 10000;
if (msgs.length > 0) {
for (let j = 0; j < msgs.length; j++) {
let smp = msgs[j];
if (smp.source_index !== 0) {
for (let i = 0; i < smp.length; i++) {
while (icdata.input.values.length < i + 1) {
icdata.input.values.push([]);
}
icdata.input.values[i] = smp.values[i];
if (icdata.input.values[i].length > MAX_VALUES) {
const pos = icdata.input.values[i].length - MAX_VALUES;
icdata.input.values[i].splice(0, pos);
}
}
icdata.input.timestamp = smp.timestamp;
icdata.input.sequence = smp.sequence;
} else {
for (let i = 0; i < smp.length; i++) {
while (icdata.output.values.length < i + 1) {
icdata.output.values.push([]);
}
icdata.output.values[i].push({ x: smp.timestamp, y: smp.values[i] });
if (icdata.output.values[i].length > MAX_VALUES) {
const pos = icdata.output.values[i].length - MAX_VALUES;
icdata.output.values[i].splice(0, pos);
}
}
icdata.output.timestamp = smp.timestamp;
icdata.output.sequence = smp.sequence;
}
}
// Dispatch the action to update the Redux state
dispatch(updateIcData({ id, newIcData: icdata }));
}
},
() => {
console.log('WebSocket connected to:', url);
dispatch(setConnectedUrl({ url })); // Optional: Track the connected URL
resolve(); // Resolve the promise on successful connection
},
() => {
console.log('WebSocket disconnected from:', url);
dispatch(disconnect());
reject(); // Reject the promise if the connection is closed
}
);
});
}
);
const websocketSlice = createSlice({
name: 'websocket',
initialState: {
connectedUrl: null,
icdata: {},
},
reducers: {
setConnectedUrl: (state, action) => {
state.connectedUrl = action.payload.url;
},
disconnect: (state) => {
wsManager.disconnect(); // Ensure the WebSocket is disconnected
state.connectedUrl = null;
},
updateIcData: (state, action) => {
const { id, newIcData } = action.payload;
const currentICdata = current(state.icdata);
if(currentICdata[id]){
const {values, ...rest} = newIcData.output;
let oldValues = [...currentICdata[id].output.values];
for(let i = 0; i < newIcData.output.values.length; i++){
oldValues[i] = [...oldValues[i], ...values[i]]
}
state.icdata[id] = {
input: newIcData.input,
output: {
...rest,
values: oldValues
}
}
} else {
state.icdata[id] = {
...newIcData,
};
}
},
},
extraReducers: (builder) => {
builder.addCase(connectWebSocket.fulfilled, (state, action) => {
// Handle the fulfilled state if needed
});
builder.addCase(connectWebSocket.rejected, (state, action) => {
// Handle the rejected state if needed
});
},
});
export const { setConnectedUrl, disconnect, updateIcData } = websocketSlice.actions;
export default websocketSlice.reducer;

View file

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

View file

@ -1,277 +0,0 @@
/**
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* VILLASweb is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import React from 'react';
import { Container } from 'flux/utils';
import AppDispatcher from '../common/app-dispatcher';
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 ResultStore from '../result/result-store';
import WidgetCustomAction from './widgets/custom-action';
import WidgetAction from './widgets/action';
import WidgetLamp from './widgets/lamp';
import WidgetValue from './widgets/value';
import WidgetPlot from './widgets/plot';
import WidgetTable from './widgets/table';
import WidgetLabel from './widgets/label';
import WidgetImage from './widgets/image';
import WidgetButton from './widgets/button';
import WidgetInput from './widgets/input';
import WidgetSlider from './widgets/slider';
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 WidgetPlayer from './widgets/player';
import WidgetICstatus from './widgets/icstatus';
//import WidgetHTML from './widgets/html';
import '../styles/widgets.css';
class Widget extends React.Component {
static getStores() {
return [ ICDataStore, ConfigsStore, FileStore, SignalStore, WebsocketStore, ResultStore];
}
static calculateState(prevState, props) {
let websockets = WebsocketStore.getState();
let icData = {};
if (props.paused) {
if (prevState && prevState.icData) {
icData = JSON.parse(JSON.stringify(prevState.icData));
}
} else {
icData = ICDataStore.getState();
}
// Get the IC IDs and signal indexes for all signals of the widget
let configs = ConfigsStore.getState().filter(c => c.scenarioID === parseInt(props.scenarioID, 10));
// TODO make sure that the signals are only the signals that belong to the scenario at hand
let signals = SignalStore.getState();
let icIDs = [];
for (let id of props.data.signalIDs){
let signal = signals.find(s => s.id === id);
if (signal !== undefined) {
let config = configs.find(m => m.id === signal.configID);
if (config !== undefined){
icIDs[signal.id] = config.icID;
}
}
}
let results = ResultStore.getState().filter(r => r.scenarioID === parseInt(props.scenarioID, 10));
let files = FileStore.getState().filter(f => f.scenarioID === parseInt(props.scenarioID, 10));
return {
websockets: websockets,
icData: icData,
signals: signals,
icIDs: icIDs,
files: files,
sessionToken: localStorage.getItem("token"),
results: results,
};
}
inputDataChanged(widget, data, controlID, controlValue, isFinalChange) {
// controlID is the path to the widget customProperty that is changed (for example 'value')
// modify the widget customProperty
if (controlID !== '' && isFinalChange) {
let updatedWidget = JSON.parse(JSON.stringify(widget));
updatedWidget.customProperties[controlID] = controlValue;
AppDispatcher.dispatch({
type: 'widgets/start-edit',
token: this.state.sessionToken,
data: updatedWidget
});
}
// The following assumes that a widget modifies/ uses exactly one signal
// get the signal with the selected signal ID
let signalID = widget.signalIDs[0];
let signal = this.state.signals.filter(s => s.id === signalID)
if (signal.length === 0){
console.warn("Unable to send signal for signal ID", signalID, ". Signal not found.");
return;
}
// determine ID of infrastructure component related to signal[0]
// Remark: there is only one selected signal for an input type widget
let icID = this.state.icIDs[signal[0].id];
AppDispatcher.dispatch({
type: 'icData/inputChanged',
ic: icID,
signalID: signal[0].id,
signalIndex: signal[0].index,
data: signal[0].scalingFactor * data
});
}
createWidget(widget) {
if (widget.type === 'CustomAction') {
return <WidgetCustomAction
widget={widget}
data={this.state.icData}
signals={this.state.signals}
icIDs={this.state.icIDs}
/>
} else if (widget.type === 'Action') {
return <WidgetAction
widget={widget}
data={this.state.icData}
/>
} else if (widget.type === 'Lamp') {
return <WidgetLamp
widget={widget}
data={this.state.icData}
signals={this.state.signals}
icIDs={this.state.icIDs}
/>
} else if (widget.type === 'Value') {
return <WidgetValue
widget={widget}
data={this.state.icData}
signals={this.state.signals}
icIDs={this.state.icIDs}
/>
} else if (widget.type === 'Plot') {
return <WidgetPlot
widget={widget}
data={this.state.icData}
signals={this.state.signals}
icIDs={this.state.icIDs}
paused={this.props.paused}
/>
} else if (widget.type === 'Table') {
return <WidgetTable
widget={widget}
data={this.state.icData}
signals={this.state.signals}
icIDs={this.state.icIDs}
/>
} else if (widget.type === 'Label') {
return <WidgetLabel
widget={widget}
/>
} else if (widget.type === 'Image') {
return <WidgetImage
widget={widget}
files={this.state.files}
token={this.state.sessionToken}
/>
} else if (widget.type === 'Button') {
return <WidgetButton
widget={widget}
editing={this.props.editing}
onInputChanged={(value, controlID, controlValue, isFinalChange) => this.inputDataChanged(widget, value, controlID, controlValue, isFinalChange)}
signals={this.state.signals}
token={this.state.sessionToken}
/>
} else if (widget.type === 'NumberInput') {
return <WidgetInput
widget={widget}
editing={this.props.editing}
onInputChanged={(value, controlID, controlValue, isFinalChange) => this.inputDataChanged(widget, value, controlID, controlValue, isFinalChange)}
signals={this.state.signals}
token={this.state.sessionToken}
/>
} else if (widget.type === 'Slider') {
return <WidgetSlider
widget={widget}
editing={this.props.editing}
onInputChanged={(value, controlID, controlValue, isFinalChange) => this.inputDataChanged(widget, value, controlID, controlValue, isFinalChange)}
signals={this.state.signals}
token={this.state.sessionToken}
/>
} else if (widget.type === 'Gauge') {
return <WidgetGauge
widget={widget}
data={this.state.icData}
editing={this.props.editing}
signals={this.state.signals}
icIDs={this.state.icIDs}
/>
} else if (widget.type === 'Box') {
return <WidgetBox
widget={widget}
editing={this.props.editing}
/>
//} else if (widget.type === 'HTML') {
//return <WidgetHTML
// widget={widget}
// editing={this.props.editing}
///>
} else if (widget.type === 'Topology') {
return <WidgetTopology
widget={widget}
files={this.state.files}
token={this.state.sessionToken}
/>
} else if (widget.type === 'Line') {
return <WidgetLine
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.props.ics}
/>
} else if (widget.type === 'ICstatus') {
return <WidgetICstatus
widget={widget}
ics={this.props.ics}
/>
} else if (widget.type === 'Player') {
return <WidgetPlayer
widget={widget}
editing={this.props.editing}
configs={this.props.configs}
onStarted={this.props.onSimulationStarted}
ics={this.props.ics}
results={this.state.results}
files={this.state.files}
scenarioID={this.props.scenarioID}
/>
}
return null;
}
render() {
return this.createWidget(this.props.data);
}
}
let fluxContainerConverter = require('../common/FluxContainerConverter');
export default Container.create(fluxContainerConverter.convert(Widget), { withProps: true });

View file

@ -1,29 +0,0 @@
/**
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* VILLASweb is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import RestDataManager from '../common/data-managers/rest-data-manager';
class WidgetsDataManager extends RestDataManager{
constructor() {
super('widget', '/widgets');
}
}
export default new WidgetsDataManager()

View file

@ -1,96 +0,0 @@
/**
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* VILLASweb is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import React, { useState, useEffect } from "react";
import Plot from "../widget-plot/plot";
import PlotLegend from "../widget-plot/plot-legend";
const WidgetPlot = (props) => {
const [data, setData] = useState([]);
const [signals, setSignals] = useState([]);
useEffect(() => {
const intersection = [];
const plotData = [];
let signalID, sig;
for (signalID of props.widget.signalIDs) {
for (sig of props.signals) {
if (signalID === sig.id) {
intersection.push(sig);
// Signal is a selected signal, get data
let icID = props.icIDs[sig.id];
let values = null;
if (
sig.direction === "out" &&
props.data[icID]?.output?.values?.[sig.index] !== undefined
) {
values = props.data[icID].output.values[sig.index];
} else if (
sig.direction === "in" &&
props.data[icID]?.input?.values?.[sig.index] !== undefined
) {
values = props.data[icID].input.values[sig.index];
}
if (values) {
if (sig.scalingFactor !== 1) {
values = values.map((v) => ({
...v,
y: v.y * sig.scalingFactor,
}));
}
plotData.push(values);
}
}
}
}
setData(plotData);
setSignals(intersection);
}, [props.widget.signalIDs, props.signals, props.icIDs, props.data]);
return (
<div className="plot-widget">
<div className="widget-plot">
<Plot
data={data}
mode={props.widget.customProperties.mode || "auto time-scrolling"}
height={props.widget.height - 55}
width={props.widget.width - 20}
time={props.widget.customProperties.time}
samples={props.widget.customProperties.nbrSamples || 100}
yMin={props.widget.customProperties.yMin}
yMax={props.widget.customProperties.yMax}
yUseMinMax={props.widget.customProperties.yUseMinMax}
paused={props.paused}
yLabel={props.widget.customProperties.ylabel}
lineColors={props.widget.customProperties.lineColors}
signalIDs={props.widget.signalIDs}
/>
</div>
<PlotLegend
signals={signals}
lineColors={props.widget.customProperties.lineColors}
showUnit={props.widget.customProperties.showUnit}
/>
</div>
);
};
export default WidgetPlot;