diff --git a/SAS/TMSS/backend/test/t_subtasks.py b/SAS/TMSS/backend/test/t_subtasks.py index 4fceef5dbcba25badfc1dcd6f798d473432c5833..e8d518742828500a48a65e37e7ef1446c87a9a59 100755 --- a/SAS/TMSS/backend/test/t_subtasks.py +++ b/SAS/TMSS/backend/test/t_subtasks.py @@ -335,125 +335,6 @@ class SubTasksCreationFromTaskBluePrint(unittest.TestCase): self.assertEqual(obs_subtask.output_dataproducts.first(), ingest_subtask.get_transformed_input_dataproduct(ingest_subtask.output_dataproducts.first())) - - -class SubTasksCreationFromTaskBluePrintCalibrator(unittest.TestCase): - - def test_create_sequence_of_subtask_from_task_blueprint_calibrator_failure(self): - """ - Create multiple subtasks from a task blueprint when task is a calibrator - Check that exception should occur due too missing related target observation - """ - task_blueprint = create_task_blueprint_object_for_testing(task_template_name="calibrator observation") - with self.assertRaises(SubtaskCreationException): - create_observation_control_subtask_from_task_blueprint(task_blueprint) - - @unittest.skip("JS 2020-09-08: Cannot reproduce SubtaskCreationException. How is this test supposed to work??") - def test_create_sequence_of_subtask_from_task_blueprint_calibrator(self): - """ - Create multiple subtasks from a task blueprint when task is a calibrator and is related to task blueprint - of a target observation - Check that exception should occur due too missing pointing setting in target observation, - the calibrator default is AutoSelect=True - Check NO exception, when AutoSelect=False - """ - cal_task_blueprint = create_task_blueprint_object_for_testing(task_template_name="calibrator observation") - target_task_blueprint = create_task_blueprint_object_for_testing() - create_scheduling_relation_task_blueprint_for_testing(cal_task_blueprint, target_task_blueprint) - - with self.assertRaises(SubtaskCreationException): - create_observation_control_subtask_from_task_blueprint(cal_task_blueprint) - - cal_task_blueprint.specifications_doc['autoselect'] = False - cal_task_blueprint.specifications_doc['pointing']['angle1'] = 1.111 - cal_task_blueprint.specifications_doc['pointing']['angle2'] = 2.222 - subtask = create_observation_control_subtask_from_task_blueprint(cal_task_blueprint) - self.assertEqual("defined", str(subtask.state)) - self.assertEqual("observation control", str(subtask.specifications_template.name)) - self.assertEqual("observation", str(subtask.specifications_template.type)) - self.assertEqual('J2000', subtask.specifications_doc['stations']['analog_pointing']['direction_type']) - self.assertEqual(1.111, subtask.specifications_doc['stations']['analog_pointing']['angle1']) - self.assertEqual(2.222, subtask.specifications_doc['stations']['analog_pointing']['angle2']) - - def test_create_combined_subtask_from_task_blueprints(self): - """ - Create subtasks from a target task blueprint and a separate calibrator task blueprint. - """ - cal_task_blueprint = create_task_blueprint_object_for_testing(task_template_name="calibrator observation") - target_task_blueprint = create_task_blueprint_object_for_testing() - create_scheduling_relation_task_blueprint_for_testing(cal_task_blueprint, target_task_blueprint, placement='parallel') - - subtask_1 = create_observation_control_subtask_from_task_blueprint(target_task_blueprint) - num_pointings_target = len(subtask_1.specifications_doc['stations']['digital_pointings']) - - # assert target subtask still in defining state - self.assertEqual("defining", str(subtask_1.state)) - self.assertTrue(subtask_1.primary) - - subtask_2 = create_observation_control_subtask_from_task_blueprint(cal_task_blueprint) - - # assert the same subtask is returned - self.assertEqual(subtask_1, subtask_2) - - # assert the calibrator obs was added as an additional beam - num_pointings_calibrator = len(subtask_2.specifications_doc['stations']['digital_pointings']) - self.assertEqual(num_pointings_target + 1, num_pointings_calibrator) - - # assert the subtask is now in defined state - self.assertEqual("defined", str(subtask_2.state)) - self.assertTrue(subtask_2.primary) - - # assert the subtask references both tasks - self.assertEqual(subtask_1.task_blueprints.count(), 2) - self.assertIn(target_task_blueprint, subtask_1.task_blueprints.all()) - self.assertIn(cal_task_blueprint, subtask_1.task_blueprints.all()) - - # assert we have subtask outputs for both tasks - self.assertEqual(subtask_1.outputs.count(), 2) - self.assertEqual(subtask_1.outputs.filter(task_blueprint=target_task_blueprint).count(), 1) - self.assertEqual(subtask_1.outputs.filter(task_blueprint=cal_task_blueprint).count(), 1) - - def test_create_combined_subtask_from_task_blueprints_fails_if_calibrator_handled_before_target(self): - """ - Create subtasks from a target task blueprint and a separate calibrator task blueprint. - Handling calibrator before target task should raise Exception. - """ - cal_task_blueprint = create_task_blueprint_object_for_testing(task_template_name="calibrator observation") - target_task_blueprint = create_task_blueprint_object_for_testing() - create_scheduling_relation_task_blueprint_for_testing(cal_task_blueprint, target_task_blueprint, placement='parallel') - - with self.assertRaises(SubtaskCreationException) as cm: - create_observation_control_subtask_from_task_blueprint(cal_task_blueprint) - create_observation_control_subtask_from_task_blueprint(target_task_blueprint) - - self.assertIn("cannot be added to the target subtask, because it does not exist", str(cm.exception)) - - def test_create_combined_subtask_from_task_blueprints_fails_if_calibrator_does_not_fit(self): - """ - Create subtasks from a target task blueprint and a separate calibrator task blueprint. - And exception is raised when the combined number of subbands exceeds 488. - """ - cal_task_blueprint = create_task_blueprint_object_for_testing(task_template_name="calibrator observation") - target_task_blueprint = create_task_blueprint_object_for_testing() - create_scheduling_relation_task_blueprint_for_testing(cal_task_blueprint, target_task_blueprint, placement='parallel') - - target_task_blueprint.specifications_doc['station_configuration']['SAPs'] = [{'name': 'target1', 'subbands': list(range(0, 150)), - 'digital_pointing': {'angle1': 0.1, 'angle2': 0.1, - 'direction_type': 'J2000', - 'target': 'target1'}}, - {'name': 'target2', 'subbands': list(range(150, 300)), - 'digital_pointing': {'angle1': 0.2, 'angle2': 0.2, - 'direction_type': 'J2000', - 'target': 'target2'}}] - target_task_blueprint.save() - - with self.assertRaises(SubtaskCreationException) as cm: - create_observation_control_subtask_from_task_blueprint(target_task_blueprint) - create_observation_control_subtask_from_task_blueprint(cal_task_blueprint) - - self.assertIn("results in 600 total subbands, but only 488 are possible", str(cm.exception)) - - class SubTasksCreationFromTaskBluePrintCalibrator(unittest.TestCase): def test_create_sequence_of_subtask_from_task_blueprint_calibrator_failure(self): 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 4419d0f6c8ccd33d930fe555e1ab1d047acbe2e8..91b41dd684a7bfdb1f9255ca259a9c0bba2fd0e8 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/WeekView.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/WeekView.js @@ -49,10 +49,7 @@ import { 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"; +import useWeekViewWebSocket from "../../utils/websocket.js"; function getTimelineHeaders(headerSettings) { return <TimelineHeaders className="sticky"> @@ -260,138 +257,12 @@ export default function WeekView() { setWeekString( newStart, newEnd); } - // 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") - } + // websocket handling - /** - * Handles the message received through websocket - * @param {String} data - String of JSON data - */ - function handleData(event) { - const jsonData = JSON.parse(event?.data) || {}; - switch (jsonData.object_type) { - case 'scheduling_unit_blueprint': { - switch (jsonData.action) { - case 'delete': { - 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) { - setShowSummary(false); - } - break; - } - case 'update': { - 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': { - // The websocket message only contains a subset of the details we need, so fetch the full set - ScheduleService.getTimelineSlimBlueprints(undefined, undefined, jsonData.object_details.id) // todo: check time - .then((response) => { - setData(prevData => ({ - ...prevData, - schedulingUnits: prevData.schedulingUnits.concat(response) - })); - }); - break; - } - default: { break; } - } - break; - } - case 'reservation': { - switch (jsonData.action) { - case 'delete': { - 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) { - setShowSummary(false); - } - break; - } - case 'update': { - 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) { - // Trigger a full refresh of the details panel - setSummaryItem({ id: jsonData.object_details.id, type: "RESERVATION" }); - } - break; - } - case 'create': { - const shouldFetchReservations = getStore(UIConstants.STORE_KEY_TIMELINE).reservationsToggle; - if (shouldFetchReservations) { - // The websocket message only contains a subset of the details we need, so fetch the full set - ReservationService.getTimelineReservations(undefined, undefined, jsonData.object_details.id) // todo: check time - .then((response) => { - setData(prevData => ({ - ...prevData, - reservations: prevData.reservations.concat(response) - })); - }); - } - break; - } - default: { break; } - } - break; - } - default: { break; } - } - } + // websocket hook that opens and allows interaction via the wss connection + // todo: there is probably a better way to access the component state from the hook? + useWeekViewWebSocket(data, setData, summaryItem, setSummaryItem, setShowSummary); /** @@ -406,23 +277,6 @@ export default function WeekView() { setShowLegendbar(newState); } - - // 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 ? "fix-element" : "hide-element")}> <ProgressBar className={isLoading ? "" : "hide-element"} mode="indeterminate" diff --git a/SAS/TMSS/frontend/tmss_webapp/src/utils/websocket.js b/SAS/TMSS/frontend/tmss_webapp/src/utils/websocket.js new file mode 100644 index 0000000000000000000000000000000000000000..2c47fff3df0862d238779274641ca8b965eb2b80 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/utils/websocket.js @@ -0,0 +1,183 @@ +import _ from 'lodash'; +import ScheduleService from "../services/schedule.service"; +import ReservationService from "../services/reservation.service"; +import useWebSocket from 'react-use-websocket'; +import { getStore } from "../services/store.helper"; +import UIConstants from "../utils/ui.constants"; + +function useWeekViewWebSocket(data, setData, summaryItem, setSummaryItem, setShowSummary) { + + /** + * Function to call when 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"); + } + + function fetchBlueprintAndAddToTimeline(id) { + // The websocket message only contains a subset of the details we need, so fetch the full set + ScheduleService.getTimelineSlimBlueprints(undefined, undefined, id) // todo: check time + .then((response) => { + setData(prevData => ({ + ...prevData, + schedulingUnits: prevData.schedulingUnits.concat(response) + })); + }) + .catch(e => console.error("Couldn't retrieve scheduling unit details for id: ", id, e)); + } + + + function fetchReservationAndAddToTimeline(id) { + const shouldFetchReservations = getStore(UIConstants.STORE_KEY_TIMELINE).reservationsToggle; + if (shouldFetchReservations) { + ReservationService.getTimelineReservations(undefined, undefined, id) // todo: check time + .then((response) => { + setData(prevData => ({ + ...prevData, + reservations: prevData.reservations.concat(response) + })); + }) + .catch(e => console.error("Couldn't retrieve reservation details for id: ", id, e)); + } + } + + /** + * Handles the message received through websocket + * @param {String} data - String of JSON data + */ + function handleData(event) { + const jsonData = JSON.parse(event?.data) || {}; + switch (jsonData.object_type) { + case 'scheduling_unit_blueprint': { + switch (jsonData.action) { + case 'delete': { + 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) { + setShowSummary(false); + } + break; + } + case 'update': { + if (data.schedulingUnits.some(unit => unit.id === jsonData.object_details.id)) { + // usually we already have most details, so only update the relevant ones. + setData(prevData => ({ + ...prevData, + schedulingUnits: prevData.schedulingUnits.map( + unit => unit.id === jsonData.object_details.id ? { ...unit, ...jsonData.object_details } : unit + ) + })); + } else { + // ...but sometimes we don't have the details yet, e.g. because it at least used to be + // outside the timelines time range, and we need to fetch all details so we don't miss + // anything that e.g. got moved into scope. + fetchBlueprintAndAddToTimeline(jsonData.object_details.id); + } + + 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': { + // The websocket message only contains a subset of the details we need, so fetch the full set + fetchBlueprintAndAddToTimeline(jsonData.object_details.id); + break; + } + default: { break; } + } + break; + } + case 'reservation': { + switch (jsonData.action) { + case 'delete': { + 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) { + setShowSummary(false); + } + break; + } + case 'update': { + if (data.reservations.some(res => res.id === jsonData.object_details.id)) { + // usually we already have most details, so only update the relevant ones. + setData(prevData => ({ + ...prevData, + reservations: prevData.reservations.map( + res => res.id === jsonData.object_details.id ? { ...res, ...jsonData.object_details } : res + ), + })); + } else { + // ...but sometimes we don't have the details yet, e.g. because it at least used to be + // outside the timelines time range, and we need to fetch all details so we don't miss + // anything that e.g. got moved into scope. + fetchReservationAndAddToTimeline(jsonData.object_details.id); + } + if (summaryItem?.id === jsonData.object_details.id) { + // Trigger a full refresh of the details panel + setSummaryItem({ id: jsonData.object_details.id, type: "RESERVATION" }); + } + break; + } + case 'create': { + // The websocket message only contains a subset of the details we need, so fetch the full set + fetchReservationAndAddToTimeline(jsonData.object_details.id); + break; + } + default: { break; } + } + break; + } + default: { break; } + } + } + + // websocket hook that opens and allows interaction via the wss connection + const { + sendMessage + } = useWebSocket(process.env.REACT_APP_WEBSOCKET_URL, { + onOpen: () => onConnect(), + onClose: () => onDisconnect(), + onMessage: (event) => handleData(event), + onError: (event) => { console.error(event); }, + shouldReconnect: (closeEvent) => true, + }); + +} + +export default useWeekViewWebSocket; \ No newline at end of file