diff --git a/src/app.js b/src/app.js index 510a649..7940505 100644 --- a/src/app.js +++ b/src/app.js @@ -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; diff --git a/src/common/api/websocket-api.js b/src/common/api/websocket-api.js index e55290a..70e741f 100644 --- a/src/common/api/websocket-api.js +++ b/src/common/api/websocket-api.js @@ -14,101 +14,87 @@ * You should have received a copy of the GNU General Public License * along with VILLASweb. If not, see . ******************************************************************************/ -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(); diff --git a/src/pages/dashboards/dashboard-button-group.js b/src/pages/dashboards/dashboard-button-group.js index 71db45e..04a1500 100644 --- a/src/pages/dashboards/dashboard-button-group.js +++ b/src/pages/dashboards/dashboard-button-group.js @@ -28,7 +28,7 @@ const buttonStyle = { const iconStyle = { height: '25px', width: '25px' -} +}; let buttonkey = 0; diff --git a/src/pages/dashboards/dashboard.js b/src/pages/dashboards/dashboard.js index 971abb8..ce3fcb9 100644 --- a/src/pages/dashboards/dashboard.js +++ b/src/pages/dashboards/dashboard.js @@ -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
Loading...
; -// } + 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
Error. Dashboard not found
; -// } -// return ( -//
-//
-//
-//

-// {dashboard.name} -// -// -// -//

