diff --git a/SAS/TMSS/backend/services/CMakeLists.txt b/SAS/TMSS/backend/services/CMakeLists.txt index 8b345edf35df93e8b6b44a28369e0b7da436bf14..cae17112663c207a295defd76d05918c1b7b8156 100644 --- a/SAS/TMSS/backend/services/CMakeLists.txt +++ b/SAS/TMSS/backend/services/CMakeLists.txt @@ -9,5 +9,5 @@ lofar_add_package(TMSSLTAAdapter tmss_lta_adapter) lofar_add_package(TMSSRAAdapter tmss_ra_adapter) lofar_add_package(TMSSSlackWebhookService slackwebhook) lofar_add_package(TMSSPreCalculationsService precalculations_service) - +lofar_add_package(TMSSSipGenerationService sip_generation) diff --git a/SAS/TMSS/backend/services/sip_generation/CMakeLists.txt b/SAS/TMSS/backend/services/sip_generation/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..b6d77ab43ad0c83b806f805980a61fa14ecf928a --- /dev/null +++ b/SAS/TMSS/backend/services/sip_generation/CMakeLists.txt @@ -0,0 +1,10 @@ +lofar_package(TMSSSipGenerationService 0.1 DEPENDS TMSSClient PyCommon pyparameterset PyMessaging) + +lofar_find_package(PythonInterp 3.4 REQUIRED) + +IF(NOT SKIP_TMSS_BUILD) + add_subdirectory(lib) + add_subdirectory(test) +ENDIF(NOT SKIP_TMSS_BUILD) + +add_subdirectory(bin) diff --git a/SAS/TMSS/backend/services/sip_generation/bin/CMakeLists.txt b/SAS/TMSS/backend/services/sip_generation/bin/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..5e36034574d8d80101fe6796cbb3e8d245c07a0f --- /dev/null +++ b/SAS/TMSS/backend/services/sip_generation/bin/CMakeLists.txt @@ -0,0 +1,4 @@ +lofar_add_bin_scripts(tmss_sip_generation_service) + +# supervisord config files +lofar_add_sysconf_files(tmss_sip_generation_service.ini DESTINATION supervisord.d) diff --git a/SAS/TMSS/backend/services/sip_generation/bin/tmss_sip_generation_service b/SAS/TMSS/backend/services/sip_generation/bin/tmss_sip_generation_service new file mode 100755 index 0000000000000000000000000000000000000000..bf0a492cba21e3c11c930c873864b7c3c422d8e9 --- /dev/null +++ b/SAS/TMSS/backend/services/sip_generation/bin/tmss_sip_generation_service @@ -0,0 +1,24 @@ +#!/usr/bin/python3 + +# Copyright (C) 2012-2015 ASTRON (Netherlands Institute for Radio Astronomy) +# P.O. Box 2, 7990 AA Dwingeloo, The Netherlands +# +# This file is part of the LOFAR software suite. +# The LOFAR software suite 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. +# +# The LOFAR software suite 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 the LOFAR software suite. If not, see <http://www.gnu.org/licenses/>. + + +from lofar.sas.tmss.services.sip_generation import main + +if __name__ == "__main__": + main() diff --git a/SAS/TMSS/backend/services/sip_generation/bin/tmss_sip_generation_service.ini b/SAS/TMSS/backend/services/sip_generation/bin/tmss_sip_generation_service.ini new file mode 100644 index 0000000000000000000000000000000000000000..33fb5faab83fb34997905700cd7f899351f99267 --- /dev/null +++ b/SAS/TMSS/backend/services/sip_generation/bin/tmss_sip_generation_service.ini @@ -0,0 +1,9 @@ +[program:tmss_sip_generation_service] +command=docker run --rm -u 7149:7149 -v /opt/lofar/var/log:/opt/lofar/var/log -v /tmp/tmp -v /etc/passwd:/etc/passwd:ro -v /etc/group:/etc/group:ro -v /localhome/lofarsys:/localhome/lofarsys --env-file /localhome/lofarsys/.lofar/.lofar_env -e HOME=/localhome/lofarsys -e USER=lofarsys nexus.cep4.control.lofar:18080/tmss_django:latest /bin/bash -c 'source ~/.lofar/.lofar_env;source $LOFARROOT/lofarinit.sh;exec tmss_sip_generation_service' +user=lofarsys +stopsignal=INT ; KeyboardInterrupt +stopasgroup=true ; bash does not propagate signals +stdout_logfile=%(program_name)s.log +redirect_stderr=true +stderr_logfile=NONE +stdout_logfile_maxbytes=0 diff --git a/SAS/TMSS/backend/services/sip_generation/lib/CMakeLists.txt b/SAS/TMSS/backend/services/sip_generation/lib/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..f74123066248fd6fd8d0ffb7886a4b9a3ea8b205 --- /dev/null +++ b/SAS/TMSS/backend/services/sip_generation/lib/CMakeLists.txt @@ -0,0 +1,10 @@ +lofar_find_package(PythonInterp 3.4 REQUIRED) +include(PythonInstall) + +set(_py_files + sip_generation.py + ) + +python_install(${_py_files} + DESTINATION lofar/sas/tmss/services) + diff --git a/SAS/TMSS/backend/services/sip_generation/lib/sip_generation.py b/SAS/TMSS/backend/services/sip_generation/lib/sip_generation.py new file mode 100644 index 0000000000000000000000000000000000000000..ddb1bb093c83864f68c1f53f516a671a339238ea --- /dev/null +++ b/SAS/TMSS/backend/services/sip_generation/lib/sip_generation.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 + +# sip_generation.py +# +# Copyright (C) 2015 +# ASTRON (Netherlands Institute for Radio Astronomy) +# P.O.Box 2, 7990 AA Dwingeloo, The Netherlands +# +# This file is part of the LOFAR software suite. +# The LOFAR software suite 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. +# +# The LOFAR software suite 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 the LOFAR software suite. If not, see <http://www.gnu.org/licenses/>. +# +# $Id: sip_generation.py 1580 2015-09-30 14:18:57Z loose $ + +""" +The Sip generation service generates a Sip for each dataproduct when their TMSS subtask finished. +It listens on the lofar notification message bus for state changes of TMSS subtasks; when a task finished, +it loops over all dataproducts and generates a Sip (rest action) one at a time, and puts them in a lookup table. +This is to avoid peaks of load on the system during Ingest, where many Sips are requested in bulk. +""" + +import os +from optparse import OptionParser +import logging +logger = logging.getLogger(__name__) + +from lofar.sas.tmss.client.tmssbuslistener import * +from lofar.sas.tmss.client.tmss_http_rest_client import TMSSsession + +class TMSSSipGenerationEventMessageHandler(TMSSEventMessageHandler): + ''' + ''' + def __init__(self, tmss_client_credentials_id: str=None): + super().__init__() + self.tmss_client = TMSSsession.create_from_dbcreds_for_ldap(tmss_client_credentials_id) + + def start_handling(self): + self.tmss_client.open() + super().start_handling() + + def stop_handling(self): + super().stop_handling() + self.tmss_client.close() + + def onSubTaskStatusChanged(self, id: int, status: str): + super().onSubTaskStatusChanged(id, status) + + if status == "finished": + dataproducts = self.tmss_client.get_subtask_output_dataproducts(id) + dataproduct_ids = sorted([d['id'] for d in dataproducts]) + + logger.info("subtask %s finished. trying to generate SIPs for its dataproducts: %s", + id, ', '.join(str(id) for id in dataproduct_ids) or 'None') + + for dataproduct_id in dataproduct_ids: + try: + self.tmss_client.get_dataproduct_SIP(dataproduct_id) + except Exception as e: + logger.error(f'Error when generating SIP for dataproduct id={dataproduct_id}: {e}') + + +def create_sip_generation_service(exchange: str=DEFAULT_BUSNAME, broker: str=DEFAULT_BROKER, tmss_client_credentials_id: str=None): + return TMSSBusListener(handler_type=TMSSSipGenerationEventMessageHandler, + handler_kwargs={'tmss_client_credentials_id': tmss_client_credentials_id}, + exchange=exchange, + broker=broker) + +def main(): + # make sure we run in UTC timezone + os.environ['TZ'] = 'UTC' + + logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s', level=logging.INFO) + + # Check the invocation arguments + parser = OptionParser('%prog [options]', description='run the tmss_sip_generation_service which automatically triggers generation of SIPs for dataproducts of finished subtasks (so that load is reduced during ingest)') + parser.add_option('-q', '--broker', dest='broker', type='string', default=DEFAULT_BROKER, help='Address of the messaging broker, default: %default') + parser.add_option('--exchange', dest='exchange', type='string', default=DEFAULT_BUSNAME, help='Name of the exchange on the messaging broker, default: %default') + parser.add_option('-t', '--tmss_client_credentials_id', dest='tmss_client_credentials_id', type='string', + default=os.environ.get("TMSS_CLIENT_DBCREDENTIALS", "TMSSClient"), + help='the credentials id for the file in ~/.lofar/dbcredentials which holds the TMSS http REST api url and credentials, default: %default') + (options, args) = parser.parse_args() + + with create_sip_generation_service(options.exchange, options.broker, options.tmss_client_credentials_id): + waitForInterrupt() + +if __name__ == '__main__': + main() diff --git a/SAS/TMSS/backend/services/sip_generation/test/CMakeLists.txt b/SAS/TMSS/backend/services/sip_generation/test/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..476e284b6dc219bce4ff96102cd03ff67089642e --- /dev/null +++ b/SAS/TMSS/backend/services/sip_generation/test/CMakeLists.txt @@ -0,0 +1,7 @@ +# $Id: CMakeLists.txt 32679 2022-01-06 15:00:00Z jkuensem $ + +if(BUILD_TESTING) + include(LofarCTest) + + lofar_add_test(t_sip_generation_service) +endif() diff --git a/SAS/TMSS/backend/services/sip_generation/test/t_sip_generation_service.py b/SAS/TMSS/backend/services/sip_generation/test/t_sip_generation_service.py new file mode 100755 index 0000000000000000000000000000000000000000..3aab47018a17268a2854513e1c23f1b28be020fe --- /dev/null +++ b/SAS/TMSS/backend/services/sip_generation/test/t_sip_generation_service.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python3 + +# Copyright (C) 2012-2015 ASTRON (Netherlands Institute for Radio Astronomy) +# P.O. Box 2, 7990 AA Dwingeloo, The Netherlands +# +# This file is part of the LOFAR software suite. +# The LOFAR software suite 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. +# +# The LOFAR software suite 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 the LOFAR software suite. If not, see <http://www.gnu.org/licenses/>. + +import unittest +import uuid + +import logging +logger = logging.getLogger('lofar.'+__name__) + +from lofar.common.test_utils import skip_integration_tests +if skip_integration_tests(): + exit(3) + +from lofar.messaging.messagebus import TemporaryExchange, BusListenerJanitor +from lofar.sas.tmss.services.sip_generation import create_sip_generation_service + +import time + +class TestSipGenerationService(unittest.TestCase): + ''' + Tests for the SipGenerationService + ''' + + # feedback doc (needs %-formatting with subband number) + feedback_doc = """{"percentage_written": 0, + "frequency": {"subbands": [%s], + "beamlet_indices": [%s], + "central_frequencies": [102734375.0], + "channel_width": 3051.757812, + "channels_per_subband": 64}, + "time": {"start_time": "2021-01-29T12:39:00Z", + "duration": 0.0, + "sample_width": 1.006633}, + "antennas": {"set": "HBA_DUAL", + "fields": [{"station": "CS001", + "field": "HBA", + "type": "HBA"}, + {"station": "CS001", + "field": "HBA1", + "type": "HBA"}]}, + "target": {"pointing": {"direction_type": "J2000", + "angle1": 0.1, + "angle2": 0.2, + "target": "my_source"}, + "coherent": true}, + "samples": {"polarisations": ["XX", "XY", "YX", "YY"], + "type": "float", + "bits": 32, + "writer": "lofarstman", + "writer_version": "3", + "complex": true}, + "$schema": "http://127.0.0.1:8000/api/schemas/dataproductfeedbacktemplate/feedback/1#", + "files": [] + }""" + + @classmethod + def setUpClass(cls) -> None: + cls.TEST_UUID = uuid.uuid1() + + cls.tmp_exchange = TemporaryExchange("%s_%s" % (cls.__name__, cls.TEST_UUID)) + cls.tmp_exchange.open() + + # override DEFAULT_BUSNAME + import lofar + lofar.messaging.config.DEFAULT_BUSNAME = cls.tmp_exchange.address + + # import here, and not at top of module, because DEFAULT_BUSNAME needs to be set before importing + from lofar.sas.tmss.test.test_environment import TMSSTestEnvironment + + cls.tmss_test_env = TMSSTestEnvironment(exchange=cls.tmp_exchange.address, populate_schemas=True, start_sip_generation_service=True, start_postgres_listener=True) + cls.tmss_test_env.start() + + from lofar.sas.tmss.test.tmss_test_data_rest import TMSSRESTTestDataCreator + cls.test_data_creator = TMSSRESTTestDataCreator(cls.tmss_test_env.django_server.url, + (cls.tmss_test_env.ldap_server.dbcreds.user, + cls.tmss_test_env.ldap_server.dbcreds.password)) + + @classmethod + def tearDownClass(cls) -> None: + cls.tmss_test_env.stop() + cls.tmp_exchange.close() + + def test_sip_generation_service_generates_sip_when_subtask_finished(self): + # create and start the service (the object under test) + service = create_sip_generation_service(exchange=self.tmp_exchange.address, tmss_client_credentials_id=self.tmss_test_env.client_credentials.dbcreds_id) + with BusListenerJanitor(service): + with self.tmss_test_env.create_tmss_client() as tmss_client: + # create a subtask with some output dataproducts + dataproduct_feedback_templates = tmss_client.get_path_as_json_object('dataproduct_feedback_template') + empty_dataproduct_feedback_template = next(x for x in dataproduct_feedback_templates if x['name']=='empty') + + dataproduct_specifications_templates = tmss_client.get_path_as_json_object('dataproduct_specifications_template') + visibilities_specifications_template = next(x for x in dataproduct_specifications_templates if x['name']=='visibilities') + + subtask_templates = tmss_client.get_path_as_json_object('subtask_template') + obs_subtask_template = next(x for x in subtask_templates if x['name']=='observation control') + + subtask = self.test_data_creator.post_data_and_get_response_as_json_object(self.test_data_creator.Subtask(specifications_template_url=obs_subtask_template['url']), '/subtask/') + subtask_id = subtask['id'] + subtask_output = self.test_data_creator.post_data_and_get_response_as_json_object(self.test_data_creator.SubtaskOutput(subtask_url=subtask['url']), '/subtask_output/') + NUM_DATAPRODUCTS = 4 + for i in range(NUM_DATAPRODUCTS): + sap_template_url = tmss_client.get_path_as_json_object('sap_template/1')['url'] + sap_url = self.test_data_creator.post_data_and_get_response_as_json_object(self.test_data_creator.SAP(specifications_template_url=sap_template_url), '/sap/')['url'] + self.test_data_creator.post_data_and_get_response_as_json_object(self.test_data_creator.Dataproduct(subtask_output_url=subtask_output['url'], + filename="L%d_SAP000_SB%03d_uv.MS" % (subtask_id, i), + specifications_template_url=visibilities_specifications_template['url'], + dataproduct_feedback_template_url=empty_dataproduct_feedback_template['url'], + dataproduct_feedback_doc=self.feedback_doc % (i,i), + sap_url=sap_url), + '/dataproduct/') + + # check that there are initially no cached SIPs for the dataproducts + dataproducts = tmss_client.get_subtask_output_dataproducts(subtask_id=subtask_id) + self.assertEqual(NUM_DATAPRODUCTS, len(dataproducts)) + SIPs = tmss_client.get_path_as_json_object('sip') + num_SIPs = len(SIPs) + for dataproduct in dataproducts: + self.assertNotIn(dataproduct['filename'], str(SIPs)) + + # set subtask state to finished to trigger service + from lofar.sas.tmss.test.test_utils import set_subtask_state_following_allowed_transitions, Subtask + set_subtask_state_following_allowed_transitions(Subtask.objects.get(id=subtask_id), 'finished') + + # wait for service to trigger generation of SIPs + for i in range(10): + SIPs = tmss_client.get_path_as_json_object('sip') + if len(SIPs) == num_SIPs + NUM_DATAPRODUCTS: + break + time.sleep(5) + + # check there now is a SIP for each dataproduct in the table + self.assertEqual(len(SIPs), num_SIPs + NUM_DATAPRODUCTS) + for dataproduct in dataproducts: + self.assertIn(dataproduct['filename'], str(SIPs)) + +logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s', level=logging.INFO) + +if __name__ == '__main__': + #run the unit tests + unittest.main() diff --git a/SAS/TMSS/backend/services/sip_generation/test/t_sip_generation_service.run b/SAS/TMSS/backend/services/sip_generation/test/t_sip_generation_service.run new file mode 100755 index 0000000000000000000000000000000000000000..06d799539c80e8f0467354bad7b00c504e45b234 --- /dev/null +++ b/SAS/TMSS/backend/services/sip_generation/test/t_sip_generation_service.run @@ -0,0 +1,6 @@ +#!/bin/bash + +# Run the unit test +source python-coverage.sh +python_coverage_test "*tmss*" t_sip_generation_service.py + diff --git a/SAS/TMSS/backend/services/sip_generation/test/t_sip_generation_service.sh b/SAS/TMSS/backend/services/sip_generation/test/t_sip_generation_service.sh new file mode 100755 index 0000000000000000000000000000000000000000..97f5e765549d1b5670af320428dc93ace9b4db22 --- /dev/null +++ b/SAS/TMSS/backend/services/sip_generation/test/t_sip_generation_service.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +./runctest.sh t_sip_generation_service \ No newline at end of file diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/adapters/sip.py b/SAS/TMSS/backend/src/tmss/tmssapp/adapters/sip.py index f78897af448bf150b1c4e5dcb4fe92766c4191f5..71ce09b999440a16961954a9cd129f021e6ad934 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/adapters/sip.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/adapters/sip.py @@ -1,5 +1,5 @@ from lofar.sas.tmss.tmss.exceptions import * -from lofar.sas.tmss.tmss.tmssapp.models.scheduling import Dataproduct, SubtaskType, Subtask, SubtaskOutput, SubtaskState, SIPidentifier, HashAlgorithm +from lofar.sas.tmss.tmss.tmssapp.models.scheduling import Dataproduct, SubtaskType, Subtask, SubtaskOutput, SubtaskState, SIPidentifier, HashAlgorithm, SIP from lofar.sas.tmss.tmss.tmssapp.models.specification import Datatype, Dataformat from lofar.lta.sip import siplib, ltasip, validator, constants from lofar.common.json_utils import add_defaults_to_json_object_for_schema @@ -768,3 +768,9 @@ def generate_sip_for_dataproduct(dataproduct): validator.check_consistency(sip) return sip + +def get_or_create_sip_xml_for_dataproduct(dataproduct): + '''look up the sip document for the provided dataproduct (generate it first if it does not yet exist)''' + if not SIP.objects.filter(dataproduct=dataproduct).exists(): + SIP.objects.create(dataproduct=dataproduct, sip=generate_sip_for_dataproduct(dataproduct).get_prettyxml()) + return SIP.objects.get(dataproduct=dataproduct).sip diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/migrations/0022_sip.py b/SAS/TMSS/backend/src/tmss/tmssapp/migrations/0022_sip.py new file mode 100644 index 0000000000000000000000000000000000000000..cf0f4b02c8e9a2a7b0742091dd9126114e4bdbfe --- /dev/null +++ b/SAS/TMSS/backend/src/tmss/tmssapp/migrations/0022_sip.py @@ -0,0 +1,22 @@ +# Generated by Django 3.0.9 on 2022-01-07 16:19 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('tmssapp', '0021_subtask_immutables'), + ] + + operations = [ + migrations.CreateModel( + name='SIP', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('sip', models.CharField(help_text='The SIP in XML form as text', max_length=1048576, null=True)), + ('dataproduct', models.OneToOneField(help_text='The dataproduct that this SIP describes.', on_delete=django.db.models.deletion.PROTECT, related_name='sip', to='tmssapp.Dataproduct')), + ], + ), + ] diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/models/scheduling.py b/SAS/TMSS/backend/src/tmss/tmssapp/models/scheduling.py index 2fa88b67bf48e503eb1a3d9d98927b7f7444d66d..c1c48af4c87aa8a95a791bc3d0a79d2335382795 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/models/scheduling.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/models/scheduling.py @@ -140,7 +140,12 @@ class SIPidentifier(Model): if model._state.adding: model.global_parset_identifier = SIPidentifier.objects.create(source="TMSS") - +class SIP(Model): + '''A SIP (Submission Information Package) is an XML document that contains provenance info of a dataproduct + and is required to ingest data into the LTA. While these documents can be generated on-the-fly, we keep them + in a table so that we can (pre-)generate them independently from the ingest itself''' + dataproduct = OneToOneField('Dataproduct', related_name='sip', on_delete=PROTECT, help_text='The dataproduct that this SIP describes.') + sip = CharField(null=True, max_length=1048576, help_text='The SIP in XML form as text') # # Instance Objects diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/serializers/scheduling.py b/SAS/TMSS/backend/src/tmss/tmssapp/serializers/scheduling.py index b74a6f19739746f018195f5511c29c692546cae8..88c0c9435e33df96af134a81e45c1d0297c1a8c7 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/serializers/scheduling.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/serializers/scheduling.py @@ -199,3 +199,8 @@ class SIPidentifierSerializer(serializers.HyperlinkedModelSerializer): model = models.SIPidentifier fields = ['unique_identifier', 'source', 'url'] + +class SIPSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = models.SIP + fields = '__all__' diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/scheduling.py b/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/scheduling.py index 4d4f906597733f7520537b83ef9f32c21a410677..dd955448b66546b08438653f89357f366cd828f1 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/scheduling.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/scheduling.py @@ -406,15 +406,15 @@ class DataproductViewSet(LOFARViewSet): operation_description="Get the Submission Information Package (SIP) for this dataproduct") @action(methods=['get'], detail=True, url_name="sip") def sip(self, request, pk=None): - from lofar.sas.tmss.tmss.tmssapp.adapters.sip import generate_sip_for_dataproduct + from lofar.sas.tmss.tmss.tmssapp.adapters.sip import get_or_create_sip_xml_for_dataproduct from lofar.sas.tmss.tmss.tmssapp import views from django.urls import reverse # get the dataproduct... dataproduct = get_object_or_404(models.Dataproduct, pk=pk) - # generate the sip - sip = generate_sip_for_dataproduct(dataproduct).get_prettyxml() + # get a sip document for the dataproduct + sip = get_or_create_sip_xml_for_dataproduct(dataproduct) # construct the schema location for the sip lta_sip_xsd_path = reverse(views.get_lta_sip_xsd) @@ -531,3 +531,8 @@ class SAPTemplateViewSet(AbstractTemplateViewSet): class SIPidentifierViewSet(LOFARViewSet): queryset = models.SIPidentifier.objects.all() serializer_class = serializers.SIPidentifierSerializer + + +class SIPViewSet(LOFARViewSet): + queryset = models.SIP.objects.all() + serializer_class = serializers.SIPSerializer diff --git a/SAS/TMSS/backend/src/tmss/urls.py b/SAS/TMSS/backend/src/tmss/urls.py index bf6ccac25858b8ed51b4a7642b165662c38fb069..9d1f5cd501e52af39d3ae030edbdd9e0129595ae 100644 --- a/SAS/TMSS/backend/src/tmss/urls.py +++ b/SAS/TMSS/backend/src/tmss/urls.py @@ -227,6 +227,7 @@ router.register(r'subtask_state_log', viewsets.SubtaskStateLogViewSet) router.register(r'user', viewsets.UserViewSet) router.register(r'sap', viewsets.SAPViewSet) router.register(r'sip_identifier', viewsets.SIPidentifierViewSet) +router.register(r'sip', viewsets.SIPViewSet) # PERMISSIONS diff --git a/SAS/TMSS/backend/test/t_adapter.py b/SAS/TMSS/backend/test/t_adapter.py index 5206c95229d9a23520d3f66c762612dd89972bff..d1dabe4c6ca8eda84b0f94993e07a5c1aba53604 100755 --- a/SAS/TMSS/backend/test/t_adapter.py +++ b/SAS/TMSS/backend/test/t_adapter.py @@ -66,7 +66,7 @@ from lofar.sas.tmss.tmss.workflowapp.models.schedulingunitflow import Scheduling from lofar.sas.tmss.tmss.exceptions import SubtaskInvalidStateException from lofar.sas.tmss.tmss.tmssapp.adapters.parset import convert_to_parset, convert_to_parset_dict, _order_beamformer_dataproducts from lofar.common.json_utils import get_default_json_object_for_schema, add_defaults_to_json_object_for_schema, resolved_remote_refs -from lofar.sas.tmss.tmss.tmssapp.adapters.sip import generate_sip_for_dataproduct, create_sip_representation_for_dataproduct +from lofar.sas.tmss.tmss.tmssapp.adapters.sip import generate_sip_for_dataproduct, get_or_create_sip_xml_for_dataproduct, create_sip_representation_for_dataproduct from lofar.lta.sip import constants from lofar.sas.tmss.test.test_utils import set_subtask_state_following_allowed_transitions from lofar.sas.tmss.tmss.tmssapp.tasks import update_task_graph_from_specifications_doc, create_scheduling_unit_blueprint_and_tasks_and_subtasks_from_scheduling_unit_draft @@ -737,8 +737,9 @@ class SIPadapterTest(unittest.TestCase): # create their SIPs (separate loop since we needed to clear the cache in between): for i in range(10): dataproduct = main_dataproducts[i] - sip = generate_sip_for_dataproduct(dataproduct) - prettyxml = sip.get_prettyxml() + self.assertEqual(models.SIP.objects.filter(dataproduct=dataproduct).count(), 0) + prettyxml = get_or_create_sip_xml_for_dataproduct(dataproduct) + self.assertEqual(models.SIP.objects.filter(dataproduct=dataproduct).count(), 1) self.assertIn(str('<fileName>my_related_dataproduct_42'), prettyxml) self.assertIn(str(f'<fileName>my_main_dataproduct_{i}'), prettyxml) self.assertNotIn(str(f'<fileName>my_main_dataproduct_{i+1}'), prettyxml) diff --git a/SAS/TMSS/backend/test/test_environment.py b/SAS/TMSS/backend/test/test_environment.py index 4d29e96ae01509172653e3f32127fbdfa5624b16..6bdac1eaed87ddbb2e5520f11453c93820c7c84a 100644 --- a/SAS/TMSS/backend/test/test_environment.py +++ b/SAS/TMSS/backend/test/test_environment.py @@ -234,6 +234,7 @@ class TMSSTestEnvironment: start_feedback_service: bool=False, start_workflow_service: bool=False, enable_viewflow: bool=False, start_precalculations_service: bool=False, + start_sip_generation_service: bool=False, ldap_dbcreds_id: str=None, db_dbcreds_id: str=None, client_dbcreds_id: str=None): self._exchange = exchange self._broker = broker @@ -280,6 +281,9 @@ class TMSSTestEnvironment: self._start_precalculations_service = start_precalculations_service self.precalculations_service = None + self._start_sip_generation_service = start_sip_generation_service + self.sip_generation_service = None + # Check for correct Django version, should be at least 3.0 if django.VERSION[0] < 3: print("\nWARNING: YOU ARE USING DJANGO VERSION '%s', WHICH WILL NOT SUPPORT ALL FEATURES IN TMSS!\n" % @@ -366,6 +370,12 @@ class TMSSTestEnvironment: except Exception as e: logger.exception(e) + if self._start_sip_generation_service: + from lofar.sas.tmss.services.sip_generation import create_sip_generation_service + self.sip_generation_service = create_sip_generation_service(exchange=self._exchange, broker=self._broker, tmss_client_credentials_id=self.client_credentials.dbcreds_id) + service_threads.append(threading.Thread(target=self.sip_generation_service.start_listening())) + service_threads[-1].start() + # wait for all services to be fully started in their background threads for thread in service_threads: thread.join() @@ -429,6 +439,10 @@ class TMSSTestEnvironment: self.precalculations_service.stop() self.precalculations_service = None + if self.sip_generation_service is not None: + BusListenerJanitor.stop_listening_and_delete_queue(self.sip_generation_service) + self.sip_generation_service = None + self.django_server.stop() self.ldap_server.stop() self.database.destroy() @@ -559,6 +573,7 @@ def main_test_environment(): group.add_option('-w', '--websockets', dest='websockets', action='store_true', help='Enable json updates pushed via websockets') group.add_option('-f', '--feedbackservice', dest='feedbackservice', action='store_true', help='Enable feedbackservice to handle feedback from observations/pipelines which comes in via the (old qpid) otdb messagebus.') group.add_option('-C', '--precalculations_service', dest='precalculations_service', action='store_true', help='Enable the PreCalculations service') + group.add_option('-G', '--sip_generation_service', dest='sip_generation_service', action='store_true', help='Enable the SIP generation service') group.add_option('--all', dest='all', action='store_true', help='Enable/Start all the services, upload schemas and testdata') group.add_option('--simulate', dest='simulate', action='store_true', help='Simulate a run of the first example scheduling_unit (implies --data and --eventmessages)') @@ -597,6 +612,7 @@ def main_test_environment(): enable_viewflow=options.viewflow_app or options.viewflow_service or options.all, start_workflow_service=options.viewflow_service or options.all, start_precalculations_service=options.precalculations_service or options.all, + start_sip_generation_service=options.sip_generation_service or options.all, ldap_dbcreds_id=options.LDAP_ID, db_dbcreds_id=options.DB_ID, client_dbcreds_id=options.REST_CLIENT_ID) as tmss_test_env: # print some nice info for the user to use the test servers... diff --git a/SAS/TMSS/frontend/tmss_webapp/src/components/JSONEditor/JEditor.js b/SAS/TMSS/frontend/tmss_webapp/src/components/JSONEditor/JEditor.js index f23dc25825f2a3c547b00154d1d9011cb2a13f45..f7ab5f16c6e3cf55e63d0a2fffa6f07997559af6 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/components/JSONEditor/JEditor.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/components/JSONEditor/JEditor.js @@ -13,6 +13,8 @@ import $RefParser from 'json-schema-ref-parser'; import "@fortawesome/fontawesome-free/css/all.css"; import flatpickr from 'flatpickr'; import "flatpickr/dist/flatpickr.css"; +import { Button } from 'primereact/button'; + const JSONEditor = require("@json-editor/json-editor").JSONEditor; function Jeditor(props) { @@ -256,6 +258,8 @@ function Jeditor(props) { message: 'Not a valid input. Mimimum: -90:00:00.0000degrees 0r -1.57079632679489661923, Maximum:90:00:00.0000degrees or 1.57079632679489661923' }); } + } else if (schema.validationType === "transitOffset") { + Validator.validateTransitOffset(schema, value, errors, path); } else if (schema.validationType === "distanceOnSky") { // To add eventlistener to the sky distance field based on the validation type set if(_.indexOf(skyDistanceProps, path) === -1) { @@ -375,7 +379,7 @@ function Jeditor(props) { }) } } - }) + }); } // Add Onchange event for Channels Per Subband field and Time Integration field to update Frequency Resolution and Time Resolution for (const channelPath of channelsPerSubbandProps) { @@ -535,8 +539,10 @@ function Jeditor(props) { * Function to get the schema change for specified properties like subbands, duration, column width, etc * @param {Object} properties */ - function getCustomProperties(properties) { + function getCustomProperties(properties, parentProps) { for (const propertyKey in properties) { + parentProps = parentProps?parentProps:[]; + parentProps[propertyKey] = properties[propertyKey]; const propertyValue = properties[propertyKey]; if ((propertyKey.toLowerCase() === 'subbands' && propertyValue.type === 'array') || propertyKey.toLowerCase() === 'list' && propertyValue.type === 'array') { @@ -618,7 +624,7 @@ function Jeditor(props) { propertyValue.properties['frequency_steps']['validationType'] = "pipelineAverage"; propertyValue.properties['frequency_steps']['format'] = "grid"; propertyValue.properties['frequency_steps']['options'] = { "grid_columns": 3 }; - } else if (propertyKey.toLowerCase() === 'duration') { + } else if (propertyKey.toLowerCase() === 'duration') { let newProperty = { "type": "string", "format": "time", @@ -645,6 +651,20 @@ function Jeditor(props) { } } }; + properties[propertyKey] = newProperty; + // durationProps.push(propertyKey); + } else if (propertyKey.toLowerCase() === 'timedelta') { + let newProperty = { + "type": "string", + "title": propertyKey.toLowerCase(), + "description": `${propertyValue.description?propertyValue.description:''} (+/- Hours:Minutes:Seconds)`, + "options": { + "grid_columns": 3, + "inputAttributes": { + "placeholder": "(+/-) HH:MM:SS" + }, + } + }; properties[propertyKey] = newProperty; // durationProps.push(propertyKey); @@ -673,6 +693,21 @@ function Jeditor(props) { newProperty.options.flatpickr["defaultMinute"] = systemTime.minutes(); } properties[propertyKey] = {...propertyValue, ...newProperty}; + } else if (propertyValue['$ref'] && propertyValue['$ref'].toLowerCase().indexOf('#/definitions/timedelta')>=0) { + // set 'required' field value, it required in fields validation + if (parentProps && parentProps.transit_offset && parentProps.transit_offset.required) { + propertyValue["required"] = _.includes(parentProps.transit_offset.required, propertyKey); + } + propertyValue["type"] = "string"; + //propertyValue["format"] = "time"; + propertyValue["title"] = propertyKey.toLowerCase(); + propertyValue["description"] = `${propertyValue.description?propertyValue.description:''} (+/- Hours:Minutes:Seconds)`; + propertyValue["options"] = {"grid_columns": 3, + "inputAttributes": { + "placeholder": "Transit Offset [+/- Hours:Minutes:Seconds]" + }}; + + properties[propertyKey] = {...propertyValue}; } else if (propertyValue instanceof Object) { // by default previously, all field will have format as Grid, but this will fail for calendar, so added property called skipFormat if (propertyKey !== 'properties' && propertyKey !== 'default' && !propertyValue.skipFormat) { @@ -698,7 +733,7 @@ function Jeditor(props) { pointingProps.push(propertyKey); } - getCustomProperties(propertyValue); + getCustomProperties(propertyValue, parentProps); } } } diff --git a/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/OffsetTimeInputmask.js b/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/OffsetTimeInputmask.js new file mode 100644 index 0000000000000000000000000000000000000000..910dce06b3ac7d3a29912bbcd7416e82bcbc3147 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/OffsetTimeInputmask.js @@ -0,0 +1,44 @@ +import React, { Component } from 'react'; +import Cleave from 'cleave.js/react'; + +const BG_COLOR= '#f878788f'; + +export default class OffsetTimeInputMask extends Component { + constructor(props) { + super(props); + this.callback = this.callback.bind(this); + } + + /** + * call back function to set value into grid + * @param {*} e + */ + callback(e) { + let isValid = false; + if (e.target.value.match('/^[\+|\-]([0-1]?[0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]$/')) { + e.target.style.backgroundColor = ''; + isValid = true; + } else { + e.target.style.backgroundColor = BG_COLOR; + } + e.target.style.border = "none"; + this.props.context.componentParent.updateAngle( + this.props.node.rowIndex,this.props.colDef.field,e.target.value,false,isValid + ); + } + + afterGuiAttached(){ + this.input.focus(); + this.input.select(); + } + + render() { + return ( + <Cleave placeholder="[+/- HH:MM:SS]" value={this.props.value} + title="Enter in hms format" + className="inputmask" + htmlRef={(ref) => this.input = ref } + onChange={this.callback} /> + ); + } +} \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/components/Timeline/CalendarTimeline.js b/SAS/TMSS/frontend/tmss_webapp/src/components/Timeline/CalendarTimeline.js index 66cc3b59426ba8511c390522a76d2afdfacbc525..cddb94f28eaf9b37a861eddf8861a68c35617d71 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/components/Timeline/CalendarTimeline.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/components/Timeline/CalendarTimeline.js @@ -317,8 +317,18 @@ export class CalendarTimeline extends Component { <div className="sidebar-header-row">{this.state.viewType===UIConstants.timeline.types.NORMAL? (this.state.dayHeaderVisible?`Day${monthDuration}`:`Week${monthDuration}`) :`Week (${this.state.timelineStartDate.week()}) / Day`}</div> - <div className="sidebar-header-row">{this.state.dayHeaderVisible?`UTC(Hr)`:`UTC(Day)`}</div> - <div className="sidebar-header-row">{this.state.dayHeaderVisible?`LST(Hr)`:`LST(Day)`}</div> + <div className="sidebar-header-row"> + <span style={{fontSize:'10px', fontWeight: 600, backgroundColor: '#c40719', marginRight: '10px'}}> + {this.cursorTime?this.cursorTime.utc.format("DD-MMM-YYYY HH:mm:00"):''} + </span> + {this.state.dayHeaderVisible?`UTC(Hr)`:`UTC(Day)`} + </div> + <div className="sidebar-header-row"> + <span style={{fontSize:'10px', fontWeight: 600, backgroundColor: '#c40719', marginRight: '10px'}}> + {this.cursorTime?this.cursorTime.lst:''} + </span> + {this.state.dayHeaderVisible?`LST(Hr)`:`LST(Day)`} + </div> {/* {this.state.viewType === UIConstants.timeline.types.NORMAL && */} <div className="p-grid legend-row" style={{height:this.props.showSunTimings?'0px':'0px'}}> @@ -693,7 +703,10 @@ export class CalendarTimeline extends Component { /** Custom Render function to pass to the CursorMarker component to display cursor labels on cursor movement */ renderCursor({ styles, date }) { const utc = moment(date).utc(); - this.getLSTof(utc); + // For week view get the row date and get the LST date of the cursor for the row date + let onRowGroup = _.find(this.state.group,['id', this.state.onRow]); + let cursorUTC = onRowGroup?onRowGroup.value.clone().hours(utc.hours()).minutes(utc.minutes()).seconds(utc.seconds()):utc; + this.getLSTof(cursorUTC); const cursorLST = this.state.cursorLST; let cursorTextStyles = {}; cursorTextStyles.backgroundColor = '#c40719' @@ -709,11 +722,13 @@ export class CalendarTimeline extends Component { cursorTextStyles.textAlign = "center"; styles.backgroundColor = '#c40719'; styles.display = "block !important"; + styles.zIndex = '999'; + this.cursorTime = {utc: cursorUTC, lst: cursorLST}; return ( <> - <div style={styles} /> + <div style={styles} /> <div style={cursorTextStyles}> - <div>UTC: { utc.format('DD-MMM-YYYY HH:mm:00')}</div> + <div>UTC: { cursorUTC.format('DD-MMM-YYYY HH:mm:00')}</div> <div>LST: {cursorLST}</div> </div> </> @@ -754,6 +769,7 @@ export class CalendarTimeline extends Component { fontSize: isStationView?"10px":"14px", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", textAlign: "center"}; + let itemStatus = null; if (item.type === "SCHEDULE" || item.type === "TASK" || item.type==="STATION_TASK" ) { itemContentStyle = {lineHeight: `${Math.floor(itemContext.dimensions.height/(isStationView?1:3))}px`, maxHeight: itemContext.dimensions.height, @@ -761,6 +777,7 @@ export class CalendarTimeline extends Component { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: isStationView?"nowrap":"inherit", textAlign: "center"}; + itemStatus = item.status; } let itemDivStyle = { background: backgroundColor, color: item.color, @@ -771,11 +788,13 @@ export class CalendarTimeline extends Component { }; if (item.type === "SCHEDULE" || item.type === "TASK" || item.type==="STATION_TASK" ) { itemDivStyle.border = item.scheduleMethod === 'dynamic'?"1.5px dashed":"1.5px solid" + } else if (this.state.viewType === UIConstants.timeline.types.WEEKVIEW && item.type === "SUNTIME") { + itemStatus = "undefined"; } return ( <div {...getItemProps({ - className: `rct-item su-${item.status}`, + className: `rct-item ${itemStatus?'su-'+itemStatus:''}`, style: itemDivStyle, onMouseDown: () => { if (item.type !== "SUNTIME") { @@ -887,6 +906,9 @@ export class CalendarTimeline extends Component { && this.props.itemMouseOverCallback) { this.setState({mouseEvent: true}); this.props.itemMouseOverCallback(evt, item); + } else if (this.state.viewType === UIConstants.timeline.types.WEEKVIEW && item.type === "SUNTIME") { + // For week view set the group id to identify the row date + this.setState({onRow: item.group}); } } @@ -900,6 +922,7 @@ export class CalendarTimeline extends Component { this.setState({mouseEvent: true}); this.props.itemMouseOutCallback(evt); } + this.cursorTime = null; } /** @@ -1622,81 +1645,83 @@ export class CalendarTimeline extends Component { </div> {/* } */} </div> + <div onMouseOut={e => this.cursorTime = null }> <Timeline - groups={this.state.group} - items={this.state.items} - // Use these below properties to stop zoom and move - // defaultTimeStart={this.props.defaultStartTime?this.props.defaultStartTime:this.state.defaultStartTime} - // defaultTimeStart={this.state.defaultStartTime} - // defaultTimeEnd={this.state.defaultEndTime} - visibleTimeStart={this.state.defaultStartTime.valueOf()} - visibleTimeEnd={this.state.defaultEndTime.valueOf()} - resizeDetector={containerResizeDetector} - stackItems={this.props.stackItems || false} - traditionalZoom={this.state.zoomAllowed} - minZoom={this.state.minZoom} - maxZoom={this.state.maxZoom} - lineHeight={this.props.rowHeight || 50} itemHeightRatio={1} - sidebarWidth={this.props.sidebarWidth?this.props.sidebarWidth:this.state.sidebarWidth} - timeSteps={this.state.timeSteps} - onZoom={this.onZoom} - onBoundsChange={this.onBoundsChange} - onTimeChange={this.onTimeChange} - itemRenderer={this.renderItem} - canMove={this.state.canMove} - canResize={this.state.canResize} - canChangeGroup={this.state.canChangeGroup}> - <TimelineHeaders className="sticky"> - <SidebarHeader>{({ getRootProps }) => {return this.renderSidebarHeader({ getRootProps })}}</SidebarHeader> - {this.state.weekHeaderVisible && - <DateHeader unit="Week" labelFormat="w"></DateHeader> } - { this.state.dayHeaderVisible && - <DateHeader unit="hour" intervalRenderer={this.renderDayHeader}></DateHeader> } - <DateHeader unit={this.state.lstDateHeaderUnit} intervalRenderer={this.renderUTCDateHeader} ></DateHeader> - {!this.state.isLSTDateHeaderLoading && - // This method keeps updating the header labels, so that the LST values will be displayed after fetching from server - <DateHeader unit={this.state.lstDateHeaderUnit} - intervalRenderer={({ getIntervalProps, intervalContext, data })=>{return this.renderLSTDateHeader({ getIntervalProps, intervalContext, data })}}> - </DateHeader> - // This method will render once but will not update the values after fetching from server - // <DateHeader unit={this.state.lstDateHeaderUnit} intervalRenderer={this.renderLSTDateHeader}></DateHeader> - } - {/* Suntime Header in normal view with sunrise, sunset and night time */} - {/* {this.props.showSunTimings && this.state.viewType === UIConstants.timeline.types.NORMAL && this.state.sunTimeMap && - <CustomHeader height={30} unit="minute" - children={({ headerContext: { intervals }, getRootProps, getIntervalProps, showPeriod, data})=> { - return this.renderNormalSuntimeHeader({ headerContext: { intervals }, getRootProps, getIntervalProps, showPeriod, data})}}> - </CustomHeader> - } */} - </TimelineHeaders> + groups={this.state.group} + items={this.state.items} + // Use these below properties to stop zoom and move + // defaultTimeStart={this.props.defaultStartTime?this.props.defaultStartTime:this.state.defaultStartTime} + // defaultTimeStart={this.state.defaultStartTime} + // defaultTimeEnd={this.state.defaultEndTime} + visibleTimeStart={this.state.defaultStartTime.valueOf()} + visibleTimeEnd={this.state.defaultEndTime.valueOf()} + resizeDetector={containerResizeDetector} + stackItems={this.props.stackItems || false} + traditionalZoom={this.state.zoomAllowed} + minZoom={this.state.minZoom} + maxZoom={this.state.maxZoom} + lineHeight={this.props.rowHeight || 50} itemHeightRatio={1} + sidebarWidth={this.props.sidebarWidth?this.props.sidebarWidth:this.state.sidebarWidth} + timeSteps={this.state.timeSteps} + onZoom={this.onZoom} + onBoundsChange={this.onBoundsChange} + onTimeChange={this.onTimeChange} + itemRenderer={this.renderItem} + canMove={this.state.canMove} + canResize={this.state.canResize} + canChangeGroup={this.state.canChangeGroup}> + <TimelineHeaders className="sticky"> + <SidebarHeader>{({ getRootProps }) => {return this.renderSidebarHeader({ getRootProps })}}</SidebarHeader> + {this.state.weekHeaderVisible && + <DateHeader unit="Week" labelFormat="w"></DateHeader> } + { this.state.dayHeaderVisible && + <DateHeader unit="hour" intervalRenderer={this.renderDayHeader}></DateHeader> } + <DateHeader unit={this.state.lstDateHeaderUnit} intervalRenderer={this.renderUTCDateHeader} ></DateHeader> + {!this.state.isLSTDateHeaderLoading && + // This method keeps updating the header labels, so that the LST values will be displayed after fetching from server + <DateHeader unit={this.state.lstDateHeaderUnit} + intervalRenderer={({ getIntervalProps, intervalContext, data })=>{return this.renderLSTDateHeader({ getIntervalProps, intervalContext, data })}}> + </DateHeader> + // This method will render once but will not update the values after fetching from server + // <DateHeader unit={this.state.lstDateHeaderUnit} intervalRenderer={this.renderLSTDateHeader}></DateHeader> + } + {/* Suntime Header in normal view with sunrise, sunset and night time */} + {/* {this.props.showSunTimings && this.state.viewType === UIConstants.timeline.types.NORMAL && this.state.sunTimeMap && + <CustomHeader height={30} unit="minute" + children={({ headerContext: { intervals }, getRootProps, getIntervalProps, showPeriod, data})=> { + return this.renderNormalSuntimeHeader({ headerContext: { intervals }, getRootProps, getIntervalProps, showPeriod, data})}}> + </CustomHeader> + } */} + </TimelineHeaders> - <TimelineMarkers> - {/* Current time line marker */} - <CustomMarker date={this.state.currentUTC}> - {({ styles, date }) => { - const customStyles = { - ...styles, - backgroundColor: 'green', - width: '2px' - } - return <div style={customStyles} /> - }} - </CustomMarker> - {/* Show sunrise and sunset markers for normal timeline view (Not station view and week view */} - {this.props.showSunTimings && this.state.viewType===UIConstants.timeline.types.NORMAL && - <> - {/* Sunrise time line markers */} - { this.renderSunriseMarkers(this.state.sunRiseTimings)} - {/* Sunset time line markers */} - { this.renderSunsetMarkers(this.state.sunSetTimings)} - </> - } - {this.state.showCursor? - <CursorMarker> - {this.renderCursor} - </CursorMarker>:""} - </TimelineMarkers> - </Timeline> + <TimelineMarkers> + {/* Current time line marker */} + <CustomMarker date={this.state.currentUTC}> + {({ styles, date }) => { + const customStyles = { + ...styles, + backgroundColor: 'green', + width: '2px' + } + return <div style={customStyles} /> + }} + </CustomMarker> + {/* Show sunrise and sunset markers for normal timeline view (Not station view and week view */} + {this.props.showSunTimings && this.state.viewType===UIConstants.timeline.types.NORMAL && + <> + {/* Sunrise time line markers */} + { this.renderSunriseMarkers(this.state.sunRiseTimings)} + {/* Sunset time line markers */} + { this.renderSunsetMarkers(this.state.sunSetTimings)} + </> + } + {this.state.showCursor? + <CursorMarker> + {this.renderCursor} + </CursorMarker>:""} + </TimelineMarkers> + </Timeline> + </div> </React.Fragment> ); } diff --git a/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_timeline.scss b/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_timeline.scss index 7d9bd66e394fb008ce026e73cc79d39ec215737e..45dd20d4534087b509e746141bf98d8cabc8e545 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_timeline.scss +++ b/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_timeline.scss @@ -179,6 +179,12 @@ color: orange; } +.su-undefined { + height: 90% !important; + border-top:1px solid rgb(224, 222, 222) !important; + border-radius: 0px; +} + .su-visible { margin-top: 30px; // margin-left: -59px !important; diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/Scheduling.Constraints.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/Scheduling.Constraints.js index fdb33be78e197fbb0e1108e3ae96c4dfe0011fbc..5347d7ee6b0669f9c883e90015eee27ed673b056 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/Scheduling.Constraints.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/Scheduling.Constraints.js @@ -59,9 +59,11 @@ export default (props) => { } if(propertyKey === 'from' ){ propertyValue.propertyOrder=10; + propertyValue.validationType= 'transitOffset'; } if(propertyKey === 'to'){ propertyValue.propertyOrder=11; + propertyValue.validationType= 'transitOffset'; } if(propertyKey === 'sun' || propertyKey === 'moon' || propertyKey === 'jupiter'){ propertyValue.default = ((propertyValue.default * 180) / Math.PI).toFixed(2); diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/SchedulingUnitList.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/SchedulingUnitList.js index 5031a12c5dd31f203f377405c9cd1827eaed878e..c2b8dd2267ed422f99696ed80b638e91562ee9e0 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/SchedulingUnitList.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/SchedulingUnitList.js @@ -98,7 +98,11 @@ class SchedulingUnitList extends Component{ do_cancel: { name: "Cancelled", filter: "switch", - }, + }, + targetName: {name: 'Target - Name'}, + targetAngle1: {name: 'Target - Angle 1'}, + targetAngle2: {name: 'Target - Angle 2'}, + targetRef: {name: 'Target - Reference Frame'}, } if (props.hideProjectColumn) { @@ -137,12 +141,10 @@ class SchedulingUnitList extends Component{ "Stations (CS/RS/IS)", "Tasks content (O/P/I)", "Number of SAPs in the target observation", - "Target 1 - Angle 1", - "Target 1 - Angle 2", - "Target 1 - Reference Frame", - "Target 2 - Angle 1", - "Target 2 - Angle 2", - "Target 2 - Reference Frame", + "Target - Name", + "Target - Angle 1", + "Target - Angle 2", + "Target - Reference Frame", "Created_At", "Updated_At" ], @@ -269,6 +271,7 @@ class SchedulingUnitList extends Component{ this.copySpecAndFailedTasks = this.copySpecAndFailedTasks.bind(this); this.scheduleConstraintsArray=this.scheduleConstraintsArray.bind(this); this.capitalize = this.capitalize.bind(this); + this.formatListToNewLineText = this.formatListToNewLineText.bind(this); } capitalize(s) { @@ -554,7 +557,7 @@ class SchedulingUnitList extends Component{ const columnDefinitionToBeRemove = ['status', 'workflowStatus', 'on_sky_start_time', 'on_sky_stop_time', 'process_start_time', 'process_stop_time']; //For Constraint const constColDefToBeRemove = ['observation_strategy_template_name', 'duration', 'observation_strategy_template_id', 'observation_strategy_template_description', 'on_sky_start_time', 'on_sky_stop_time', 'process_start_time', 'process_stop_time', 'task_content', - 'target_observation_sap', 'do_cancel', 'created_at', 'updated_at', 'priority_rank', 'priority_queue', 'output_pinned', 'draft',]; + 'target_observation_sap', 'do_cancel', 'created_at', 'updated_at', 'priority_rank', 'priority_queue', 'output_pinned', 'draft', 'targetName','targetAngle1','targetAngle2','targetRef']; const suFilters = await ScheduleService.getSchedulingUnitFilterDefinition(type); this.columnMap = []; let tmpDefaulColumns = _.cloneDeep(this.state.defaultcolumns[0]); @@ -688,44 +691,6 @@ class SchedulingUnitList extends Component{ output.push(scheduleunit); } } - output.map(su => { - su.taskDetails = su.type==="Draft"?su.task_drafts:su.task_blueprints; - const targetObserv = su && su.taskDetails ? su.taskDetails.find(task => task.specifications_template.type_value==='observation' && task.specifications_doc.SAPs) : null; - // Constructing targets in single string to make it clear display - if (targetObserv && targetObserv.specifications_doc) { - targetObserv.specifications_doc.SAPs.map((target, index) => { - if (index === 0){ - defaultcolumns[`target${index}angle1`] = {name: `Target ${index + 1} - Angle 1`}; - defaultcolumns[`target${index}angle2`] = {name: `Target ${index + 1} - Angle 2`}; - defaultcolumns[`target${index}referenceframe`] = { - name: `Target ${index + 1} - Reference Frame`, - filter: "select" - }; - - } else { - optionalColumns[`target${index}angle1`] = {name: `Target ${index + 1} - Angle 1`}; - optionalColumns[`target${index}angle2`] = {name: `Target ${index + 1} - Angle 2`}; - optionalColumns[`target${index}referenceframe`] = { - name: `Target ${index + 1} - Reference Frame`, - filter: "select" - }; - } - su[`target${index}angle1`] = UnitConverter.getAngleInput(target.digital_pointing.angle1); - su[`target${index}angle2`] = UnitConverter.getAngleInput(target.digital_pointing.angle2,true); - su[`target${index}referenceframe`] = target.digital_pointing.direction_type; - /*optionalColumns[`target${index}angle1`] = {name: `Target ${index + 1} - Angle 1`}; - optionalColumns[`target${index}angle2`] = {name: `Target ${index + 1} - Angle 2`}; - optionalColumns[`target${index}referenceframe`] = { - name: `Target ${index + 1} - Reference Frame`, - filter: "select" - };*/ - columnclassname[`Target ${index + 1} - Angle 1`] = "filter-input-75"; - columnclassname[`Target ${index + 1} - Angle 2`] = "filter-input-75"; - return target; - }); - } - return su; - }); } else if ( suType.toLowerCase() === 'blueprint') { const suIds = _.map(scheduleunits, 'id'); let workflows = await this.timelineCommonUtils.getWorkflowsAndTasks(suIds); @@ -789,10 +754,6 @@ class SchedulingUnitList extends Component{ this.constraintColumns = []; for (const key in derviedscheduleconstraints) { if (key !== '$schema') { - /*const header = _.includes(this.minDistance, key)?` ${this.capitalize(key).replace('_',' ')} (Degrees)`: ` ${this.capitalize(key).replace('_',' ')} `; - defaultcolumns[`${key}`] = { - name: header - };*/ this.constraintColumns.push(key); scheduleunit[key] = derviedscheduleconstraints[key]; } @@ -812,62 +773,57 @@ class SchedulingUnitList extends Component{ this.selectedRows = []; } + /** + * Prepare new line string in table column + * @param {Array} arrayVal + * @returns + */ + formatListToNewLineText(arrayVal){ + return ( + <> + {arrayVal.length>0 && arrayVal.map((item) => ( + <div>{item}</div> + ))} + </> + ); + } + + /** + * Group the target values into 4 columns + * @param {*} schedulingUnits + */ async addTargetColumns(schedulingUnits) { - let optionalColumns = this.state.optionalcolumns[0]; - let defaultcolumns = this.state.defaultcolumns[0]; - let columnclassname = this.state.columnclassname[0]; let dataLoadstatus = false; - - await schedulingUnits.map(su => { + for (let su of schedulingUnits) { su['priority_queue'] = su.priority_queue_value; su.taskDetails = su.type==="Draft"?su.task_drafts:su.task_blueprints; const targetObserv = su.taskDetails ? su.taskDetails.find(task => task.specifications_template.type_value==='observation' && (task.specifications_doc.SAPs || task.specifications_doc.target)) : null; - // const targetObservationSAPs = su.taskDetails.find(task => task.specifications_template.name==='target observation'); - // if (targetObservationSAPs.specifications_doc && targetObservationSAPs.specifications_doc.SAPs) { - // su['target_observation_sap'] = targetObservationSAPs.specifications_doc.SAPs.length; - // } else { - // su['target_observation_sap'] = 0; - // } - // Addin target pointing fields as separate column - // if (targetObserv && targetObserv.specifications_doc) { su['priority_queue'] = su.priority_queue_value; if (targetObserv) { this.suTypeColumnToBeRemove = []; + let targetNames = []; + let targetAngle1s = []; + let targetAngle2s = []; + let targetRefs = []; su['target_observation_sap'] = targetObserv.specifications_doc.target? targetObserv.specifications_doc.target.SAPs.length: targetObserv.specifications_doc.SAPs.length; let SAPs = targetObserv.specifications_doc.target? targetObserv.specifications_doc.target.SAPs: targetObserv.specifications_doc.SAPs - SAPs.map((target, index) => { - su[`target${index}angle1`] = UnitConverter.getAngleInput(target.digital_pointing.angle1); - su[`target${index}angle2`] = UnitConverter.getAngleInput(target.digital_pointing.angle2,true); - su[`target${index}referenceframe`] = target.digital_pointing.direction_type; - if (index === 0 ){ - defaultcolumns[`target${index}angle1`] = {name: `Target ${index + 1} - Angle 1`}; - defaultcolumns[`target${index}angle2`] = {name: `Target ${index + 1} - Angle 2`}; - defaultcolumns[`target${index}referenceframe`] = {name: `Target ${index + 1} - Reference Frame`}; - } else { - optionalColumns[`target${index}angle1`] = {name: `Target ${index + 1} - Angle 1`}; - optionalColumns[`target${index}angle2`] = {name: `Target ${index + 1} - Angle 2`}; - /*optionalColumns[`target${index}referenceframe`] = { - name: `Target ${index + 1} - Reference Frame`, - filter: "select" - };*/ //TODO: Need to check why this code is not working - optionalColumns[`target${index}referenceframe`] = {name: `Target ${index + 1} - Reference Frame`}; - } - this.suTypeColumnToBeRemove.push(`target${index}angle1`); - this.suTypeColumnToBeRemove.push(`target${index}angle2`); - this.suTypeColumnToBeRemove.push(`target${index}referenceframe`); - columnclassname[`Target ${index + 1} - Angle 1`] = "filter-input-75"; - columnclassname[`Target ${index + 1} - Angle 2`] = "filter-input-75"; - columnclassname[`Target ${index + 1} - Reference Frame`] = "filter-input-75"; - return target; - }); + for (const target of SAPs) { + // targetNames.push(target.digital_pointing.target) ; + targetNames.push(target.name) ; + targetAngle1s.push(UnitConverter.getAngleInput(target.digital_pointing.angle1)); + targetAngle2s.push(UnitConverter.getAngleInput(target.digital_pointing.angle2, true)); + targetRefs.push(target.digital_pointing.direction_type); + }; + su['targetName'] = this.formatListToNewLineText(targetNames); + su['targetAngle1'] = this.formatListToNewLineText(targetAngle1s); + su['targetAngle2'] = this.formatListToNewLineText(targetAngle2s); + su['targetRef'] = this.formatListToNewLineText(targetRefs); } else { su['target_observation_sap'] = 0; } - return su; - }); + } await this.setState({ - scheduleunit: schedulingUnits, isLoading: false, optionalColumns: [optionalColumns], - columnclassname: [columnclassname], loadingStatus: dataLoadstatus + scheduleunit: schedulingUnits, isLoading: false, loadingStatus: dataLoadstatus }); this.getFilterColumns(this.changesutype()); } diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/ViewSchedulingUnit.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/ViewSchedulingUnit.js index f6e9f0440525cad3c34ce81bbe6d658c9825b4ec..185d69bfec18aee658ee009272fb0a8a54eb887c 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/ViewSchedulingUnit.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/ViewSchedulingUnit.js @@ -41,7 +41,28 @@ class ViewSchedulingUnit extends Component { SU_ACTIVE_STATUSES = ['started', 'observing', 'observed', 'processing', 'processed', 'ingesting']; SU_END_STATUSES = ['finished', 'error', 'cancelled']; TASK_END_STATUSES = ['finished', 'error', 'cancelled']; - + SU_BLUEPRINT_EXPAND= 'draft.scheduling_constraints_template,draft,draft.scheduling_set,task_blueprints.specifications_template,task_blueprints,task_blueprints.subtasks,draft.observation_strategy_template' + SU_BLUEPRINT_FIELDS= ['id','url','created_at','status','tags','output_pinned','duration','name','on_sky_start_time','on_sky_stop_time','scheduling_constraints_doc','description', + 'updated_at','draft.url','draft.id','draft.name','draft.scheduling_set.url','draft.scheduling_set.name','draft.scheduling_set.project_id','task_blueprints.status', + 'draft.priority_rank','draft.priority_queue_value','task_blueprints.subtasks.id','task_blueprints.subtasks.primary','task_blueprints.subtasks.specifications_template_id', + 'task_blueprints.task_type','task_blueprints.id','task_blueprints.subtasks_ids','task_blueprints.name','task_blueprints.description', + 'task_blueprints.short_description','task_blueprints.on_sky_start_time','task_blueprints.on_sky_stop_time','task_blueprints.process_start_time', + 'task_blueprints.process_stop_time','task_blueprints.duration','task_blueprints.relative_start_time','task_blueprints.relative_stop_time','task_blueprints.tags', 'task_blueprints.url', + 'task_blueprints.do_cancel','task_blueprints.obsolete_since','task_blueprints.created_at','task_blueprints.updated_at','task_blueprints.specifications_template.id', 'task_blueprints.draft_id', + 'task_blueprints.specifications_template.type_value','draft.scheduling_constraints_template.schema','draft.scheduling_constraints_template.url','task_blueprints.produced_by_ids','task_blueprints.specifications_doc', + 'draft.observation_strategy_template_id','draft.observation_strategy_template.id', ,'draft.observation_strategy_template.template'] + SU_DRAFT_EXPAND= 'scheduling_constraints_template,scheduling_set,task_drafts.specifications_template,task_drafts,observation_strategy_template,scheduling_unit_blueprints' + SU_DRAFT_FIELDS=['id','url','created_at','status','tags','output_pinned','duration','name','on_sky_start_time','on_sky_stop_time','priority_rank','priority_queue_value','description', + 'scheduling_constraints_doc','scheduling_constraints_template.schema','scheduling_constraints_template.url','observation_strategy_template_id','task_drafts.url','scheduling_constraints_template_id', + 'updated_at','scheduling_set.url','scheduling_set.name','scheduling_set.project_id','task_drafts.status','task_drafts.task_type','task_drafts.id','task_drafts.subtasks_ids', + 'task_drafts.name','task_drafts.description','task_drafts.short_description','task_drafts.on_sky_start_time','task_drafts.on_sky_stop_time','task_drafts.process_start_time', + 'task_drafts.process_stop_time','task_drafts.duration','task_drafts.relative_start_time','task_drafts.relative_stop_time','task_drafts.tags','task_drafts.do_cancel', + 'task_drafts.obsolete_since','task_drafts.created_at','task_drafts.updated_at','task_drafts.specifications_template.id','task_drafts.specifications_template.type_value', + 'task_drafts.task_blueprints_ids','task_drafts.specifications_doc','task_drafts.produced_by_ids','scheduling_unit_blueprints_ids', + 'observation_strategy_template.id','observation_strategy_template.template', + 'scheduling_unit_blueprints.id','scheduling_unit_blueprints.name'] + + constructor(props) { super(props); this.setToggleBySorting(); @@ -339,21 +360,22 @@ class ViewSchedulingUnit extends Component { }; getSchedulingUnitDetails(schedule_type, schedule_id) { - ScheduleService.getSchedulingUnitExtended(schedule_type, schedule_id) + let expand = schedule_type.toLowerCase() === 'draft' ? this.SU_DRAFT_EXPAND: this.SU_BLUEPRINT_EXPAND; + let fields = schedule_type.toLowerCase() === 'draft' ? this.SU_DRAFT_FIELDS: this.SU_BLUEPRINT_FIELDS; + ScheduleService.getExpandedSchedulingUnit(schedule_type, schedule_id,expand,fields) .then(async (schedulingUnit) => { if (schedulingUnit) { - ScheduleService.getSchedulingConstraintTemplate(schedulingUnit.scheduling_constraints_template_id) - .then((template) => { - this.setState({ scheduleunitId: schedule_id, - scheduleunit: schedulingUnit, - scheduleunitType: schedule_type, - constraintTemplate: template }) - }); - if (schedulingUnit.draft_id) { - await ScheduleService.getSchedulingUnitDraftById(schedulingUnit.draft_id).then((response) => { - schedulingUnit['observation_strategy_template_id'] = response.observation_strategy_template_id; - }); + schedulingUnit = this.formatConstraintDocForUI(schedulingUnit); + if (schedulingUnit.draft) { + schedulingUnit['observation_strategy_template_id'] = schedulingUnit.draft.observation_strategy_template_id; + schedulingUnit['scheduling_set'] = schedulingUnit.draft.scheduling_set; + schedulingUnit['scheduling_constraints_template'] = schedulingUnit.draft.scheduling_constraints_template } + this.setState({ scheduleunitId: schedule_id, + scheduleunit: schedulingUnit, + scheduleunitType: schedule_type, + constraintTemplate: schedulingUnit.scheduling_constraints_template}) + let tasks = schedulingUnit.task_drafts ? (await this.getFormattedTaskDrafts(schedulingUnit)) :await this.getFormattedTaskBlueprints(schedulingUnit); let ingestGroup; if(this.props.match.params.type === 'draft') { @@ -399,7 +421,8 @@ class ViewSchedulingUnit extends Component { && task.tasktype.toLowerCase() === schedule_type && (task.specifications_doc.station_groups || task.specifications_doc.target?.station_groups) }); const isIngestPresent = _.find(tasks, (task) => { return task.template.type_value === 'ingest'}); - await this.getFilterColumns(this.props.match.params.type.toLowerCase()); + await this.getFilterColumns(this.props.match.params.type.toLowerCase()); + this.setState({ scheduleunitId: schedule_id, scheduleunit: schedulingUnit, @@ -411,7 +434,7 @@ class ViewSchedulingUnit extends Component { dialogVisible: false, ingestGroup }); - this.loadTaskParameters(schedulingUnit.observation_strategy_template_id); + this.loadTaskParameters(schedulingUnit.draft?schedulingUnit.draft.observation_strategy_template: schedulingUnit.observation_strategy_template); this.selectedRows = []; // Add Action menu this.getActionMenu(schedule_type, isIngestPresent); @@ -423,70 +446,80 @@ class ViewSchedulingUnit extends Component { } }); } - + + /** + * Format constraint field value for UI + * @param {*} scheduling_constraints_doc + * @returns + */ + formatConstraintDocForUI(scheduleunit) { + if (scheduleunit.scheduling_constraints_doc && !scheduleunit.scheduling_constraints_doc.sky.transit_offset.fromoffset) { + scheduleunit.scheduling_constraints_doc.sky.transit_offset.from = (scheduleunit.scheduling_constraints_doc.sky.transit_offset.from<0?'-':'')+UnitConverter.getSecsToHHmmss(scheduleunit.scheduling_constraints_doc.sky.transit_offset.from); + scheduleunit.scheduling_constraints_doc.sky.transit_offset.to = (scheduleunit.scheduling_constraints_doc.sky.transit_offset.to<0?'-':'')+UnitConverter.getSecsToHHmmss(scheduleunit.scheduling_constraints_doc.sky.transit_offset.to); + } + return scheduleunit; + } + /** * To get task parameters from observation strategy to create custom json schema and load with existing value of the task as input * parameters to pass to the JSON editor. */ - loadTaskParameters(observationStrategyId) { - ScheduleService.getObservationStrategy(observationStrategyId) - .then(async(observStrategy) => { - if (observStrategy) { - const tasks = observStrategy.template.tasks; - const parameters = observStrategy.template.parameters; - let paramsOutput = {}; - let schema = { type: 'object', additionalProperties: false, - properties: {}, definitions:{} - }; - let bandPassFilter = null; - const $strategyRefs = await $RefParser.resolve(observStrategy.template); - // TODo: This schema reference resolving code has to be moved to common file and needs to rework - for (const param of parameters) { - // TODO: make parameter handling more generic, instead of task specific. - if (!param.refs[0].startsWith("#/tasks/")) { continue; } - let taskPaths = param.refs[0].split("/"); - const taskName = taskPaths[2]; - //taskPaths = taskPaths.slice(4, taskPaths.length); - /** - * For Short_Description, the task path length will be 4, so added below condition to get short_description details - * #/tasks/Combined Observation/short_description - */ - taskPaths = taskPaths.slice((taskPaths.length===4?3:4), taskPaths.length); - const task = tasks[taskName]; - const suTask = this.state.schedulingUnitTasks.find(taskD => taskD.name === taskName); - if (suTask) { task.specifications_doc = suTask.specifications_doc; - task.short_description = suTask.short_description; - //task.specifications_doc = suTask.specifications_doc; - const taskKeys = Object.keys(task); - for (const taskKey of taskKeys) { - if (taskKey !== 'specifications_template') { - task[taskKey] = suTask[taskKey]; - } + async loadTaskParameters(observStrategy) { + if (observStrategy) { + const tasks = observStrategy.template.tasks; + const parameters = observStrategy.template.parameters; + let paramsOutput = {}; + let schema = { type: 'object', additionalProperties: false, + properties: {}, definitions:{} + }; + let bandPassFilter = null; + const $strategyRefs = await $RefParser.resolve(observStrategy.template); + // TODo: This schema reference resolving code has to be moved to common file and needs to rework + for (const param of parameters) { + // TODO: make parameter handling more generic, instead of task specific. + if (!param.refs[0].startsWith("#/tasks/")) { continue; } + let taskPaths = param.refs[0].split("/"); + const taskName = taskPaths[2]; + //taskPaths = taskPaths.slice(4, taskPaths.length); + /** + * For Short_Description, the task path length will be 4, so added below condition to get short_description details + * #/tasks/Combined Observation/short_description + */ + taskPaths = taskPaths.slice((taskPaths.length===4?3:4), taskPaths.length); + const task = tasks[taskName]; + const suTask = this.state.schedulingUnitTasks.find(taskD => taskD.name === taskName); + if (suTask) { task.specifications_doc = suTask.specifications_doc; + task.short_description = suTask.short_description; + //task.specifications_doc = suTask.specifications_doc; + const taskKeys = Object.keys(task); + for (const taskKey of taskKeys) { + if (taskKey !== 'specifications_template') { + task[taskKey] = suTask[taskKey]; } } - if (task) { - const taskTemplate = suTask.template; - // Get the default Bandpass filter and pass to the editor for frequency calculation from subband list - if (taskTemplate.type_value === 'observation' && task.specifications_doc.filter) { - bandPassFilter = task.specifications_doc.filter; - } else if (taskTemplate.type_value === 'observation' && taskTemplate.schema.properties.filter) { - bandPassFilter = taskTemplate.schema.properties.filter.default; - } - let taskTemplateSchema = await UtilService.resolveSchema(_.cloneDeep(taskTemplate.schema)); - schema.definitions = {...schema.definitions, ...taskTemplateSchema.definitions}; - taskPaths.reverse(); - const paramProp = await ParserUtility.getParamProperty($strategyRefs, taskPaths, taskTemplateSchema, this.taskFilters); - schema.properties[param.name] = _.cloneDeep(paramProp); - if (schema.properties[param.name]) { - schema.properties[param.name].title = param.name; - schema.properties[param.name].default = $strategyRefs.get(param.refs[0]); - paramsOutput[param.name] = schema.properties[param.name].default; - } + } + if (task) { + const taskTemplate = suTask.template; + // Get the default Bandpass filter and pass to the editor for frequency calculation from subband list + if (taskTemplate.type_value === 'observation' && task.specifications_doc.filter) { + bandPassFilter = task.specifications_doc.filter; + } else if (taskTemplate.type_value === 'observation' && taskTemplate.schema.properties.filter) { + bandPassFilter = taskTemplate.schema.properties.filter.default; + } + let taskTemplateSchema = await UtilService.resolveSchema(_.cloneDeep(taskTemplate.schema)); + schema.definitions = {...schema.definitions, ...taskTemplateSchema.definitions}; + taskPaths.reverse(); + const paramProp = await ParserUtility.getParamProperty($strategyRefs, taskPaths, taskTemplateSchema, this.taskFilters); + schema.properties[param.name] = _.cloneDeep(paramProp); + if (schema.properties[param.name]) { + schema.properties[param.name].title = param.name; + schema.properties[param.name].default = $strategyRefs.get(param.refs[0]); + paramsOutput[param.name] = schema.properties[param.name].default; } } - this.setState({paramsSchema: schema, paramsOutput: paramsOutput, bandPassFilter: bandPassFilter }); } - }); + this.setState({paramsSchema: schema, paramsOutput: paramsOutput, bandPassFilter: bandPassFilter }); + } } async getFilterColumns(type) { @@ -501,6 +534,7 @@ class ViewSchedulingUnit extends Component { tmpOptionalColumns = _.omit(tmpOptionalColumns,columnDefinitionToRemove) await this.setState({tmpDefaulcolumns: [tmpDefaulColumns], tmpOptionalcolumns:[tmpOptionalColumns], tmpColumnOrders: tmpColumnOrders, columnMap: this.columnMap}) } + /** * Get action menus for page header */ @@ -524,7 +558,7 @@ class ViewSchedulingUnit extends Component { }); this.actions.push({ icon: 'fa-window-close', title: 'Click to Close Scheduling Unit View', type: 'button', actOn: 'click', props:{ callback: this.cancelView }}); if (this.props.match.params.type ==='draft') { - let blueprintExist = this.state.scheduleunit && this.state.scheduleunit.scheduling_unit_blueprints && this.state.scheduleunit.scheduling_unit_blueprints.length>0; + let blueprintExist = this.state.scheduleunit && this.state.scheduleunit.scheduling_unit_blueprints_ids && this.state.scheduleunit.scheduling_unit_blueprints_ids.length>0; if(isIngestPresent) { this.actions.unshift({ icon: 'fa-file-import', @@ -734,7 +768,7 @@ class ViewSchedulingUnit extends Component { dialog.onSubmit = this.createBlueprintTree; dialog.content = null; dialog.width = null; - if (this.state.scheduleunit.scheduling_unit_blueprints.length > 0) { + if (this.state.scheduleunit.scheduling_unit_blueprints_ids.length > 0) { dialog.detail = "Blueprint(s) already exist for this Scheduling Unit. Do you want to create another one?"; } else { dialog.detail = "Do you want to create a Scheduling Unit Blueprint?"; @@ -1597,19 +1631,21 @@ class ViewSchedulingUnit extends Component { * Enable/Disable autodeletion in the scheduling unit */ async setAutoDeletion() { + let suCopy = _.cloneDeep(this.state.scheduleunit); let resSU = this.state.scheduleunit; resSU['output_pinned'] = !this.state.scheduleunit.output_pinned; + resSU['draft'] = this.state.scheduleunit.draft.url; + resSU['scheduling_constraints_template'] = this.state.scheduleunit.scheduling_constraints_template.url; delete resSU['task_blueprints']; delete resSU['task_drafts']; - resSU = await ScheduleService.updateSchedulingUnit(this.props.match.params.type, resSU); - if (resSU) { + let updatedResSU = await ScheduleService.updateSchedulingUnit(this.props.match.params.type, resSU); + if (updatedResSU) { appGrowl.show({ severity: 'success', summary: 'Success', detail: 'Prevent Automatic Deletion updated successfully' }); - let tmpSu = this.state.scheduleunit; - tmpSu['output_pinned'] = resSU.output_pinned; + suCopy['output_pinned'] = updatedResSU.output_pinned; var index = _.indexOf(this.actions, _.find(this.actions, {'icon' :'fa-thumbtack'})); this.actions.splice(index, 1, { icon: 'fa-thumbtack', title: this.state.scheduleunit.output_pinned? 'Allow Automatic Deletion' : 'Prevent Automatic Deletion', type: 'button', actOn: 'click', props: { callback: this.confirmAutoDeletion } }); - this.setState({scheduleunit: tmpSu, actions: this.actions, dialogVisible: false}); + this.setState({scheduleunit: suCopy, actions: this.actions, dialogVisible: false}); } else { appGrowl.show({ severity: 'error', summary: 'Failed', detail: 'Unable to update Automatic Deletion' }); this.setState({dialogVisible: false}); @@ -1818,35 +1854,35 @@ class ViewSchedulingUnit extends Component { <span className="col-lg-4 col-md-4 col-sm-12">{this.state.scheduleunit.observation_strategy_template_id}</span> </div> <div className="p-grid"> - {this.state.scheduleunit.scheduling_set_object.project_id && + {this.state.scheduleunit.scheduling_set && this.state.scheduleunit.scheduling_set.project_id && <> <label className="col-lg-2 col-md-2 col-sm-12">Project</label> <span className="col-lg-4 col-md-4 col-sm-12"> - <Link to={`/project/view/${this.state.scheduleunit.scheduling_set_object.project_id}`}>{this.state.scheduleunit.scheduling_set_object.project_id}</Link> + <Link to={`/project/view/${this.state.scheduleunit.scheduling_set.project_id}`}>{this.state.scheduleunit.scheduling_set.project_id}</Link> </span> </> } <label className="col-lg-2 col-md-2 col-sm-12">Scheduling set</label> - <span className="col-lg-4 col-md-4 col-sm-12">{this.state.scheduleunit.scheduling_set_object.name}</span> + <span className="col-lg-4 col-md-4 col-sm-12">{this.state.scheduleunit.scheduling_set && this.state.scheduleunit.scheduling_set.name}</span> </div> <div className="p-grid"> <label className="col-lg-2 col-md-2 col-sm-12" >Priority Rank</label> - <span className="col-lg-4 col-md-4 col-sm-12">{this.state.scheduleunit.priority_rank}</span> + <span className="col-lg-4 col-md-4 col-sm-12">{this.state.scheduleunit.draft?this.state.scheduleunit.draft.priority_rank: this.state.scheduleunit.priority_rank}</span> <label className="col-lg-2 col-md-2 col-sm-12">Priority Queue</label> - <span className="col-lg-4 col-md-4 col-sm-12">{this.state.scheduleunit.priority_queue_value}</span> + <span className="col-lg-4 col-md-4 col-sm-12">{this.state.scheduleunit.draft? this.state.scheduleunit.draft.priority_queue_value : this.state.scheduleunit.priority_queue_value}</span> </div> <div className="p-grid"> <label className="col-lg-2 col-md-2 col-sm-12">{this.props.match.params.type === 'blueprint' ? 'Draft' : 'Blueprints'}</label> <span className="col-lg-4 col-md-4 col-sm-12"> <ul className="task-list"> - {(this.state.scheduleunit.blueprintList || []).map(blueprint => ( + {(this.state.scheduleunit.scheduling_unit_blueprints || []).map(blueprint => ( <li> <Link to={{ pathname: `/schedulingunit/view/blueprint/${blueprint.id}` }}>{blueprint.name}</Link> </li>))} - {this.state.scheduleunit.draft_object && + {this.state.scheduleunit.draft && <li> - <Link to={{ pathname: `/schedulingunit/view/draft/${this.state.scheduleunit.draft_object.id}` }}> - {this.state.scheduleunit.draft_object.name} + <Link to={{ pathname: `/schedulingunit/view/draft/${this.state.scheduleunit.draft.id}` }}> + {this.state.scheduleunit.draft.name} </Link> </li>} </ul> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/create.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/create.js index a048736ad2884324f4a27adea18e0b282d212aea..b0afedca78429ddc834977a1b6b352ecdea13183 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/create.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/create.js @@ -162,7 +162,7 @@ export class SchedulingUnitCreate extends Component { * It generates the JSON schema for JSON editor and defult vales for the parameters to be captured * @param {number} strategyId */ - async changeStrategy (strategyId) { + async changeStrategy (strategyId) { const observStrategy = _.find(this.observStrategies, {'id': strategyId}); let station_group = []; const tasks = observStrategy.template.tasks; @@ -255,8 +255,7 @@ export class SchedulingUnitCreate extends Component { if (jsonOutput.scheduler === 'online' || jsonOutput.scheduler === 'dynamic') { err = err.filter(e => e.path !== 'root.time.at'); } - // this.constraintParamsOutput = jsonOutput; - // condition goes here.. + this.constraintParamsOutput = jsonOutput; this.constraintValidEditor = err.length === 0; if ( !this.state.isDirty && this.state.constraintParamsOutput && !_.isEqual(this.state.constraintParamsOutput, jsonOutput) ) { this.setState({ constraintParamsOutput: jsonOutput, constraintValidEditor: err.length === 0, validForm: this.validateForm(), isDirty: true}); @@ -388,7 +387,7 @@ export class SchedulingUnitCreate extends Component { } if (!constStrategy.time.before) { delete constStrategy.time.before; - } + } if (constStrategy.time[type] && constStrategy.time[type].length) { if (typeof constStrategy.time[type] === 'string') { constStrategy.time[type] = `${moment(constStrategy.time[type]).format("YYYY-MM-DDTHH:mm:ss.SSSSS", { trim: false })}Z`; @@ -401,6 +400,11 @@ export class SchedulingUnitCreate extends Component { } } } + if (constStrategy.sky.transit_offset) { + constStrategy.sky.transit_offset.from = UnitConversion.getHHmmssToSecs(constStrategy.sky.transit_offset.from); + constStrategy.sky.transit_offset.to = UnitConversion.getHHmmssToSecs(constStrategy.sky.transit_offset.to); + } + //station const station_groups = []; (this.state.selectedStations || []).forEach(key => { @@ -484,9 +488,11 @@ export class SchedulingUnitCreate extends Component { this.setState({showDialog: false}); } - constraintStrategy(e){ + async constraintStrategy(e){ let schedulingUnit = { ...this.state.schedulingUnit }; schedulingUnit.scheduling_constraints_template_id = e.id; + this.constraintTemplates[0].schema.properties.sky.properties.transit_offset.properties.from.default = UnitConversion.getSecsToHHmmssWithSign(this.constraintTemplates[0].schema.properties.sky.properties.transit_offset.properties.from.default); + this.constraintTemplates[0].schema.properties.sky.properties.transit_offset.properties.to.default = UnitConversion.getSecsToHHmmssWithSign(this.constraintTemplates[0].schema.properties.sky.properties.transit_offset.properties.to.default); this.setState({ constraintSchema: this.constraintTemplates[0], schedulingUnit}); } @@ -588,17 +594,15 @@ export class SchedulingUnitCreate extends Component { setSUSet(suSet) { this.setState({newSet: suSet}); } - + render() { if (this.state.redirect) { return <Redirect to={ {pathname: this.state.redirect} }></Redirect> } const schema = this.state.paramsSchema; const {scheduleunit_draft} = this.state.userrole; - let jeditor = null; if (schema) { - jeditor = React.createElement(Jeditor, {title: "Task Parameters", schema: schema, initValue: this.state.paramsOutput, diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/edit.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/edit.js index 9b46ee48d6b47905525e0a1846d84f8d01d71b64..50e40dc823a5df4faf6e6ac4bb39dc1234837ef5 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/edit.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/edit.js @@ -219,10 +219,23 @@ export class EditSchedulingUnit extends Component { } else { this.setState({isLoading: false}); } - this.constraintStrategy(this.constraintTemplates[0], this.state.schedulingUnit.scheduling_constraints_doc) + this.constraintStrategy(this.constraintTemplates[0], this.formatConstraintDocForUI(this.state.schedulingUnit.scheduling_constraints_doc)) }); } + /** + * Format constraint field value for UI + * @param {*} scheduling_constraints_doc + * @returns + */ + formatConstraintDocForUI(scheduling_constraints_doc) { + if (scheduling_constraints_doc) { + scheduling_constraints_doc.sky.transit_offset.from = (scheduling_constraints_doc.sky.transit_offset.from<0?'-':'')+UnitConversion.getSecsToHHmmss(scheduling_constraints_doc.sky.transit_offset.from); + scheduling_constraints_doc.sky.transit_offset.to = (scheduling_constraints_doc.sky.transit_offset.to<0?'-':'')+UnitConversion.getSecsToHHmmss(scheduling_constraints_doc.sky.transit_offset.to); + } + return scheduling_constraints_doc; + } + /** * This is the callback method to be passed to the JSON editor. * JEditor will call this function when there is change in the editor. @@ -394,6 +407,7 @@ export class EditSchedulingUnit extends Component { } } } + /* for (let type in constStrategy.sky.transit_offset) { constStrategy.sky.transit_offset[type] = constStrategy.sky.transit_offset[type] * 60; }*/ @@ -412,6 +426,10 @@ export class EditSchedulingUnit extends Component { }); } const schUnit = { ...this.state.schedulingUnit }; + if (constStrategy.sky.transit_offset) { + constStrategy.sky.transit_offset.from = UnitConversion.getHHmmssToSecs(constStrategy.sky.transit_offset.from); + constStrategy.sky.transit_offset.to = UnitConversion.getHHmmssToSecs(constStrategy.sky.transit_offset.to); + } schUnit.scheduling_constraints_doc = constStrategy; //station const station_groups = []; @@ -471,8 +489,8 @@ export class EditSchedulingUnit extends Component { this.setState({showDialog: false}); } - constraintStrategy(schema, initValue){ - this.setState({ constraintSchema: schema, initValue: initValue}); + async constraintStrategy(schema, initValue){ + this.setState({ constraintSchema: schema, initValue: initValue}); } onUpdateStations = (state, selectedStations, missingStationFieldsErrors, customSelectedStations) => { @@ -685,7 +703,7 @@ export class EditSchedulingUnit extends Component { <div className="p-grid p-justify-start"> <div className="p-col-1"> <Button label="Save" className="p-button-primary" icon="pi pi-check" onClick={this.saveSchedulingUnit} - disabled={!this.state.validEditor || !this.state.validForm} data-testid="save-btn" /> + disabled={!this.state.constraintValidEditor || !this.state.validEditor || !this.state.validForm} data-testid="save-btn" /> </div> <div className="p-col-1"> <Button label="Cancel" className="p-button-danger" icon="pi pi-times" onClick={this.checkIsDirty} /> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/excelview.schedulingset.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/excelview.schedulingset.js index 8161da156d7adcc0967b1be8d8439208ba89dc2a..89080987537460045b65532e57c77a40762f1656 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/excelview.schedulingset.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/excelview.schedulingset.js @@ -9,6 +9,7 @@ import { DataTable } from 'primereact/datatable'; import { Column } from 'primereact/column'; import TimeInputmask from '../../components/Spreadsheet/TimeInputmask' +import OffsetTimeInputmask from '../../components/Spreadsheet/OffsetTimeInputmask' import DegreeInputmask from '../../components/Spreadsheet/DegreeInputmask' import NumericEditor from '../../components/Spreadsheet/numericEditor'; import BetweenEditor from '../../components/Spreadsheet/BetweenEditor'; @@ -102,6 +103,7 @@ export class SchedulingSetCreate extends Component { frameworkComponents: { numericEditor: NumericEditor, timeInputMask: TimeInputmask, + offsetTimeInputmask:OffsetTimeInputmask, degreeInputMask: DegreeInputmask, betweenRenderer: BetweenRenderer, betweenEditor: BetweenEditor, @@ -808,8 +810,8 @@ export class SchedulingSetCreate extends Component { observationProps['min_target_elevation'] = constraint.sky.min_target_elevation; observationProps['min_calibrator_elevation'] = constraint.sky.min_calibrator_elevation; if ( constraint.sky.transit_offset ){ - observationProps['offset_from'] = constraint.sky.transit_offset.from?constraint.sky.transit_offset.from:0; - observationProps['offset_to'] = constraint.sky.transit_offset.to?constraint.sky.transit_offset.to:0; + observationProps['offset_from'] = constraint.sky.transit_offset.from?(constraint.sky.transit_offset.from<0?'-':'')+UnitConverter.getSecsToHHmmss(constraint.sky.transit_offset.from):0; + observationProps['offset_to'] = constraint.sky.transit_offset.to?(constraint.sky.transit_offset.to<0?'-':'')+UnitConverter.getSecsToHHmmss(constraint.sky.transit_offset.to):0; observationProps['offset_from_max'] = this.constraintSchema.schema.properties.sky.properties.transit_offset.properties.from.maximum; observationProps['offset_from_min'] = this.constraintSchema.schema.properties.sky.properties.transit_offset.properties.from.minimum; observationProps['offset_to_max'] = this.constraintSchema.schema.properties.sky.properties.transit_offset.properties.to.maximum; @@ -1124,7 +1126,8 @@ export class SchedulingSetCreate extends Component { {headerName: 'Description', field: 'sudesc', cellStyle: function(params) { if (params.data && params.data.suname && (params.data.suname !== '' && (!params.value || params.value === ''))) { return { backgroundColor: BG_COLOR}; - } else { return { backgroundColor: ''};} + } else { + return { backgroundColor: ''};} },}, {headerName: 'Priority Rank', field: 'priority_rank',cellEditor: 'numericEditor', cellStyle: function(params) { let value = params.data.priority_rank? params.data.priority_rank: params.data.gdef_priority_rank ? params.data.gdef_priority_rank : ''; @@ -1132,8 +1135,10 @@ export class SchedulingSetCreate extends Component { const splitValue = _.split((value+''),"."); if (value < 0 || value > 1 || (splitValue.length > 1 && splitValue[1].length > 4)) { return {backgroundColor: BG_COLOR}; - } else { return {backgroundColor: ''};} - } else { return {backgroundColor: ''};} + } else { + return {backgroundColor: ''};} + } else { + return {backgroundColor: ''};} }}, {headerName: 'Priority Queue', field: 'priority_queue',cellEditor: 'agSelectCellEditor', cellEditorParams: {values: this.state.priorityQueuelist}},] @@ -1342,8 +1347,8 @@ export class SchedulingSetCreate extends Component { this.agSUWithDefaultValue['scheduler'] = this.constraintSchema.schema.properties.scheduler.default; this.agSUWithDefaultValue['min_target_elevation'] = ((this.constraintSchema.schema.properties.sky.properties.min_target_elevation.default * 180) / Math.PI).toFixed(2); this.agSUWithDefaultValue['min_calibrator_elevation'] = ((this.constraintSchema.schema.properties.sky.properties.min_calibrator_elevation.default * 180) / Math.PI).toFixed(2); - this.agSUWithDefaultValue['offset_from'] = 0; - this.agSUWithDefaultValue['offset_to'] = 0; + this.agSUWithDefaultValue['offset_from'] = '00:00:00'; + this.agSUWithDefaultValue['offset_to'] = '00:00:00'; this.agSUWithDefaultValue['offset_from_max'] = this.constraintSchema.schema.properties.sky.properties.transit_offset.properties.from.maximum; this.agSUWithDefaultValue['offset_from_min'] = this.constraintSchema.schema.properties.sky.properties.transit_offset.properties.from.minimum; this.agSUWithDefaultValue['offset_to_max'] = this.constraintSchema.schema.properties.sky.properties.transit_offset.properties.to.maximum; @@ -1398,50 +1403,8 @@ export class SchedulingSetCreate extends Component { } } }, }, - {headerName: 'Offset Window From',field: 'offset_from',cellStyle: function(params) { - if (params.value){ - const maxValue = params.data.offset_from_max? params.data.offset_from_max:params.data.gdef_offset_from_max; - const minValue = params.data.offset_from_min? params.data.offset_from_min:params.data.gdef_offset_from_min; - if (params.value === 'undefined' || params.value === ''){ - return { backgroundColor: ''}; - } - if(params.value === "0"){ - return { backgroundColor: ''}; - } - if (!Number(params.value)){ - return { backgroundColor: BG_COLOR}; - } - else if ( Number(params.value) < minValue || Number(params.value) > maxValue) { - return { backgroundColor: BG_COLOR}; - } else{ - return { backgroundColor: ''}; - } - } else { - return { backgroundColor: ''}; - } - }, }, - {headerName: 'Offset Window To',field: 'offset_to', cellStyle: function(params) { - const maxValue = params.data.offset_to_max? params.data.offset_to_max:params.data.gdef_offset_to_max; - const minValue = params.data.offset_to_min? params.data.offset_to_min:params.data.gdef_offset_to_min; - if (params.value){ - if (params.value === 'undefined' || params.value === ''){ - return { backgroundColor: ''}; - } - if(params.value === "0"){ - return { backgroundColor: ''}; - } - if ( !Number(params.value)){ - return { backgroundColor: BG_COLOR}; - } - else if ( Number(params.value) < minValue || Number(params.value) > maxValue) { - return { backgroundColor: BG_COLOR}; - } else{ - return { backgroundColor: ''}; - } - } else { - return { backgroundColor: ''}; - } - }, }, + {headerName: 'Offset Window From',field: 'offset_from',cellRenderer: 'betweenRenderer',cellEditor: 'offsetTimeInputmask',valueSetter: 'newValueSetter',}, + {headerName: 'Offset Window To',field: 'offset_to', cellRenderer: 'betweenRenderer',cellEditor: 'offsetTimeInputmask',valueSetter: 'newValueSetter', }, ], }); this.colKeyOrder.push('md_sun'); @@ -1681,18 +1644,24 @@ export class SchedulingSetCreate extends Component { errorMsg += column.colDef.headerName+", "; } } else if (column.colId === 'offset_from'){ - if ( typeof rowData[column.colId] === 'undefined' || (rowData[column.colId] && isNaN(rowData[column.colId]))){ + const tmpTime = _.split(rowData[column.colId], ":"); + if ( typeof rowData[column.colId] === 'undefined' || + (tmpTime.length !== 3 || isNaN(tmpTime[1]) || tmpTime[1]>59 || isNaN(tmpTime[2]) || tmpTime[2]>59)){ isValidRow = false; errorMsg += column.colDef.headerName+", "; - } else if ( Number(rowData[column.colId]) < rowData['offset_from_min'] || Number(rowData[column.colId]) > rowData['offset_from_max']) { + // column.colDef.cellStyle = { backgroundColor: BG_COLOR}; + // rowNoColumn.colDef.cellStyle = { backgroundColor: BG_COLOR}; + } else if ( UnitConverter.getHHmmssToSecs(rowData[column.colId]) < rowData['offset_from_min'] || UnitConverter.getHHmmssToSecs(rowData[column.colId]) > rowData['offset_from_max']) { isValidRow = false; errorMsg += column.colDef.headerName+", "; } } else if (column.colId === 'offset_to'){ - if ( typeof rowData[column.colId] === 'undefined' || (rowData[column.colId] && isNaN(rowData[column.colId]))){ + const tmpTime = _.split(rowData[column.colId], ":"); + if ( typeof rowData[column.colId] === 'undefined' || + (tmpTime.length !== 3 || tmpTime[1]>59 || tmpTime[2]>59)){ isValidRow = false; errorMsg += column.colDef.headerName+", "; - } else if ( Number(rowData[column.colId]) < rowData['offset_to_min'] || Number(rowData[column.colId]) > rowData['offset_to_max']) { + } else if ( UnitConverter.getHHmmssToSecs(rowData[column.colId]) < rowData['offset_to_min'] || UnitConverter.getHHmmssToSecs(rowData[column.colId]) > rowData['offset_to_max']) { isValidRow = false; errorMsg += column.colDef.headerName+", "; } @@ -1952,8 +1921,8 @@ export class SchedulingSetCreate extends Component { constraint.sky.min_distance = min_distance_res; let transit_offset_res = {}; - transit_offset_res['from'] = +suRow.offset_from; - transit_offset_res['to'] = +suRow.offset_to; + transit_offset_res['from'] = UnitConverter.getHHmmssToSecs(suRow.offset_from); + transit_offset_res['to'] = UnitConverter.getHHmmssToSecs(suRow.offset_to); if (transit_offset_res){ constraint.sky.transit_offset= transit_offset_res; } diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/summary.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/summary.js index af5de42019e732c35b3f7cbc6e9293aaa177deb4..087340e36268ebbdf5fc96479227a153ab48bb3b 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/summary.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/summary.js @@ -11,7 +11,7 @@ import { Button } from "primereact/button"; import AuthStore from '../../authenticate/auth.store'; import AuthUtil from '../../utils/auth.util'; import { CustomDialog } from '../../layout/components/CustomDialog'; -import { findAllByDisplayValue } from '@testing-library/dom'; +import UnitConverter from '../../utils/unit.converter'; /** * Component to view summary of the scheduling unit with limited task details @@ -27,6 +27,7 @@ export class SchedulingUnitSummary extends Component { editCheck:1, close:false, userrole: AuthStore.getState(), + constraintsTemplate : this.props.constraintsTemplate, }; this.constraintsOrder = ['scheduler', 'time', 'daily', 'sky']; this.closeSUDets = this.closeSUDets.bind(this); @@ -164,15 +165,49 @@ export class SchedulingUnitSummary extends Component { async componentDidMount() { const promises = [ AuthUtil.getUserRolePermission() - ]; + ]; + this.updateConstraintTemplate(); await Promise.all(promises).then(responses => { this.setState({userrole: responses[0],editCheck:1}); }); } + /** + * Add new fields for plus/minu value for offset (from & to) + * @param {*} constraintTemplate + * @returns + */ + async updateConstraintTemplate() { + if (this.props.constraintsTemplate) { + let constraintTemplate = this.props.constraintsTemplate; + constraintTemplate.schema.properties.sky.properties.transit_offset.properties.from.default = + UnitConverter.getSecsToHHmmssWithSign(this.props.constraintsTemplate.schema.properties.sky.properties.transit_offset.properties.from.default); + constraintTemplate.schema.properties.sky.properties.transit_offset.properties.to.default = + UnitConverter.getSecsToHHmmssWithSign(this.props.constraintsTemplate.schema.properties.sky.properties.transit_offset.properties.to.default); + this.setState({constraintTemplate: constraintTemplate}); + } + } + + /** + * Format constraint field value for UI + * @param {*} scheduling_constraints_doc + * @returns + */ + formatConstraintDocForUI(scheduleunit) { + if (scheduleunit.scheduling_constraints_doc) { + scheduleunit.scheduling_constraints_doc.sky.transit_offset.from = UnitConverter.getSecsToHHmmssWithSign(scheduleunit.scheduling_constraints_doc.sky.transit_offset.from); + scheduleunit.scheduling_constraints_doc.sky.transit_offset.to = UnitConverter.getSecsToHHmmssWithSign(scheduleunit.scheduling_constraints_doc.sky.transit_offset.to); + } + if (this.props.schedulingUnit) { + this.props.schedulingUnit.scheduling_constraints_doc.sky.transit_offset.from = UnitConverter.getSecsToHHmmssWithSign(this.props.schedulingUnit.scheduling_constraints_doc.sky.transit_offset.from); + this.props.schedulingUnit.scheduling_constraints_doc.sky.transit_offset.to = UnitConverter.getSecsToHHmmssWithSign(this.props.schedulingUnit.scheduling_constraints_doc.sky.transit_offset.to); + } + return scheduleunit; + } + render() { const permissions = this.state.userrole.userRolePermission.scheduleunit; - const schedulingUnit = this.props.schedulingUnit; + let schedulingUnit = _.cloneDeep(this.props.schedulingUnit); const suTaskList = this.props.suTaskList; suTaskList.map(task => { task.typeValue = task.specifications_template.type_value; @@ -180,6 +215,7 @@ export class SchedulingUnitSummary extends Component { }); const constraintsTemplate = this.props.constraintsTemplate; // After receiving output from the SchedulingConstraint editor order and format it to display + schedulingUnit = this.formatConstraintDocForUI(schedulingUnit); let constraintsDoc = schedulingUnit.scheduling_constraints_doc ? this.getOrderedConstraints(schedulingUnit.scheduling_constraints_doc,this.constraintsOrder) : null; let disableBtn= this.state.showerror!==0 ?true: false; return ( @@ -305,8 +341,8 @@ export class SchedulingUnitSummary extends Component { } > <SchedulingConstraints - constraintTemplate={constraintsTemplate} - initValue={schedulingUnit.scheduling_constraints_doc} + constraintTemplate={this.state.constraintsTemplate} + initValue={this.props.schedulingUnit.scheduling_constraints_doc} callback={this.setConstraintsEditorOutput} /> </Dialog> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/list.tabs.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/list.tabs.js index db0997e9693b03850e3ba47b7bd6fe8606a94bf0..95ffc42ae4ba635efb3cb198bfa85fe5dd99a0a4 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/list.tabs.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/list.tabs.js @@ -30,6 +30,7 @@ class TimelineListTabs extends Component { this.suListFilterCallback = this.suListFilterCallback.bind(this); this.taskListFilterCallback = this.taskListFilterCallback.bind(this); this.reservListFilterCallback = this.reservListFilterCallback.bind(this); + this.unschedulableCallback = this.unschedulableCallback.bind(this); this.getTaskList = this.getTaskList.bind(this); this.getSUFilterOptions = this.getSUFilterOptions.bind(this); this.getTaskFilterOptions = this.getTaskFilterOptions.bind(this); @@ -136,7 +137,16 @@ class TimelineListTabs extends Component { this.filteredReservs = filteredData; this.props.suListFilterCallback(this.filteredSUB, this.filteredTasks, filteredData); } - + + /** + * Callback function to pass the filtered data for all table data to the parent component to display in the timeline even if the + * selected tab is unscedulable. + * @param {Array} filteredData - Array of unschedulable SUB rows + */ + unschedulableCallback(filteredData) { + this.props.suListFilterCallback(this.filteredSUB, this.filteredTasks, this.filteredReservs); + } + /** * Child Component to display the status log column value with icon and show the dialog on clicking it. * @param {Object} task @@ -300,7 +310,7 @@ class TimelineListTabs extends Component { showTopTotal={false} showGlobalFilter={true} showColumnFilter={true} - // filterCallback={this.suListFilterCallback} + filterCallback={this.unschedulableCallback} lsKeySortColumn={"UnschedulableListSortColumn"} toggleBySorting={(sortData) => this.storeSortingColumn(`${this.props.viewName}_UnschedulableListSortColumn`, sortData)} pageUpdated={this.pageUpdated} diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/view.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/view.js index 5ba3e8fc679a215a1dd82935e2ce4faa588bb09a..13c330939f98b6368df2dbd86aced0eb8fd0ea53 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/view.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/view.js @@ -192,8 +192,12 @@ export class TimelineView extends Component { }) } } - } - UnitConversion.degreeToRadians(constStrategy.sky); + } + if (constStrategy.sky.transit_offset) { + constStrategy.sky.transit_offset.from = UnitConversion.getHHmmssToSecs(constStrategy.sky.transit_offset.from); + constStrategy.sky.transit_offset.to = UnitConversion.getHHmmssToSecs(constStrategy.sky.transit_offset.to); + } + UnitConversion.degreeToRadians(constStrategy.sky); // getConstraintsEditorOutputService - service call is done for getting updated blueprint id const bluePrintValue = await ScheduleService.getConstraintsEditorOutputService(id); diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/week.view.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/week.view.js index 81d52efca2e8b353401e38e6a544ad11e2178d01..926b782e3f2c1101d0d8f28631f0a6d6097d828b 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/week.view.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/week.view.js @@ -181,8 +181,12 @@ export class WeekTimelineView extends Component { }) } } - } - UnitConversion.degreeToRadians(constStrategy.sky); + } + if (constStrategy.sky.transit_offset) { + constStrategy.sky.transit_offset.from = UnitConversion.getHHmmssToSecs(constStrategy.sky.transit_offset.from); + constStrategy.sky.transit_offset.to = UnitConversion.getHHmmssToSecs(constStrategy.sky.transit_offset.to); + } + UnitConversion.degreeToRadians(constStrategy.sky); // getConstraintsEditorOutputService - service call is done for getting updated blueprint id const bluePrintValue = await ScheduleService.getConstraintsEditorOutputService(id); @@ -1342,7 +1346,7 @@ export class WeekTimelineView extends Component { itemClickCallback={this.onItemClick} itemMouseOverCallback={this.onItemMouseOver} itemMouseOutCallback={this.onItemMouseOut} - sidebarWidth={150} + sidebarWidth={175} stackItems={true} startTime={moment.utc(this.state.currentUTC).hour(0).minutes(0).seconds(0)} endTime={moment.utc(this.state.currentUTC).hour(23).minutes(59).seconds(59)} diff --git a/SAS/TMSS/frontend/tmss_webapp/src/services/schedule.service.js b/SAS/TMSS/frontend/tmss_webapp/src/services/schedule.service.js index b5384aa4d05b406bb5a5e7237688f617711c3cdf..b6684a162480a97b754ef2aa9cc10c6fe18abcd8 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/services/schedule.service.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/services/schedule.service.js @@ -117,6 +117,21 @@ const ScheduleService = { console.error('[schedule.services.getSchedulingUnitsExtendedWithFilter]',error); } return response; + }, + getExpandedSchedulingUnit: async function(type, id, expand, fields) { + let schedulingUnit = null; + try { + let api = `/api/scheduling_unit_${type}/${id}/?`; + api += (expand === '')? '' : 'expand='+expand+'&'; + api += (!fields || fields === '')? '' : 'fields='+fields+'&'; + const response = await axios.get(api); + schedulingUnit = response.data; + } catch(error) { + console.error('[schedule.services.getSchedulingUnitsExpandWithFilter]',error); + } + return schedulingUnit + + }, getSchedulingUnitExtended: async function (type, id, ignoreRef){ if (type === "constraints") { diff --git a/SAS/TMSS/frontend/tmss_webapp/src/utils/unit.converter.js b/SAS/TMSS/frontend/tmss_webapp/src/utils/unit.converter.js index dc72950df943ac70367d8f5d5a3763f194dbc79f..345e6adbdfc27cbe75bfa9faaa768119b9e47260 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/utils/unit.converter.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/utils/unit.converter.js @@ -61,19 +61,34 @@ const UnitConverter = { let stopTime = moment(stopdate).unix(); return UnitConverter.getSecsToDDHHmmss(stopTime - startTime); }, + getSecsToHHmmssWithSign: function (seconds) { + var prefix = ''; + if (!isNaN(seconds)) { + if (seconds<0) { + prefix = '-'; + } + seconds = prefix+this.getSecsToHHmmss(seconds); + } + return seconds; + }, getSecsToHHmmss: function (seconds) { - if (seconds >= 0) { + if (!isNaN(seconds)) { + seconds = Math.abs(seconds); const hh = Math.floor(seconds / 3600); const mm = Math.floor((seconds - hh * 3600) / 60); const ss = +((seconds - (hh * 3600) - (mm * 60)) / 1); - return (hh < 10 ? `0${hh}` : `${hh}`) + ':' + (mm < 10 ? `0${mm}` : `${mm}`) + ':' + (ss < 10 ? `0${ss}` : `${ss}`); + let retStr = (hh < 10 ? `0${hh}` : `${hh}`) + ':' + (mm < 10 ? `0${mm}` : `${mm}`) + ':' + (ss < 10 ? `0${ss}` : `${ss}`); + return retStr; } return seconds; }, - getHHmmssToSecs: function (seconds) { - if (seconds) { - const strSeconds = _.split(seconds, ":"); - return strSeconds[0] * 3600 + strSeconds[1] * 60 + Number(strSeconds[2]); + getHHmmssToSecs: function (time) { + if (time) { + time = _.trim(time); + let prefix = _.startsWith(time, '-')?-1:1; + time = time.replace("-","").replace("+",""); + const strSeconds = _.split(time, ":"); + return prefix * (strSeconds[0] * 3600 + strSeconds[1] * 60 + Number(strSeconds[2])); } return 0; }, diff --git a/SAS/TMSS/frontend/tmss_webapp/src/utils/validator.js b/SAS/TMSS/frontend/tmss_webapp/src/utils/validator.js index 4453a7ceca38bf1dfa7d24a9ef25df4d499f1e1d..a11cde4f0e02ca0c3e3e16cf046abcb0d5a605d2 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/utils/validator.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/utils/validator.js @@ -1,4 +1,5 @@ import UnitConverter from "./unit.converter"; +import _ from 'lodash'; const Validator = { validateTime(value) { @@ -62,8 +63,7 @@ const Validator = { } else { var timeFormat = /^([0-9]|[0-1][0-9]|[0-2][0-3]):([0-9]|[0-5][0-9]):([0-9]|[0-5][0-9])$/; isValid = timeFormat.test(time); - } - + } } else { isValid = false; } @@ -96,7 +96,34 @@ const Validator = { } else { return false; } - } + }, + validateTransitOffset(schema, jsonOutput, error, path) { + const tmpTime = _.split(jsonOutput, ":"); + if (jsonOutput.length === 0 && schema.required === true) { + error.push({ + message:`Transit Offset - ${schema.title} is required. Time format should be [+/- Hours:Minutes:Seconds]. eg. '-23:59:59', '+20:23:25', '15:45:45'`, + path:path, + property:'validationType', + }); + } + //here the isValidHHmmss() valiadtion function not used because it requires only MM:SS validation and the hours may define more than 23 + else if (tmpTime.length !== 3 || tmpTime[1]>59 || tmpTime[1].trim() === '' || tmpTime[2]>59 || tmpTime[2].trim() === '') { + error.push({ + message:"Invalid time format. Time format should be [+/- Hours:Minutes:Seconds]. eg. '-23:59:59', '+20:23:25', '15:45:45'", + path:path, + property:'validationType', + }); + } else { + let value = UnitConverter.getHHmmssToSecs(jsonOutput); + if (isNaN(value) || (value < schema.minimum || value > schema.maximum)) { + error.push({ + message:'Time must be between '+((schema.minimum<0)?'-':'')+UnitConverter.getSecsToHHmmss(schema.minimum)+' and '+((schema.maximum<0)?'-':'')+UnitConverter.getSecsToHHmmss(schema.maximum), + path:path, + property:'validationType', + }); + } + } + }, }; export default Validator;