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:
parent
4d97b629cc
commit
bfcc11ebae
81 changed files with 2114 additions and 1231 deletions
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -28,7 +28,7 @@ const buttonStyle = {
|
|||
const iconStyle = {
|
||||
height: '25px',
|
||||
width: '25px'
|
||||
}
|
||||
};
|
||||
|
||||
let buttonkey = 0;
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
82
src/pages/dashboards/dialogs/edit-file-content.js
Normal file
82
src/pages/dashboards/dialogs/edit-file-content.js
Normal 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;
|
192
src/pages/dashboards/dialogs/edit-files-dialog.js
Normal file
192
src/pages/dashboards/dialogs/edit-files-dialog.js
Normal 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;
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
|
@ -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 {
|
|
@ -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 {
|
|
@ -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) {
|
|
@ -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 {
|
|
@ -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() {
|
|
@ -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 = {
|
|
@ -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 });
|
||||
}
|
|
@ -145,4 +145,4 @@ WidgetContextMenu.propTypes = {
|
|||
onChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default WidgetContextMenu
|
||||
export default WidgetContextMenu;
|
|
@ -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;
|
283
src/pages/dashboards/widget/widget-plot/plot.jsx
Normal file
283
src/pages/dashboards/widget/widget-plot/plot.jsx
Normal 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;
|
|
@ -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;
|
452
src/pages/dashboards/widget/widget.js
Normal file
452
src/pages/dashboards/widget/widget.js
Normal 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;
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
|
@ -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);
|
|
@ -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) {
|
120
src/pages/dashboards/widget/widgets/plot.jsx
Normal file
120
src/pages/dashboards/widget/widgets/plot.jsx
Normal 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;
|
|
@ -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(() => {
|
|
@ -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([]);
|
|
@ -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 = () => {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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" : "";
|
||||
|
||||
|
|
|
@ -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 = () => {
|
||||
|
|
|
@ -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}));
|
||||
}, []);
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -51,4 +51,4 @@ export const loadConfig = createAsyncThunk(
|
|||
}
|
||||
);
|
||||
|
||||
export default configSlice.reducer;
|
||||
export default configSlice.reducer;
|
||||
|
|
|
@ -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}`,
|
||||
|
|
103
src/store/endpoints/websocket-endpoints.js
Normal file
103
src/store/endpoints/websocket-endpoints.js
Normal 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),
|
||||
};
|
||||
}
|
|
@ -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',
|
||||
|
|
|
@ -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
134
src/store/websocketSlice.js
Normal 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;
|
|
@ -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;
|
|
@ -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 });
|
|
@ -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()
|
|
@ -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;
|
Loading…
Add table
Reference in a new issue