-//
+ 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); + } + } -// -//
-//
e.preventDefault()}> -// {editing && -// -// } + 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); + } + } -// -// {widgets != null && Object.keys(widgets).map(widgetKey => ( -//
-// deleteWidget(widget.id)} -// onChange={editing ? widgetChange : onChange} -// > -// -// -//
-// ))} -//
+ setEditing(false); + setWidgetChangeData([]); + }; -// + 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); + }; -// -//
-//
-// ); -// }; + 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
-} + 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
Loading...
; + } + + if (dashboardError) { + return
Error. Dashboard not found
; + } + + return ( +
+
+
+

+ {dashboard.name} + + + +

+
+ + +
+ +
e.preventDefault()}> + {editing && + + } + + + {widgets != null && Object.keys(widgets).map(widgetKey => ( +
+ deleteWidget(widget.id)} + onChange={editing ? widgetChange : onChange} + > + + +
+ ))} +
+ + + + {/* */} + + +
+
+ ); +}; + +export default Fullscreenable()(Dashboard); diff --git a/src/pages/dashboards/dialogs/edit-file-content.js b/src/pages/dashboards/dialogs/edit-file-content.js new file mode 100644 index 0000000..c14570d --- /dev/null +++ b/src/pages/dashboards/dialogs/edit-file-content.js @@ -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 . + ******************************************************************************/ + +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 this.onClose()} + blendOutCancel = {true} + valid={true} + > + + this.selectUploadFile(event)} /> + + + + + + ; + } +} + +export default EditFileContent; \ No newline at end of file diff --git a/src/pages/dashboards/dialogs/edit-files-dialog.js b/src/pages/dashboards/dialogs/edit-files-dialog.js new file mode 100644 index 0000000..3eacda4 --- /dev/null +++ b/src/pages/dashboards/dialogs/edit-files-dialog.js @@ -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 . + ******************************************************************************/ + +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( + + ) + fileOptions.push(this.props.files.map((file, index) => ( + + ))) + } else { + fileOptions = + } + + const progressBarStyle = { + marginLeft: '100px', + marginTop: '-40px' + }; + + let title = this.props.locked ? "View files of scenario" : "Edit Files of Scenario"; + + return ( + this.onClose()} + blendOutCancel = {true} + valid={true} + > + + + + + + this.deleteFile(index)} + editButton + onEdit={index => this.setState({ editModal: true, modalFile: this.props.files[index] })} + locked={this.props.locked} + /> +
+ +
+
Add file
+ + + this.selectUploadFile(event)} + disabled={this.props.locked} + /> + + + + + + + +
+ +
+ + + + + +
+ + this.closeEditModal(data)} + sessionToken={this.props.sessionToken} + file={this.state.modalFile} + /> +
+ ); + } +} + +export default EditFilesDialog; \ No newline at end of file diff --git a/src/pages/dashboards/widget-area.js b/src/pages/dashboards/widget-area.js index 1e55641..a70fd9c 100644 --- a/src/pages/dashboards/widget-area.js +++ b/src/pages/dashboards/widget-area.js @@ -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) { diff --git a/src/widget/edit-widget/edit-widget-aspect-control.jsx b/src/pages/dashboards/widget/edit-widget/edit-widget-aspect-control.jsx similarity index 100% rename from src/widget/edit-widget/edit-widget-aspect-control.jsx rename to src/pages/dashboards/widget/edit-widget/edit-widget-aspect-control.jsx diff --git a/src/widget/edit-widget/edit-widget-checkbox-control.jsx b/src/pages/dashboards/widget/edit-widget/edit-widget-checkbox-control.jsx similarity index 100% rename from src/widget/edit-widget/edit-widget-checkbox-control.jsx rename to src/pages/dashboards/widget/edit-widget/edit-widget-checkbox-control.jsx diff --git a/src/widget/edit-widget/edit-widget-checkbox-list.jsx b/src/pages/dashboards/widget/edit-widget/edit-widget-checkbox-list.jsx similarity index 100% rename from src/widget/edit-widget/edit-widget-checkbox-list.jsx rename to src/pages/dashboards/widget/edit-widget/edit-widget-checkbox-list.jsx diff --git a/src/widget/edit-widget/edit-widget-color-control.jsx b/src/pages/dashboards/widget/edit-widget/edit-widget-color-control.jsx similarity index 96% rename from src/widget/edit-widget/edit-widget-color-control.jsx rename to src/pages/dashboards/widget/edit-widget/edit-widget-color-control.jsx index 7d3eecb..7cdde20 100644 --- a/src/widget/edit-widget/edit-widget-color-control.jsx +++ b/src/pages/dashboards/widget/edit-widget/edit-widget-color-control.jsx @@ -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); diff --git a/src/widget/edit-widget/edit-widget-color-zones-control.js b/src/pages/dashboards/widget/edit-widget/edit-widget-color-zones-control.js similarity index 98% rename from src/widget/edit-widget/edit-widget-color-zones-control.js rename to src/pages/dashboards/widget/edit-widget/edit-widget-color-zones-control.js index 9f2ff77..da113ce 100644 --- a/src/widget/edit-widget/edit-widget-color-zones-control.js +++ b/src/pages/dashboards/widget/edit-widget/edit-widget-color-zones-control.js @@ -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 { diff --git a/src/widget/edit-widget/edit-widget-config-select.js b/src/pages/dashboards/widget/edit-widget/edit-widget-config-select.js similarity index 100% rename from src/widget/edit-widget/edit-widget-config-select.js rename to src/pages/dashboards/widget/edit-widget/edit-widget-config-select.js diff --git a/src/widget/edit-widget/edit-widget-control-creator.js b/src/pages/dashboards/widget/edit-widget/edit-widget-control-creator.js similarity index 100% rename from src/widget/edit-widget/edit-widget-control-creator.js rename to src/pages/dashboards/widget/edit-widget/edit-widget-control-creator.js diff --git a/src/widget/edit-widget/edit-widget-file-control.jsx b/src/pages/dashboards/widget/edit-widget/edit-widget-file-control.jsx similarity index 100% rename from src/widget/edit-widget/edit-widget-file-control.jsx rename to src/pages/dashboards/widget/edit-widget/edit-widget-file-control.jsx diff --git a/src/widget/edit-widget/edit-widget-html-content.js b/src/pages/dashboards/widget/edit-widget/edit-widget-html-content.js similarity index 100% rename from src/widget/edit-widget/edit-widget-html-content.js rename to src/pages/dashboards/widget/edit-widget/edit-widget-html-content.js diff --git a/src/widget/edit-widget/edit-widget-ic-select.js b/src/pages/dashboards/widget/edit-widget/edit-widget-ic-select.js similarity index 100% rename from src/widget/edit-widget/edit-widget-ic-select.js rename to src/pages/dashboards/widget/edit-widget/edit-widget-ic-select.js diff --git a/src/widget/edit-widget/edit-widget-min-max-control.js b/src/pages/dashboards/widget/edit-widget/edit-widget-min-max-control.js similarity index 100% rename from src/widget/edit-widget/edit-widget-min-max-control.js rename to src/pages/dashboards/widget/edit-widget/edit-widget-min-max-control.js diff --git a/src/widget/edit-widget/edit-widget-number-control.js b/src/pages/dashboards/widget/edit-widget/edit-widget-number-control.js similarity index 100% rename from src/widget/edit-widget/edit-widget-number-control.js rename to src/pages/dashboards/widget/edit-widget/edit-widget-number-control.js diff --git a/src/widget/edit-widget/edit-widget-orientation.js b/src/pages/dashboards/widget/edit-widget/edit-widget-orientation.js similarity index 99% rename from src/widget/edit-widget/edit-widget-orientation.js rename to src/pages/dashboards/widget/edit-widget/edit-widget-orientation.js index 6df5e1f..76e9d8b 100644 --- a/src/widget/edit-widget/edit-widget-orientation.js +++ b/src/pages/dashboards/widget/edit-widget/edit-widget-orientation.js @@ -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 { diff --git a/src/widget/edit-widget/edit-widget-parameters-control.js b/src/pages/dashboards/widget/edit-widget/edit-widget-parameters-control.js similarity index 95% rename from src/widget/edit-widget/edit-widget-parameters-control.js rename to src/pages/dashboards/widget/edit-widget/edit-widget-parameters-control.js index d743017..8f60aa6 100644 --- a/src/widget/edit-widget/edit-widget-parameters-control.js +++ b/src/pages/dashboards/widget/edit-widget/edit-widget-parameters-control.js @@ -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) { diff --git a/src/widget/edit-widget/edit-widget-plot-colors-control.js b/src/pages/dashboards/widget/edit-widget/edit-widget-plot-colors-control.js similarity index 98% rename from src/widget/edit-widget/edit-widget-plot-colors-control.js rename to src/pages/dashboards/widget/edit-widget/edit-widget-plot-colors-control.js index c058f12..5dd9a5e 100644 --- a/src/widget/edit-widget/edit-widget-plot-colors-control.js +++ b/src/pages/dashboards/widget/edit-widget/edit-widget-plot-colors-control.js @@ -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 { diff --git a/src/widget/edit-widget/edit-widget-plot-mode-control.js b/src/pages/dashboards/widget/edit-widget/edit-widget-plot-mode-control.js similarity index 100% rename from src/widget/edit-widget/edit-widget-plot-mode-control.js rename to src/pages/dashboards/widget/edit-widget/edit-widget-plot-mode-control.js diff --git a/src/widget/edit-widget/edit-widget-rotation-control.js b/src/pages/dashboards/widget/edit-widget/edit-widget-rotation-control.js similarity index 100% rename from src/widget/edit-widget/edit-widget-rotation-control.js rename to src/pages/dashboards/widget/edit-widget/edit-widget-rotation-control.js diff --git a/src/widget/edit-widget/edit-widget-samples-control.js b/src/pages/dashboards/widget/edit-widget/edit-widget-samples-control.js similarity index 100% rename from src/widget/edit-widget/edit-widget-samples-control.js rename to src/pages/dashboards/widget/edit-widget/edit-widget-samples-control.js diff --git a/src/widget/edit-widget/edit-widget-signal-control.js b/src/pages/dashboards/widget/edit-widget/edit-widget-signal-control.js similarity index 100% rename from src/widget/edit-widget/edit-widget-signal-control.js rename to src/pages/dashboards/widget/edit-widget/edit-widget-signal-control.js diff --git a/src/widget/edit-widget/edit-widget-signals-control.js b/src/pages/dashboards/widget/edit-widget/edit-widget-signals-control.js similarity index 100% rename from src/widget/edit-widget/edit-widget-signals-control.js rename to src/pages/dashboards/widget/edit-widget/edit-widget-signals-control.js diff --git a/src/widget/edit-widget/edit-widget-text-control.js b/src/pages/dashboards/widget/edit-widget/edit-widget-text-control.js similarity index 100% rename from src/widget/edit-widget/edit-widget-text-control.js rename to src/pages/dashboards/widget/edit-widget/edit-widget-text-control.js diff --git a/src/widget/edit-widget/edit-widget-text-size-control.js b/src/pages/dashboards/widget/edit-widget/edit-widget-text-size-control.js similarity index 100% rename from src/widget/edit-widget/edit-widget-text-size-control.js rename to src/pages/dashboards/widget/edit-widget/edit-widget-text-size-control.js diff --git a/src/widget/edit-widget/edit-widget.js b/src/pages/dashboards/widget/edit-widget/edit-widget.js similarity index 96% rename from src/widget/edit-widget/edit-widget.js rename to src/pages/dashboards/widget/edit-widget/edit-widget.js index 6b2b655..5d5a0ea 100644 --- a/src/widget/edit-widget/edit-widget.js +++ b/src/pages/dashboards/widget/edit-widget/edit-widget.js @@ -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() { diff --git a/src/widget/toolbox-item.jsx b/src/pages/dashboards/widget/toolbox-item.jsx similarity index 97% rename from src/widget/toolbox-item.jsx rename to src/pages/dashboards/widget/toolbox-item.jsx index ac5e834..5df4682 100644 --- a/src/widget/toolbox-item.jsx +++ b/src/pages/dashboards/widget/toolbox-item.jsx @@ -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 = { diff --git a/src/widget/websocket-store.js b/src/pages/dashboards/widget/websocket-store.js similarity index 100% rename from src/widget/websocket-store.js rename to src/pages/dashboards/widget/websocket-store.js diff --git a/src/widget/widget-container.js b/src/pages/dashboards/widget/widget-container.js similarity index 99% rename from src/widget/widget-container.js rename to src/pages/dashboards/widget/widget-container.js index 2b4bcfc..ff336e1 100644 --- a/src/widget/widget-container.js +++ b/src/pages/dashboards/widget/widget-container.js @@ -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 }); } diff --git a/src/widget/widget-context-menu.js b/src/pages/dashboards/widget/widget-context-menu.js similarity index 99% rename from src/widget/widget-context-menu.js rename to src/pages/dashboards/widget/widget-context-menu.js index 3fed074..51d96e8 100644 --- a/src/widget/widget-context-menu.js +++ b/src/pages/dashboards/widget/widget-context-menu.js @@ -145,4 +145,4 @@ WidgetContextMenu.propTypes = { onChange: PropTypes.func.isRequired }; -export default WidgetContextMenu +export default WidgetContextMenu; diff --git a/src/widget/widget-factory.js b/src/pages/dashboards/widget/widget-factory.js similarity index 90% rename from src/widget/widget-factory.js rename to src/pages/dashboards/widget/widget-factory.js index 67e6116..213226c 100644 --- a/src/widget/widget-factory.js +++ b/src/pages/dashboards/widget/widget-factory.js @@ -15,7 +15,7 @@ * along with VILLASweb. If not, see . ******************************************************************************/ -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; diff --git a/src/widget/widget-player/player-machine.js b/src/pages/dashboards/widget/widget-player/player-machine.js similarity index 100% rename from src/widget/widget-player/player-machine.js rename to src/pages/dashboards/widget/widget-player/player-machine.js diff --git a/src/widget/widget-plot/plot-legend.jsx b/src/pages/dashboards/widget/widget-plot/plot-legend.jsx similarity index 100% rename from src/widget/widget-plot/plot-legend.jsx rename to src/pages/dashboards/widget/widget-plot/plot-legend.jsx diff --git a/src/pages/dashboards/widget/widget-plot/plot.jsx b/src/pages/dashboards/widget/widget-plot/plot.jsx new file mode 100644 index 0000000..268382b --- /dev/null +++ b/src/pages/dashboards/widget/widget-plot/plot.jsx @@ -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 . + ******************************************************************************/ + +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

, prevState: Readonly, 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 + }); + + this.setState({ lines, xAxis, yAxis }); + } + + render() { + + const yLabelPos = { + x: 12, + y: this.props.height / 2 + } + + return + select(node).call(this.state.xAxis)} style={{ transform: `translateX(${leftMargin + this.state.labelMargin}px) translateY(${this.props.height + topMargin - bottomMargin}px)` }} /> + select(node).call(this.state.yAxis)} style={{ transform: `translateX(${leftMargin + this.state.labelMargin}px)` }} /> + + {this.props.yLabel} + Time [s] + + + + + + + + + {this.state.lines} + + ; + } +} + +export default Plot; diff --git a/src/widget/widget-store.js b/src/pages/dashboards/widget/widget-store.js similarity index 100% rename from src/widget/widget-store.js rename to src/pages/dashboards/widget/widget-store.js diff --git a/src/widget/widget-toolbox.js b/src/pages/dashboards/widget/widget-toolbox.js similarity index 99% rename from src/widget/widget-toolbox.js rename to src/pages/dashboards/widget/widget-toolbox.js index 3f8f7a6..0812987 100644 --- a/src/widget/widget-toolbox.js +++ b/src/pages/dashboards/widget/widget-toolbox.js @@ -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; diff --git a/src/pages/dashboards/widget/widget.js b/src/pages/dashboards/widget/widget.js new file mode 100644 index 0000000..8dfbe8c --- /dev/null +++ b/src/pages/dashboards/widget/widget.js @@ -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 . + ******************************************************************************/ + +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 + case 'Box': + return + case 'Label': + return ; + case 'Image': + return + //Displaying widgets + case 'Plot': + return + case 'Table': + return + case 'Value': + return + case 'Lamp': + return + case 'Gauge': + return + case 'TimeOffset': + return + case 'ICstatus': + return + //Manipulation widgets + default: + return

