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/pages/dashboards/widget/widgets/button.jsx b/src/pages/dashboards/widget/widgets/button.jsx
index 7dced13..2c285f4 100644
--- a/src/pages/dashboards/widget/widgets/button.jsx
+++ b/src/pages/dashboards/widget/widgets/button.jsx
@@ -17,7 +17,6 @@
import React, { useState, useEffect } from 'react';
import { Button } from 'react-bootstrap';
-import AppDispatcher from '../../common/app-dispatcher';
const WidgetButton = (props) => {
const [pressed, setPressed] = useState(props.widget.customProperties.pressed);
@@ -27,11 +26,11 @@ const WidgetButton = (props) => {
widget.customProperties.simStartedSendValue = false;
widget.customProperties.pressed = false;
- AppDispatcher.dispatch({
- type: 'widgets/start-edit',
- token: props.token,
- data: widget
- });
+ // AppDispatcher.dispatch({
+ // type: 'widgets/start-edit',
+ // token: props.token,
+ // data: widget
+ // });
// Effect cleanup
return () => {
diff --git a/src/pages/dashboards/widget/widgets/image.jsx b/src/pages/dashboards/widget/widgets/image.jsx
index b5964f6..a9a6ee4 100644
--- a/src/pages/dashboards/widget/widgets/image.jsx
+++ b/src/pages/dashboards/widget/widgets/image.jsx
@@ -16,20 +16,36 @@
******************************************************************************/
import React, { useState, useEffect } from "react";
+import { useLazyDownloadImageQuery } from "../../../../store/apiSlice";
+import FileSaver from "file-saver";
const WidgetImage = (props) => {
const [file, setFile] = useState(null);
+ const [objectURL, setObjectURL] = useState("");
const widget = JSON.parse(JSON.stringify(props.widget));
+ const [triggerDownloadImage] = useLazyDownloadImageQuery();
+
+ const handleDownloadFile = async (fileID) => {
+ try {
+ const res = await triggerDownloadImage(fileID);
+ const blob = await res.data; // This is where you get the blob directly
+ setObjectURL(URL.createObjectURL(blob))
+ } catch (error) {
+ console.error(`Failed to download file with ID ${fileID}`, error);
+ }
+ }
+
+ useEffect(() => {
+ if(file !== null){
+ handleDownloadFile(file.id);
+ }
+ }, [file])
+
useEffect(() => {
let widgetFile = widget.customProperties.file;
if (widgetFile !== -1 && file === null) {
- // AppDispatcher.dispatch({
- // type: "files/start-download",
- // data: widgetFile,
- // token: props.token,
- // });
}
}, [file, props.token, widget.customProperties.file]);
@@ -38,17 +54,11 @@ const WidgetImage = (props) => {
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(widget.customProperties.file, 10)
);
if (foundFile && widget.customProperties.update) {
widget.customProperties.update = false;
- // AppDispatcher.dispatch({
- // type: "files/start-download",
- // data: foundFile.id,
- // token: props.token,
- // });
setFile(foundFile);
}
}
@@ -58,7 +68,14 @@ const WidgetImage = (props) => {
console.error("Image error:", e);
};
- let objectURL = file && file.objectURL ? file.objectURL : "";
+ //revoke object url when component unmounts
+ useEffect(() => {
+ return () => {
+ if (objectURL) {
+ URL.revokeObjectURL(objectURL);
+ }
+ };
+}, [objectURL]);
return (
diff --git a/src/pages/dashboards/widget/widgets/input.jsx b/src/pages/dashboards/widget/widgets/input.jsx
index 732c9f9..938e2d7 100644
--- a/src/pages/dashboards/widget/widgets/input.jsx
+++ b/src/pages/dashboards/widget/widgets/input.jsx
@@ -16,7 +16,6 @@
******************************************************************************/
import React, { useState, useEffect } from "react";
import { Form, Col, InputGroup } from "react-bootstrap";
-import AppDispatcher from "../../common/app-dispatcher";
function WidgetInput(props) {
const [value, setValue] = useState("");
@@ -26,11 +25,11 @@ function WidgetInput(props) {
const 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/pages/dashboards/widget/widgets/player.js b/src/pages/dashboards/widget/widgets/player.js
index 1cee2b4..100bf1a 100644
--- a/src/pages/dashboards/widget/widgets/player.js
+++ b/src/pages/dashboards/widget/widgets/player.js
@@ -18,12 +18,10 @@
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 IconButton from '../../../../common/buttons/icon-button';
+import IconTextButton from '../../../../common/buttons/icon-text-button';
+import ParametersEditor from '../../../../common/parameters-editor';
+import ResultPythonDialog from '../../../scenarios/dialogs/result-python-dialog';
import { playerMachine } from '../widget-player/player-machine';
import { interpret } from 'xstate';
@@ -33,7 +31,6 @@ function transitionState(currentState, playerEvent) {
return playerMachine.transition(currentState, { type: playerEvent })
}
-
class WidgetPlayer extends Component {
constructor(props) {
super(props);
@@ -131,17 +128,17 @@ class WidgetPlayer extends Component {
switch (state.ic.state) {
case 'stopping': // if configured, show results
if (state.uploadResults) {
- AppDispatcher.dispatch({
- type: 'results/start-load',
- param: '?scenarioID=' + props.scenarioID,
- token: state.sessionToken,
- })
+ // AppDispatcher.dispatch({
+ // type: 'results/start-load',
+ // param: '?scenarioID=' + props.scenarioID,
+ // token: state.sessionToken,
+ // })
- AppDispatcher.dispatch({
- type: 'files/start-load',
- token: state.sessionToken,
- param: '?scenarioID=' + props.scenarioID,
- });
+ // AppDispatcher.dispatch({
+ // type: 'files/start-load',
+ // token: state.sessionToken,
+ // param: '?scenarioID=' + props.scenarioID,
+ // });
}
newState = transitionState(state.playerState, 'FINISH')
return { playerState: newState, icState: state.ic.state }
@@ -163,14 +160,14 @@ class WidgetPlayer extends Component {
clickStart() {
let config = this.state.config
config.startParameters = this.state.startParameters
- ICAction.start([config], '{}', [this.state.ic], new Date(), this.state.sessionToken, this.state.uploadResults)
+ // dispatch(sendActionToIC({token: sessionToken, id: id, actions: newAction}));
let newState = transitionState(this.state.playerState, 'START')
this.setState({ playerState: newState })
}
clickReset() {
- ICAction.reset(this.state.ic.id, new Date(), this.state.sessionToken)
+ //dispatch(sendActionToIC({token: sessionToken, id: id, actions: newAction}));
}
openPythonDialog() {
@@ -197,11 +194,11 @@ class WidgetPlayer extends Component {
}
toDownload.forEach(fileid => {
- AppDispatcher.dispatch({
- type: 'files/start-download',
- data: fileid,
- token: this.state.sessionToken
- });
+ // AppDispatcher.dispatch({
+ // type: 'files/start-download',
+ // data: fileid,
+ // token: this.state.sessionToken
+ // });
});
this.setState({ filesToDownload: toDownload });
diff --git a/src/pages/dashboards/widget/widgets/slider.jsx b/src/pages/dashboards/widget/widgets/slider.jsx
index 7e2a40b..7c6a262 100644
--- a/src/pages/dashboards/widget/widgets/slider.jsx
+++ b/src/pages/dashboards/widget/widgets/slider.jsx
@@ -40,11 +40,11 @@ const WidgetSlider = (props) => {
if (props.widget.customProperties.simStartedSendValue) {
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,
+ // });
// Send value without changing widget
props.onInputChanged(widget.customProperties.value, "", "", false);
diff --git a/src/store/apiSlice.js b/src/store/apiSlice.js
index 9d8608b..3080f15 100644
--- a/src/store/apiSlice.js
+++ b/src/store/apiSlice.js
@@ -84,5 +84,7 @@ export const {
useAuthenticateUserMutation,
useLazyGetFilesQuery,
useUpdateSignalMutation,
- useGetIcDataQuery
+ useGetIcDataQuery,
+ useLazyDownloadImageQuery,
+ useUpdateComponentConfigMutation
} = apiSlice;
diff --git a/src/store/endpoints/file-endpoints.js b/src/store/endpoints/file-endpoints.js
index 7810b19..765c03f 100644
--- a/src/store/endpoints/file-endpoints.js
+++ b/src/store/endpoints/file-endpoints.js
@@ -40,6 +40,13 @@ export const fileEndpoints = (builder) => ({
responseType: 'blob',
}),
}),
+ downloadImage: builder.query({
+ query: (fileID) => ({
+ url: `files/${fileID}`,
+ method: 'GET',
+ responseHandler: (response) => response.blob(),
+ }),
+ }),
updateFile: builder.mutation({
query: ({ fileID, file }) => {
const formData = new FormData();
diff --git a/src/store/websocketSlice.js b/src/store/websocketSlice.js
index d147902..240ddcb 100644
--- a/src/store/websocketSlice.js
+++ b/src/store/websocketSlice.js
@@ -1,14 +1,39 @@
+/**
+ * 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 { 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 }) => {
+ async ({ url, id, length }, { dispatch, getState }) => {
+
+ console.log('Want to connect to', url);
+
+ //check if we are already connected to this socket
+ if(getState().websocket.activeSocketURLs.includes(url)) return;
+
return new Promise((resolve, reject) => {
+ dispatch(addActiveSocket({parameters: {id: id, url: url, length: length}}));
wsManager.connect(
+ id,
url,
- (msgs) => {
+ (msgs, id) => {
const icdata = {
input: {
sequence: -1,
@@ -63,14 +88,13 @@ export const connectWebSocket = createAsyncThunk(
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
},
() => {
@@ -86,21 +110,30 @@ export const connectWebSocket = createAsyncThunk(
const websocketSlice = createSlice({
name: 'websocket',
initialState: {
- connectedUrl: null,
icdata: {},
+ activeSocketURLs: []
},
reducers: {
- setConnectedUrl: (state, action) => {
- state.connectedUrl = action.payload.url;
+ addActiveSocket: (state, action) => {
+ const {url, id, length} = action.payload.parameters;
+ const currentSockets = current(state.activeSocketURLs);
+ state.activeSocketURLs = [...currentSockets, url];
+ state.icdata[id] = {input: {
+ sequence: -1,
+ length: length,
+ version: 2,
+ type: 0,
+ timestamp: Date.now(),
+ values: new Array(length).fill(0)
+ }, output: {}};
},
- disconnect: (state) => {
- wsManager.disconnect(); // Ensure the WebSocket is disconnected
- state.connectedUrl = null;
+ disconnect: (state, action) => {
+ wsManager.disconnect(action.payload.id); // Ensure the WebSocket is disconnected
},
updateIcData: (state, action) => {
const { id, newIcData } = action.payload;
const currentICdata = current(state.icdata);
- if(currentICdata[id]){
+ if(currentICdata[id].output.values){
const {values, ...rest} = newIcData.output;
let oldValues = [...currentICdata[id].output.values];
for(let i = 0; i < newIcData.output.values.length; i++){
@@ -119,16 +152,36 @@ const websocketSlice = createSlice({
};
}
},
+ sendMessageToWebSocket: (state, action) => {
+ const { ic, signalID, signalIndex, data} = action.payload.message;
+ const currentICdata = current(state.icdata);
+
+ if (!(ic == null || currentICdata[ic].input == null)) {
+ const inputAction = JSON.parse(JSON.stringify(currentICdata[ic].input));
+ // update message properties
+ inputAction.timestamp = Date.now();
+ inputAction.sequence++;
+ inputAction.values[signalIndex] = data;
+ inputAction.length = inputAction.values.length;
+ inputAction.source_index = signalID;
+ // The previous line sets the source_index field of the message to the ID of the signal
+ // so that upon loopback through VILLASrelay the value can be mapped to correct signal
+
+ state.icdata[ic].input = inputAction;
+ let input = JSON.parse(JSON.stringify(inputAction));
+ wsManager.send(ic, input);
+ }
+ }
},
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
+ console.log('error', action);
});
},
});
-export const { setConnectedUrl, disconnect, updateIcData } = websocketSlice.actions;
+export const { disconnect, updateIcData, addActiveSocket, sendMessageToWebSocket } = websocketSlice.actions;
export default websocketSlice.reducer;