diff --git a/SAS/TMSS/frontend/tmss_webapp/package-lock.json b/SAS/TMSS/frontend/tmss_webapp/package-lock.json index 9b5bad26eaeabec4390b8fc073efd424b097ae0f..50cb006c1af9b9ea004908086540e01fbdb0b02a 100644 --- a/SAS/TMSS/frontend/tmss_webapp/package-lock.json +++ b/SAS/TMSS/frontend/tmss_webapp/package-lock.json @@ -69,6 +69,7 @@ "react-tooltip": "^4.5.1", "react-transition-group": "^2.5.1", "react-websocket": "^2.1.0", + "react-use-websocket": "^3.0.0", "reactstrap": "^9.2.0", "redux": "^4.2.1", "replace-in-file": "^7.0.1", diff --git a/SAS/TMSS/frontend/tmss_webapp/package.json b/SAS/TMSS/frontend/tmss_webapp/package.json index 58c6cd1027acc86472966de92cb96b92bb850ffe..04f4f9578ee3b6f8e0209e146d6e823b3ab5ac0c 100644 --- a/SAS/TMSS/frontend/tmss_webapp/package.json +++ b/SAS/TMSS/frontend/tmss_webapp/package.json @@ -62,6 +62,7 @@ "react-tooltip": "^4.5.1", "react-transition-group": "^2.5.1", "react-websocket": "^2.1.0", + "react-use-websocket": "^3.0.0", "reactstrap": "^9.2.0", "redux": "^4.2.1", "replace-in-file": "^7.0.1", diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/WeekView.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/WeekView.js index 5564120a48b210f538eba4710beaf719d422b380..eba8b8a422a961cf78be432ae23c75b400b1751d 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/WeekView.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/WeekView.js @@ -46,9 +46,14 @@ import { fetchSchedulerStatuses, fetchSunTimings, fetchSUSummaryInformation, fetchTimelineData, - fetchUserPermissions + fetchUserPermissions, } from "./data/week.view.data"; +import useWebSocket from 'react-use-websocket'; +import _ from 'lodash'; +import ScheduleService from "../../services/schedule.service"; +import ReservationService from "../../services/reservation.service"; + function getTimelineHeaders(headerSettings) { return <TimelineHeaders className="sticky"> <SidebarHeader> @@ -243,6 +248,169 @@ export default function WeekView() { } }, [summaryItem]); + // websocket handling + + /** + * Function to call wnen websocket is connected + */ + function onConnect() { + try{ + console.log("WS Opened"); + const userDets = localStorage.getItem("user"); + if (userDets) { + sendMessage(JSON.stringify({"token": JSON.parse(userDets).websocket_token})); + console.log("Auth token submitted"); + } + }catch (err) { + console.log('err',err) + } + } + + /** + * Function to call when websocket is disconnected + */ + function onDisconnect() { + console.log("WS Closed") + } + + /** + * Handles the message received through websocket + * @param {String} data - String of JSON data + */ + function handleData(event) { + const jsonData = JSON.parse(event?.data) || {}; + console.log('received websocket data:', jsonData) + switch (jsonData.object_type) { + case 'scheduling_unit_blueprint': { + switch (jsonData.action) { + case 'delete': { + console.log('ws delete SU'); + const schedulingUnits = data.schedulingUnits + _.remove(schedulingUnits, function (su) { return su.id === jsonData.object_details.id}); + setData(prevData => ({ + ...prevData, + schedulingUnits: schedulingUnits + })) + if (summaryItem?.id === jsonData.object_details.id) { + setSummaryItem({}); + } + break; + } + case 'update': { + console.log("ws update SU"); + setData(prevData => ({ + ...prevData, + schedulingUnits: prevData.schedulingUnits.map( + unit => unit.id === jsonData.object_details.id? {...unit, ...jsonData.object_details}: unit + ) + })); + if (summaryItem?.id === jsonData.object_details.id) { + // Note: we could also update details directly so we don't need to fetch again. + // However, we would have to send full deltas via websocket instead of the subset that is + // relevant for the timeline (like id, timestamps, and status). + // e.g. + // + // setSummarySettings(prevState => ({ + // ...prevState, + // schedulingUnitItem: {...prevState.schedulingUnitItem, ...jsonData.object_details} + // })); + // + // Instead, trigger a full refresh of the details panel: + setSummaryItem({id: jsonData.object_details.id, type: "SCHEDULE"}); + } + break; + } + case 'create': { + console.log("ws create SU"); + // the ws message only contains a subset of the details we need, so fetch the full set + return async (dispatch) => { // todo: async here problematic? + const response = await ScheduleService.getTimelineSlimBlueprints(undefined, undefined, jsonData.object_details.id); // todo: check time + console.log('response:', response); + setData(prevData => ({ + ...prevData, + schedulingUnits: prevData.schedulingUnits.concat(response.results) + })); + }; + break; + } + default: { break; } + } + break; + } + case 'reservation': { + switch (jsonData.action) { + case 'delete': { + console.log('ws delete reservation'); + const reservations = data.reservations + _.remove(reservations, function (res) { return res.id === jsonData.object_details.id}); + setData(prevData => ({ + ...prevData, + reservations: reservations + })) + if (summaryItem?.id === jsonData.object_details.id) { + setSummaryItem({}); + } + break; + } + case 'update': { + console.log("ws update reservation"); + setData(prevData => ({ + ...prevData, + reservations: prevData.reservations.map( + res => res.id === jsonData.object_details.id? {...res, ...jsonData.object_details}: res + ), + })); + if (summaryItem?.id === jsonData.object_details.id) { + // Note: this triggers a full fetch again to get all details (see SU above). + setSummaryItem({id: jsonData.object_details.id, type: "RESERVATION"}); + } + break; + } + case 'create': { + console.log("ws create reservation"); + const shouldFetchReservations = getStore(UIConstants.STORE_KEY_TIMELINE).reservationsToggle; + if (shouldFetchReservations) { + // the ws message only contains a subset of the details we need, so fetch the full set + return async (dispatch) => { // todo: async here problematic? + const response = await ReservationService.getTimelineReservations(undefined, undefined, jsonData.object_details.id); // todo: check time + console.log('response:', response); + setData(prevData => ({ + ...prevData, + reservations: prevData.reservations.concat(response.results) + })); + }; + } + break; + } + default: { break; } + } + break; + } + default: { break; } + } + } + + // debug logging whenever the SU and reservation data gets updated + useEffect(() => { + console.log('data update:', data); + }, [data]) + + // websocket hook that opens and allows interaction via the wss connection + const { + sendMessage, + sendJsonMessage, + lastMessage, + lastJsonMessage, + readyState, + getWebSocket, + } = useWebSocket(process.env.REACT_APP_WEBSOCKET_URL, { + onOpen: () => onConnect(), + onClose: () => onDisconnect(), + onMessage: (event) => handleData(event), + onError: (event) => { console.error(event); }, + shouldReconnect: (closeEvent) => true, + }); + return <div> <div className={(isLoading ? "" : "hide-element")}> <ProgressBar className={isLoading ? "" : "hide-element"} mode="indeterminate"