Error: Widget not found!
+ } + + + // if (widget.type === 'CustomAction') { + // return + // } else if (widget.type === 'Action') { + // return + // } else if (widget.type === 'Lamp') { + // return + // } else if (widget.type === 'Value') { + // return + // } else if (widget.type === 'Plot') { + // return + // } else if (widget.type === 'Table') { + // return + // } else if (widget.type === 'Label') { + // return + // } else if (widget.type === 'Image') { + // return + // } else if (widget.type === 'Button') { + // return this.inputDataChanged(widget, value, controlID, controlValue, isFinalChange)} + // signals={this.state.signals} + // token={this.state.sessionToken} + // /> + // } else if (widget.type === 'NumberInput') { + // return this.inputDataChanged(widget, value, controlID, controlValue, isFinalChange)} + // signals={this.state.signals} + // token={this.state.sessionToken} + // /> + // } else if (widget.type === 'Slider') { + // return this.inputDataChanged(widget, value, controlID, controlValue, isFinalChange)} + // signals={this.state.signals} + // token={this.state.sessionToken} + // /> + // } else if (widget.type === 'Gauge') { + // return + // //} else if (widget.type === 'HTML') { + // //return + // } else if (widget.type === 'Topology') { + // return + // } else if (widget.type === 'TimeOffset') { + // return + // } else if (widget.type === 'ICstatus') { + // return + // } else if (widget.type === 'Player') { + // return + // } + + 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 +// } else if (widget.type === 'Action') { +// return +// } else if (widget.type === 'Lamp') { +// return +// } else if (widget.type === 'Value') { +// return +// } else if (widget.type === 'Plot') { +// return +// } else if (widget.type === 'Table') { +// return +// } else if (widget.type === 'Label') { +// return +// } else if (widget.type === 'Image') { +// return +// } else if (widget.type === 'Button') { +// return this.inputDataChanged(widget, value, controlID, controlValue, isFinalChange)} +// signals={this.state.signals} +// token={this.state.sessionToken} +// /> +// } else if (widget.type === 'NumberInput') { +// return this.inputDataChanged(widget, value, controlID, controlValue, isFinalChange)} +// signals={this.state.signals} +// token={this.state.sessionToken} +// /> +// } else if (widget.type === 'Slider') { +// return this.inputDataChanged(widget, value, controlID, controlValue, isFinalChange)} +// signals={this.state.signals} +// token={this.state.sessionToken} +// /> +// } else if (widget.type === 'Gauge') { +// return +// } else if (widget.type === 'Box') { +// return +// //} else if (widget.type === 'HTML') { +// //return +// } else if (widget.type === 'Topology') { +// return +// } else if (widget.type === 'Line') { +// return +// } else if (widget.type === 'TimeOffset') { +// return +// } else if (widget.type === 'ICstatus') { +// return +// } else if (widget.type === 'Player') { +// return +// } + +// 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; diff --git a/src/widget/widgets/action.jsx b/src/pages/dashboards/widget/widgets/action.jsx similarity index 100% rename from src/widget/widgets/action.jsx rename to src/pages/dashboards/widget/widgets/action.jsx diff --git a/src/widget/widgets/box.jsx b/src/pages/dashboards/widget/widgets/box.jsx similarity index 100% rename from src/widget/widgets/box.jsx rename to src/pages/dashboards/widget/widgets/box.jsx diff --git a/src/widget/widgets/button.jsx b/src/pages/dashboards/widget/widgets/button.jsx similarity index 100% rename from src/widget/widgets/button.jsx rename to src/pages/dashboards/widget/widgets/button.jsx diff --git a/src/widget/widgets/custom-action.jsx b/src/pages/dashboards/widget/widgets/custom-action.jsx similarity index 100% rename from src/widget/widgets/custom-action.jsx rename to src/pages/dashboards/widget/widgets/custom-action.jsx diff --git a/src/widget/widgets/gauge.jsx b/src/pages/dashboards/widget/widgets/gauge.jsx similarity index 100% rename from src/widget/widgets/gauge.jsx rename to src/pages/dashboards/widget/widgets/gauge.jsx diff --git a/src/widget/widgets/html.jsx b/src/pages/dashboards/widget/widgets/html.jsx similarity index 100% rename from src/widget/widgets/html.jsx rename to src/pages/dashboards/widget/widgets/html.jsx diff --git a/src/widget/widgets/icstatus.jsx b/src/pages/dashboards/widget/widgets/icstatus.jsx similarity index 88% rename from src/widget/widgets/icstatus.jsx rename to src/pages/dashboards/widget/widgets/icstatus.jsx index 19b7dba..943c418 100644 --- a/src/widget/widgets/icstatus.jsx +++ b/src/pages/dashboards/widget/widgets/icstatus.jsx @@ -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, - }); }); } }; diff --git a/src/widget/widgets/image.jsx b/src/pages/dashboards/widget/widgets/image.jsx similarity index 60% rename from src/widget/widgets/image.jsx rename to src/pages/dashboards/widget/widgets/image.jsx index 8b658d8..b5964f6 100644 --- a/src/widget/widgets/image.jsx +++ b/src/pages/dashboards/widget/widgets/image.jsx @@ -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); diff --git a/src/widget/widgets/input.jsx b/src/pages/dashboards/widget/widgets/input.jsx similarity index 100% rename from src/widget/widgets/input.jsx rename to src/pages/dashboards/widget/widgets/input.jsx diff --git a/src/widget/widgets/label.jsx b/src/pages/dashboards/widget/widgets/label.jsx similarity index 100% rename from src/widget/widgets/label.jsx rename to src/pages/dashboards/widget/widgets/label.jsx diff --git a/src/widget/widgets/lamp.jsx b/src/pages/dashboards/widget/widgets/lamp.jsx similarity index 100% rename from src/widget/widgets/lamp.jsx rename to src/pages/dashboards/widget/widgets/lamp.jsx diff --git a/src/widget/widgets/line.jsx b/src/pages/dashboards/widget/widgets/line.jsx similarity index 100% rename from src/widget/widgets/line.jsx rename to src/pages/dashboards/widget/widgets/line.jsx diff --git a/src/widget/widgets/player.js b/src/pages/dashboards/widget/widgets/player.js similarity index 99% rename from src/widget/widgets/player.js rename to src/pages/dashboards/widget/widgets/player.js index 398dd9d..1cee2b4 100644 --- a/src/widget/widgets/player.js +++ b/src/pages/dashboards/widget/widgets/player.js @@ -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) { diff --git a/src/pages/dashboards/widget/widgets/plot.jsx b/src/pages/dashboards/widget/widgets/plot.jsx new file mode 100644 index 0000000..27041b0 --- /dev/null +++ b/src/pages/dashboards/widget/widgets/plot.jsx @@ -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 . + ******************************************************************************/ + +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
+
+ +
+ +
; + } +} + +export default WidgetPlot; diff --git a/src/widget/widgets/slider.jsx b/src/pages/dashboards/widget/widgets/slider.jsx similarity index 95% rename from src/widget/widgets/slider.jsx rename to src/pages/dashboards/widget/widgets/slider.jsx index 89d33fd..7e2a40b 100644 --- a/src/widget/widgets/slider.jsx +++ b/src/pages/dashboards/widget/widgets/slider.jsx @@ -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(() => { diff --git a/src/widget/widgets/table.jsx b/src/pages/dashboards/widget/widgets/table.jsx similarity index 98% rename from src/widget/widgets/table.jsx rename to src/pages/dashboards/widget/widgets/table.jsx index e87fce7..88d546e 100644 --- a/src/widget/widgets/table.jsx +++ b/src/pages/dashboards/widget/widgets/table.jsx @@ -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([]); diff --git a/src/widget/widgets/time-offset.jsx b/src/pages/dashboards/widget/widgets/time-offset.jsx similarity index 100% rename from src/widget/widgets/time-offset.jsx rename to src/pages/dashboards/widget/widgets/time-offset.jsx diff --git a/src/widget/widgets/topology.js b/src/pages/dashboards/widget/widgets/topology.js similarity index 100% rename from src/widget/widgets/topology.js rename to src/pages/dashboards/widget/widgets/topology.js diff --git a/src/widget/widgets/value.jsx b/src/pages/dashboards/widget/widgets/value.jsx similarity index 100% rename from src/widget/widgets/value.jsx rename to src/pages/dashboards/widget/widgets/value.jsx diff --git a/src/pages/infrastructure/ic-action-board.js b/src/pages/infrastructure/ic-action-board.js index f330777..6e90464 100644 --- a/src/pages/infrastructure/ic-action-board.js +++ b/src/pages/infrastructure/ic-action-board.js @@ -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 = () => { diff --git a/src/pages/infrastructure/ic-category-table.js b/src/pages/infrastructure/ic-category-table.js index 59cd821..6b19698 100644 --- a/src/pages/infrastructure/ic-category-table.js +++ b/src/pages/infrastructure/ic-category-table.js @@ -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); diff --git a/src/pages/infrastructure/ic-pages/default-manager-page.js b/src/pages/infrastructure/ic-pages/default-manager-page.js index 2210c53..46c306d 100644 --- a/src/pages/infrastructure/ic-pages/default-manager-page.js +++ b/src/pages/infrastructure/ic-pages/default-manager-page.js @@ -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); diff --git a/src/pages/infrastructure/ic-pages/kubernetes-ic-page.js b/src/pages/infrastructure/ic-pages/kubernetes-ic-page.js index 80d8942..96e7b1e 100644 --- a/src/pages/infrastructure/ic-pages/kubernetes-ic-page.js +++ b/src/pages/infrastructure/ic-pages/kubernetes-ic-page.js @@ -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); diff --git a/src/pages/infrastructure/ic-pages/manager-villas-node.js b/src/pages/infrastructure/ic-pages/manager-villas-node.js index 0446e77..3735722 100644 --- a/src/pages/infrastructure/ic-pages/manager-villas-node.js +++ b/src/pages/infrastructure/ic-pages/manager-villas-node.js @@ -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" : ""; diff --git a/src/pages/infrastructure/ic-pages/manager-villas-relay.js b/src/pages/infrastructure/ic-pages/manager-villas-relay.js index f4f4c0e..0576d8e 100644 --- a/src/pages/infrastructure/ic-pages/manager-villas-relay.js +++ b/src/pages/infrastructure/ic-pages/manager-villas-relay.js @@ -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 = () => { diff --git a/src/pages/infrastructure/ic.js b/src/pages/infrastructure/ic.js index 51480d2..c43980b 100644 --- a/src/pages/infrastructure/ic.js +++ b/src/pages/infrastructure/ic.js @@ -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})); }, []); diff --git a/src/pages/infrastructure/infrastructure.js b/src/pages/infrastructure/infrastructure.js index af2a1fe..0c22168 100644 --- a/src/pages/infrastructure/infrastructure.js +++ b/src/pages/infrastructure/infrastructure.js @@ -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()); } diff --git a/src/pages/scenarios/dialogs/edit-signal-mapping.js b/src/pages/scenarios/dialogs/edit-signal-mapping.js index c416190..7ccaaee 100644 --- a/src/pages/scenarios/dialogs/edit-signal-mapping.js +++ b/src/pages/scenarios/dialogs/edit-signal-mapping.js @@ -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} > IMPORTANT: Signal configurations that were created before January 2022 have to be fixed manually. Signal indices have to start at 0 and not 1. Click in table cell to edit - onSignalChecked(signal)} data={signals}> +
onSignalChecked(signal)} data={updatedSignals}> onSignalChecked(index, event)} checked={(signal) => isSignalChecked(signal)} diff --git a/src/pages/scenarios/tables/configs-table.js b/src/pages/scenarios/tables/configs-table.js index bb171b0..afa7976 100644 --- a/src/pages/scenarios/tables/configs-table.js +++ b/src/pages/scenarios/tables/configs-table.js @@ -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(); diff --git a/src/store/apiSlice.js b/src/store/apiSlice.js index dac5b23..9d8608b 100644 --- a/src/store/apiSlice.js +++ b/src/store/apiSlice.js @@ -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; diff --git a/src/store/configSlice.js b/src/store/configSlice.js index 920fef4..744fcd6 100644 --- a/src/store/configSlice.js +++ b/src/store/configSlice.js @@ -51,4 +51,4 @@ export const loadConfig = createAsyncThunk( } ); -export default configSlice.reducer; \ No newline at end of file +export default configSlice.reducer; diff --git a/src/store/endpoints/signal-endpoints.js b/src/store/endpoints/signal-endpoints.js index 544d969..77ac615 100644 --- a/src/store/endpoints/signal-endpoints.js +++ b/src/store/endpoints/signal-endpoints.js @@ -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}`, diff --git a/src/store/endpoints/websocket-endpoints.js b/src/store/endpoints/websocket-endpoints.js new file mode 100644 index 0000000..cb191c5 --- /dev/null +++ b/src/store/endpoints/websocket-endpoints.js @@ -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 . + ******************************************************************************/ + + +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), + }; +} \ No newline at end of file diff --git a/src/store/icSlice.js b/src/store/icSlice.js index aa4b95b..175b485 100644 --- a/src/store/icSlice.js +++ b/src/store/icSlice.js @@ -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', diff --git a/src/store/index.js b/src/store/index.js index c8acda2..71f8893 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -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) => diff --git a/src/store/websocketSlice.js b/src/store/websocketSlice.js new file mode 100644 index 0000000..d147902 --- /dev/null +++ b/src/store/websocketSlice.js @@ -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; diff --git a/src/widget/widget-plot/plot.jsx b/src/widget/widget-plot/plot.jsx deleted file mode 100644 index 4f48a6b..0000000 --- a/src/widget/widget-plot/plot.jsx +++ /dev/null @@ -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 . - ******************************************************************************/ -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 ( - - - - - {props.yLabel} - - - Time [s] - - - - - - - {lines} - - ); -} - -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; diff --git a/src/widget/widget.js b/src/widget/widget.js deleted file mode 100644 index d51f205..0000000 --- a/src/widget/widget.js +++ /dev/null @@ -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 . - ******************************************************************************/ - -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 - } else if (widget.type === 'Action') { - return - } else if (widget.type === 'Lamp') { - return - } else if (widget.type === 'Value') { - return - } else if (widget.type === 'Plot') { - return - } else if (widget.type === 'Table') { - return - } else if (widget.type === 'Label') { - return - } else if (widget.type === 'Image') { - return - } else if (widget.type === 'Button') { - return this.inputDataChanged(widget, value, controlID, controlValue, isFinalChange)} - signals={this.state.signals} - token={this.state.sessionToken} - /> - } else if (widget.type === 'NumberInput') { - return this.inputDataChanged(widget, value, controlID, controlValue, isFinalChange)} - signals={this.state.signals} - token={this.state.sessionToken} - /> - } else if (widget.type === 'Slider') { - return this.inputDataChanged(widget, value, controlID, controlValue, isFinalChange)} - signals={this.state.signals} - token={this.state.sessionToken} - /> - } else if (widget.type === 'Gauge') { - return - } else if (widget.type === 'Box') { - return - //} else if (widget.type === 'HTML') { - //return - } else if (widget.type === 'Topology') { - return - } else if (widget.type === 'Line') { - return - } else if (widget.type === 'TimeOffset') { - return - } else if (widget.type === 'ICstatus') { - return - } else if (widget.type === 'Player') { - return - } - - return null; - } - - render() { - return this.createWidget(this.props.data); - } -} - -let fluxContainerConverter = require('../common/FluxContainerConverter'); -export default Container.create(fluxContainerConverter.convert(Widget), { withProps: true }); diff --git a/src/widget/widgets-data-manager.js b/src/widget/widgets-data-manager.js deleted file mode 100644 index 61d1cf0..0000000 --- a/src/widget/widgets-data-manager.js +++ /dev/null @@ -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 . - ******************************************************************************/ - - -import RestDataManager from '../common/data-managers/rest-data-manager'; - -class WidgetsDataManager extends RestDataManager{ - - constructor() { - super('widget', '/widgets'); - } - -} - -export default new WidgetsDataManager() diff --git a/src/widget/widgets/plot.jsx b/src/widget/widgets/plot.jsx deleted file mode 100644 index 74d5a16..0000000 --- a/src/widget/widgets/plot.jsx +++ /dev/null @@ -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 . - ******************************************************************************/ - -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 ( -
-
- -
- -
- ); -}; - -export default WidgetPlot;