diff --git a/CMake/LofarPackageList.cmake b/CMake/LofarPackageList.cmake index a54966e0ffb3587a9b9410d65317abe283462504..f151e9395a89417b77f596cc5105a4f0ef8ef335 100644 --- a/CMake/LofarPackageList.cmake +++ b/CMake/LofarPackageList.cmake @@ -1,7 +1,7 @@ # - Create for each LOFAR package a variable containing the absolute path to # its source directory. # -# Generated by gen_LofarPackageList_cmake.sh at do 21 jan 2021 13:07:07 CET +# Generated by gen_LofarPackageList_cmake.sh at di 9 feb 2021 14:22:29 CET # # ---- DO NOT EDIT ---- # @@ -96,6 +96,7 @@ if(NOT DEFINED LOFAR_PACKAGE_LIST_INCLUDED) set(LTAIngest_SOURCE_DIR ${CMAKE_SOURCE_DIR}/LTA/LTAIngest) set(ltastorageoverview_SOURCE_DIR ${CMAKE_SOURCE_DIR}/LTA/ltastorageoverview) set(sip_SOURCE_DIR ${CMAKE_SOURCE_DIR}/LTA/sip) + set(LTACatalogue_SOURCE_DIR ${CMAKE_SOURCE_DIR}/LTA/LTACatalogue) set(LTAIngestCommon_SOURCE_DIR ${CMAKE_SOURCE_DIR}/LTA/LTAIngest/LTAIngestCommon) set(LTAIngestServer_SOURCE_DIR ${CMAKE_SOURCE_DIR}/LTA/LTAIngest/LTAIngestServer) set(LTAIngestClient_SOURCE_DIR ${CMAKE_SOURCE_DIR}/LTA/LTAIngest/LTAIngestClient) diff --git a/CMake/variants/variants.cbm206 b/CMake/variants/variants.cbm206 index 75b51fb5fec037c6b10f57bd67b226075ea31122..13b95d875c4f57fbd9cf1b52aaa133b9e04fa076 100644 --- a/CMake/variants/variants.cbm206 +++ b/CMake/variants/variants.cbm206 @@ -11,7 +11,7 @@ option(USE_LOG4CPLUS "Use Log4Cplus" ON) option(USE_MPI "Use MPI" ON) set(CASACORE_ROOT_DIR /opt/casacore/3.0.0) -set(DAL_ROOT_DIR /opt/DAL/3.3.1) +set(DAL_ROOT_DIR /opt/DAL/3.3.2) set(BLITZ_ROOT_DIR /opt/blitz/1.0.1) # MPI_ROOT_DIR does not need to be specified, because it can just be found thanks to the usage of mpi-selector diff --git a/CMake/variants/variants.head01 b/CMake/variants/variants.head01 index 891fc9269fc6156b6b67d2063c5e66d7ecd0e23f..8e14b09865dafd8b7f1b3cc0510e54ff84e25c3f 100644 --- a/CMake/variants/variants.head01 +++ b/CMake/variants/variants.head01 @@ -1,2 +1,2 @@ set(CASACORE_ROOT_DIR /opt/casacore-v3.0.0) -set(DAL_ROOT_DIR /opt/DAL-v3.3.1) +set(DAL_ROOT_DIR /opt/DAL-v3.3.2) diff --git a/Docker/lofar-ci/Dockerfile_ci_base b/Docker/lofar-ci/Dockerfile_ci_base index 1afd6427e70ff6bc4c5937f5986241e418751e0c..35016a984fb0dd38b48cef912ed12cfa5d1caec4 100644 --- a/Docker/lofar-ci/Dockerfile_ci_base +++ b/Docker/lofar-ci/Dockerfile_ci_base @@ -8,6 +8,7 @@ FROM centos:centos7.6.1810 RUN yum -y groupinstall 'Development Tools' && \ yum -y install epel-release && \ yum -y install cmake cmake3 gcc git log4cplus-devel python3 python3-devel python3-pip which wget curl atop valgrind && \ + python3 -m pip install -U pip && \ pip3 install kombu==4.6.8 requests coverage python-qpid-proton && \ adduser lofarsys && \ mkdir -p /opt/lofar && chown -R lofarsys:lofarsys /opt diff --git a/LCS/Messaging/python/messaging/config.py b/LCS/Messaging/python/messaging/config.py index c8ea8f0763e0d97779fc78a78caa9abbf0e5e63c..7373d04e9077f0be5f188f7be8c6236909cbd64a 100644 --- a/LCS/Messaging/python/messaging/config.py +++ b/LCS/Messaging/python/messaging/config.py @@ -5,6 +5,8 @@ logger = logging.getLogger(__name__) import kombu # make default kombu/amqp logger less spammy logging.getLogger("amqp").setLevel(logging.INFO) +# we're logging when this file is loaded, so format must be correct +logging.basicConfig(format = '%(asctime)s %(levelname)s %(message)s', level = logging.INFO) from lofar.messaging import adaptNameToEnvironment from lofar.common import isProductionEnvironment, isTestEnvironment @@ -59,6 +61,8 @@ for port in possible_ports: except Exception as e: logger.debug("cannot connect to broker: hostname=%s port=%s userid=%s password=*** error=%s", DEFAULT_BROKER, port, DEFAULT_USER, e) +else: + logger.error("Cannot connect to rabbitmq broker with hostname=%s userid=%s password=***. I tried ports %s.", DEFAULT_BROKER, DEFAULT_USER, possible_ports) # default exchange to use for publishing messages DEFAULT_BUSNAME = adaptNameToEnvironment(os.environ.get('LOFAR_DEFAULT_EXCHANGE', 'lofar')) diff --git a/LCS/PyCommon/CMakeLists.txt b/LCS/PyCommon/CMakeLists.txt index 2ab093021552dc5a10dcd660e127dd67f0be96b4..8e03082eebc95c9b2150c9d8589e84bb450615b2 100644 --- a/LCS/PyCommon/CMakeLists.txt +++ b/LCS/PyCommon/CMakeLists.txt @@ -8,6 +8,7 @@ include(PythonInstall) include(FindPythonModule) find_python_module(jsonschema) find_python_module(psycopg2) +find_python_module(cx_Oracle) set(_py_files __init__.py @@ -22,6 +23,8 @@ set(_py_files math.py methodtrigger.py util.py + database.py + oracle.py postgres.py datetimeutils.py flask_utils.py diff --git a/LCS/PyCommon/database.py b/LCS/PyCommon/database.py new file mode 100644 index 0000000000000000000000000000000000000000..77951c94f88a89e508c0df2f8653516cc67f0ab7 --- /dev/null +++ b/LCS/PyCommon/database.py @@ -0,0 +1,203 @@ +#!/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/>. + +# $Id$ + +''' +common abstract database connection class +''' + +import logging +from datetime import datetime, timedelta +import collections +import time +import re +from lofar.common.util import single_line_with_single_spaces +from lofar.common.dbcredentials import DBCredentials + +logger = logging.getLogger(__name__) + +FETCH_NONE=0 +FETCH_ONE=1 +FETCH_ALL=2 + +class DatabaseError(Exception): + pass + +class DatabaseConnectionError(DatabaseError): + pass + +class DatabaseExecutionError(DatabaseError): + pass + +class AbstractDatabaseConnection: + '''Abstract DatabaseConnection class defining a uniform class API for lofar database connections.''' + def __init__(self, + dbcreds: DBCredentials, + auto_commit_selects: bool=False, + num_connect_retries: int=5, + connect_retry_interval: float=1.0, + query_timeout: float=3600): + self._dbcreds = dbcreds + self._connection = None + self._cursor = None + self.__auto_commit_selects = auto_commit_selects + self.__num_connect_retries = num_connect_retries + self.__connect_retry_interval = connect_retry_interval + self.__query_timeout = query_timeout + + def connect_if_needed(self): + if not self.is_connected: + self.connect() + + def connect(self): + if self.is_connected: + logger.debug("already connected to database: %s", self) + return + + for retry_cntr in range(self.__num_connect_retries+1): + try: + logger.debug("connecting to database: %s", self) + + # let the subclass create the connection and cursor. + # handle connection errors here. + self._connection, self._cursor = self._do_create_connection_and_cursor() + logger.info("connected to database: %s", self) + # we have a proper connection, so return + return + except Exception as error: + error_string = single_line_with_single_spaces(error) + logger.error(error_string) + + if self._is_recoverable_connection_error(error): + # try to reconnect on connection-like-errors + if retry_cntr == self.__num_connect_retries: + raise DatabaseConnectionError("Error while connecting to %s. error=%s" % (self, error_string)) + + logger.info('retrying to connect to %s in %s seconds', self.database, self.__connect_retry_interval) + time.sleep(self.__connect_retry_interval) + else: + # non-connection-error, raise generic DatabaseError + raise DatabaseError(error_string) + + def disconnect(self): + if self._connection is not None or self._cursor is not None: + logger.debug("disconnecting from database: %s", self) + + if self._cursor is not None: + self._cursor.close() + self._cursor = None + + if self._connection is not None: + self._connection.close() + self._connection = None + + logger.info("disconnected from database: %s", self) + + def _is_recoverable_connection_error(self, error: Exception) -> bool: + return False + + def __str__(self) -> str: + '''returns the class name and connection string with hidden password.''' + return "%s %s" % (self.__class__.__name__, self._dbcreds.stringWithHiddenPassword()) + + @property + def database(self) -> str: + '''returns the database name''' + return self._dbcreds.database + + @property + def dbcreds(self) -> DBCredentials: + '''returns the database credentials''' + return self._dbcreds + + @property + def is_connected(self) -> bool: + return self._connection is not None + + def reconnect(self): + logger.info("reconnecting %s", self) + self.disconnect() + self.connect() + + def __enter__(self): + '''connects to the database''' + try: + self.connect() + except: + self.disconnect() + raise + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + '''disconnects from the database''' + self.disconnect() + + @staticmethod + def _queryAsSingleLine(query, qargs=None): + line = ' '.join(single_line_with_single_spaces(query).split()) + if qargs: + line = line % tuple(['\'%s\'' % a if isinstance(a, str) else a for a in qargs]) + return line + + def executeQuery(self, query, qargs=None, fetch=FETCH_NONE): + start = datetime.utcnow() + while True: + try: + return self._do_execute_query(query, qargs, fetch) + except DatabaseConnectionError as e: + logger.warning(e) + if datetime.utcnow() - start < timedelta(seconds=self.__query_timeout): + try: + # reconnect, log retrying..., and do the retry in the next loop iteration + self.reconnect() + logger.info("retrying %s", self._queryAsSingleLine(query, qargs)) + except DatabaseConnectionError as ce: + logger.warning(ce) + else: + raise + except Exception as error: + self._log_error_rollback_and_raise(error, self._queryAsSingleLine(query, qargs)) + + def _do_execute_query(self, query, qargs=None, fetch=FETCH_NONE): + raise NotImplementedError() + + def _log_error_rollback_and_raise(self, e: Exception, query_log_line: str): + error_string = single_line_with_single_spaces(e) + logger.error("Rolling back query=\'%s\' due to error: \'%s\'" % (query_log_line, error_string)) + self.rollback() + + # wrap original error in DatabaseExecutionError and raise + raise DatabaseExecutionError("Could not execute query '%s' error=%s" % (query_log_line, error_string)) + + def _commit_selects_if_needed(self, query): + if self.__auto_commit_selects and re.search('select', query, re.IGNORECASE): + # prevent dangling in idle transaction on server + self.commit() + + def commit(self): + if self.is_connected: + logger.debug('commit') + self._connection.commit() + + def rollback(self): + if self.is_connected: + logger.debug('rollback') + self._connection.rollback() + diff --git a/LCS/PyCommon/json_utils.py b/LCS/PyCommon/json_utils.py index 0ff868fe1f18f5a6066fe7057531139d920b8d19..402ad319a91fac270cc7a0879dae4a0be28c764a 100644 --- a/LCS/PyCommon/json_utils.py +++ b/LCS/PyCommon/json_utils.py @@ -24,7 +24,6 @@ def _extend_with_default(validator_class): """ Extend the properties validation so that it adds missing properties with their default values (where one is defined in the schema). - Note: Make sure that items of type object or array in the schema define empty structures as defaults for this to traverse down and add enclosed properties. see: <https://python-jsonschema.readthedocs.io/en/stable/faq/#why-doesn-t-my-schema-s-default-property-set-the-default-on-my-instance> """ @@ -34,6 +33,15 @@ def _extend_with_default(validator_class): for property, subschema in properties.items(): if "default" in subschema: instance.setdefault(property, subschema["default"]) + elif "type" not in subschema: + # could be anything, probably a $ref. + pass + elif subschema["type"] == "object": + # giving objects the {} default causes that default to be populated by the properties of the object + instance.setdefault(property, {}) + elif subschema["type"] == "array": + # giving arrays the [] default causes that default to be populated by the items of the array + instance.setdefault(property, []) for error in validate_properties( validator, properties, instance, schema, ): diff --git a/LCS/PyCommon/oracle.py b/LCS/PyCommon/oracle.py new file mode 100644 index 0000000000000000000000000000000000000000..de63802072b0cea65a1a1870b3873d3edf2ddeee --- /dev/null +++ b/LCS/PyCommon/oracle.py @@ -0,0 +1,149 @@ +#!/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/>. + +# $Id$ + +''' +Module with nice postgres helper methods and classes. +''' + +import logging +from datetime import datetime, timedelta +import cx_Oracle +import re +from lofar.common.dbcredentials import DBCredentials +from lofar.common.database import AbstractDatabaseConnection, DatabaseError, DatabaseConnectionError, DatabaseExecutionError, FETCH_NONE, FETCH_ONE, FETCH_ALL +from lofar.common.util import single_line_with_single_spaces + +logger = logging.getLogger(__name__) + + +class OracleDBError(DatabaseError): + pass + +class OracleDBConnectionError(OracleDBError, DatabaseConnectionError): + pass + +class OracleDBQueryExecutionError(OracleDBError, DatabaseExecutionError): + pass + +class OracleDatabaseConnection(AbstractDatabaseConnection): + def _do_create_connection_and_cursor(self): + connection = cx_Oracle.connect(self._dbcreds.user, + self._dbcreds.password, + "%s:%s/%s" % (self._dbcreds.host, self._dbcreds.port, self._dbcreds.database)) + + cursor = connection.cursor() + return connection, cursor + + @property + def is_connected(self) -> bool: + return super().is_connected and self._connection.handle!=0 + + def _is_recoverable_connection_error(self, error: cx_Oracle.DatabaseError) -> bool: + '''test if cx_Oracle.DatabaseError is a recoverable connection error''' + if isinstance(error, cx_Oracle.OperationalError) and re.search('connection', str(error), re.IGNORECASE): + return True + + if error.code is not None: + # # see https://docs.oracle.com/cd/B19306_01/server.102/b14219.pdf + # TODO: check which oracle error code indicates a recoverable connection error. + pass + + return False + + def _do_execute_query(self, query, qargs=None, fetch=FETCH_NONE): + '''execute the query and reconnect upon OperationalError''' + query_log_line = self._queryAsSingleLine(query, qargs) + + try: + self.connect_if_needed() + + # log + logger.debug('executing query: %s', query_log_line) + + # execute (and time it) + start = datetime.utcnow() + if qargs: + arg_cntr = 0 + while '%s' in query: + query = query.replace('%s', ':arg_%03d'%arg_cntr, 1) + arg_cntr += 1 + assert arg_cntr == len(qargs) + + self._cursor.execute(query, qargs or tuple()) + + # use rowfactory to turn the results into dicts of (col_name, value) pairs + if self._cursor.description: + columns = [col[0] for col in self._cursor.description] + self._cursor.rowfactory = lambda *col_args: dict(zip(columns, col_args)) + + elapsed = datetime.utcnow() - start + elapsed_ms = 1000.0 * elapsed.total_seconds() + + # log execution result + logger.info('executed query in %.1fms%s yielding %s rows: %s', elapsed_ms, + ' (SLOW!)' if elapsed_ms > 250 else '', # for easy log grep'ing + self._cursor.rowcount, + query_log_line) + + self._commit_selects_if_needed(query) + + # fetch and return results + if fetch == FETCH_ONE: + row = self._cursor.fetchone() + return row if row is not None else None + if fetch == FETCH_ALL: + return [row for row in self._cursor.fetchall() if row is not None] + return [] + + except cx_Oracle.OperationalError as oe: + if self._is_recoverable_connection_error(oe): + raise OracleDBConnectionError("Could not execute query due to connection errors. '%s' error=%s" % + (query_log_line, + single_line_with_single_spaces(oe))) + else: + self._log_error_rollback_and_raise(oe, query_log_line) + + except Exception as e: + self._log_error_rollback_and_raise(e, query_log_line) + + def _log_error_rollback_and_raise(self, e: Exception, query_log_line: str): + error_string = single_line_with_single_spaces(e) + logger.error("Rolling back query=\'%s\' due to error: \'%s\'" % (query_log_line, error_string)) + self.rollback() + if isinstance(e, OracleDBError): + # just re-raise our OracleDBError + raise + else: + # wrap original error in OracleDBQueryExecutionError + raise OracleDBQueryExecutionError("Could not execute query '%s' error=%s" % (query_log_line, error_string)) + + +if __name__ == '__main__': + logging.basicConfig(format = '%(asctime)s %(levelname)s %(message)s', level = logging.INFO) + + dbcreds = DBCredentials().get('LTA') + print(dbcreds.stringWithHiddenPassword()) + + with OracleDatabaseConnection(dbcreds=dbcreds) as db: + from pprint import pprint + pprint(db.executeQuery("SELECT table_name, owner, tablespace_name FROM all_tables", fetch=FETCH_ALL)) + # pprint(db.executeQuery("SELECT * FROM awoper.aweprojects", fetch=FETCH_ALL)) + #pprint(db.executeQuery("SELECT * FROM awoper.aweprojectusers", fetch=FETCH_ALL)) diff --git a/LCS/PyCommon/postgres.py b/LCS/PyCommon/postgres.py index db1092fea0db908ab0cb27664f5a8c34b862ee57..84a50c779d733de0e54498f9337eb858dbf795d5 100644 --- a/LCS/PyCommon/postgres.py +++ b/LCS/PyCommon/postgres.py @@ -37,6 +37,7 @@ import psycopg2.extensions from lofar.common.util import single_line_with_single_spaces from lofar.common.datetimeutils import totalSeconds from lofar.common.dbcredentials import DBCredentials +from lofar.common.database import AbstractDatabaseConnection, DatabaseError, DatabaseConnectionError, DatabaseExecutionError, FETCH_NONE, FETCH_ONE, FETCH_ALL logger = logging.getLogger(__name__) @@ -115,166 +116,47 @@ def makePostgresNotificationQueries(schema, table, action, column_name=None, quo sql_lines = '\n'.join([s.strip() for s in sql.split('\n')]) + '\n' return sql_lines -FETCH_NONE=0 -FETCH_ONE=1 -FETCH_ALL=2 - -class PostgresDBError(Exception): +class PostgresDBError(DatabaseError): pass -class PostgresDBConnectionError(PostgresDBError): +class PostgresDBConnectionError(PostgresDBError, DatabaseConnectionError): pass -class PostgresDBQueryExecutionError(PostgresDBError): +class PostgresDBQueryExecutionError(PostgresDBError, DatabaseExecutionError): pass -class PostgresDatabaseConnection: - def __init__(self, - dbcreds: DBCredentials, - auto_commit_selects: bool=False, - num_connect_retries: int=5, - connect_retry_interval: float=1.0, - query_timeout: float=3600): - self._dbcreds = dbcreds - self._connection = None - self._cursor = None - self.__auto_commit_selects = auto_commit_selects - self.__num_connect_retries = num_connect_retries - self.__connect_retry_interval = connect_retry_interval - self.__query_timeout = query_timeout - - def connect_if_needed(self): - if not self.is_connected: - self.connect() +class PostgresDatabaseConnection(AbstractDatabaseConnection): + '''A DatabaseConnection to a postgres database using the common API from lofar AbstractDatabaseConnection''' + def _do_create_connection_and_cursor(self): + connection = psycopg2.connect(host=self._dbcreds.host, + user=self._dbcreds.user, + password=self._dbcreds.password, + database=self._dbcreds.database, + port=self._dbcreds.port, + connect_timeout=5) - def connect(self): - if self.is_connected: - logger.debug("already connected to database: %s", self) - return - - for retry_cntr in range(self.__num_connect_retries+1): - try: - logger.debug("connecting to database: %s", self) - - self._connection = psycopg2.connect(host=self._dbcreds.host, - user=self._dbcreds.user, - password=self._dbcreds.password, - database=self._dbcreds.database, - port=self._dbcreds.port, - connect_timeout=5) - - if self._connection: - self._cursor = self._connection.cursor(cursor_factory=psycopg2.extras.RealDictCursor) - - logger.info("connected to database: %s", self) - - # see http://initd.org/psycopg/docs/connection.html#connection.notices - # try to set the notices attribute with a non-list collection, - # so we can log more than 50 messages. Is only available since 2.7, so encapsulate in try/except. - try: - self._connection.notices = collections.deque() - except TypeError: - logger.warning("Cannot overwrite self._connection.notices with a deque... only max 50 notifications available per query. (That's ok, no worries.)") - - # we have a proper connection, so return - return - except psycopg2.DatabaseError as dbe: - error_string = single_line_with_single_spaces(dbe) - logger.error(error_string) - - if self._is_recoverable_connection_error(dbe): - # try to reconnect on connection-like-errors - if retry_cntr == self.__num_connect_retries: - raise PostgresDBConnectionError("Error while connecting to %s. error=%s" % (self, error_string)) - - logger.info('retrying to connect to %s in %s seconds', self.database, self.__connect_retry_interval) - time.sleep(self.__connect_retry_interval) - else: - # non-connection-error, raise generic PostgresDBError - raise PostgresDBError(error_string) + cursor = connection.cursor(cursor_factory=psycopg2.extras.RealDictCursor) + return connection, cursor - def disconnect(self): - if self._connection is not None or self._cursor is not None: - logger.debug("disconnecting from database: %s", self) - - if self._cursor is not None: - self._cursor.close() - self._cursor = None - - if self._connection is not None: - self._connection.close() - self._connection = None - - logger.info("disconnected from database: %s", self) + @property + def is_connected(self) -> bool: + return super().is_connected and self._connection.closed==0 def _is_recoverable_connection_error(self, error: psycopg2.DatabaseError) -> bool: '''test if psycopg2.DatabaseError is a recoverable connection error''' if isinstance(error, psycopg2.OperationalError) and re.search('connection', str(error), re.IGNORECASE): return True - if error.pgcode is not None: - # see https://www.postgresql.org/docs/current/errcodes-appendix.html#ERRCODES-TABLE - if error.pgcode.startswith('08') or error.pgcode.startswith('57P') or error.pgcode.startswith('53'): - return True + try: + if error.pgcode is not None: + # see https://www.postgresql.org/docs/current/errcodes-appendix.html#ERRCODES-TABLE + if error.pgcode.startswith('08') or error.pgcode.startswith('57P') or error.pgcode.startswith('53'): + return True + except: + return False return False - def __str__(self) -> str: - '''returns the class name and connection string with hidden password.''' - return "%s %s" % (self.__class__.__name__, self._dbcreds.stringWithHiddenPassword()) - - @property - def database(self) -> str: - '''returns the database name''' - return self._dbcreds.database - - @property - def dbcreds(self) -> DBCredentials: - '''returns the database credentials''' - return self._dbcreds - - @property - def is_connected(self) -> bool: - return self._connection is not None and self._connection.closed==0 - - def reconnect(self): - logger.info("reconnecting %s", self) - self.disconnect() - self.connect() - - def __enter__(self): - '''connects to the database''' - self.connect() - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - '''disconnects from the database''' - self.disconnect() - - @staticmethod - def _queryAsSingleLine(query, qargs=None): - line = ' '.join(single_line_with_single_spaces(query).split()) - if qargs: - line = line % tuple(['\'%s\'' % a if isinstance(a, str) else a for a in qargs]) - return line - - def executeQuery(self, query, qargs=None, fetch=FETCH_NONE): - start = datetime.utcnow() - while True: - try: - return self._do_execute_query(query, qargs, fetch) - except PostgresDBConnectionError as e: - logger.warning(e) - if datetime.utcnow() - start < timedelta(seconds=self.__query_timeout): - try: - # reconnect, log retrying..., and do the retry in the next loop iteration - self.reconnect() - logger.info("retrying %s", self._queryAsSingleLine(query, qargs)) - except PostgresDBConnectionError as ce: - logger.warning(ce) - else: - raise - def _do_execute_query(self, query, qargs=None, fetch=FETCH_NONE): '''execute the query and reconnect upon OperationalError''' query_log_line = self._queryAsSingleLine(query, qargs) @@ -333,11 +215,6 @@ class PostgresDatabaseConnection: # wrap original error in PostgresDBQueryExecutionError raise PostgresDBQueryExecutionError("Could not execute query '%s' error=%s" % (query_log_line, error_string)) - def _commit_selects_if_needed(self, query): - if self.__auto_commit_selects and re.search('select', query, re.IGNORECASE): - # prevent dangling in idle transaction on server - self.commit() - def _log_database_notifications(self): try: if self._connection.notices: @@ -350,16 +227,6 @@ class PostgresDatabaseConnection: except Exception as e: logger.error(str(e)) - def commit(self): - if self.is_connected: - logger.debug('commit') - self._connection.commit() - - def rollback(self): - if self.is_connected: - logger.debug('rollback') - self._connection.rollback() - class PostgresListener(PostgresDatabaseConnection): ''' This class lets you listen to postgres notifications diff --git a/LCS/PyCommon/test/t_json_utils.py b/LCS/PyCommon/test/t_json_utils.py index a315632d8d5cbca4d82b3a9166c6575f4a2b9cea..df044073190350dbd82e7da273a80b1cd6da9950 100755 --- a/LCS/PyCommon/test/t_json_utils.py +++ b/LCS/PyCommon/test/t_json_utils.py @@ -53,7 +53,6 @@ class TestJSONUtils(unittest.TestCase): "default": {}, "properties": { "sub_a": {"type": "object", - "default": {}, "properties": { "prop_a": {"type": "integer", "default": 42}, "prop_b": {"type": "number", "default": 3.14} diff --git a/LCS/PyCommon/test/t_postgres.py b/LCS/PyCommon/test/t_postgres.py index 8d8b77463e8eae107ce7c109fd77b18aa54d96c3..91e298cfcdc66a93e7c069a0552283b6f460f02d 100755 --- a/LCS/PyCommon/test/t_postgres.py +++ b/LCS/PyCommon/test/t_postgres.py @@ -41,8 +41,8 @@ class TestPostgres(MyPostgresTestMixin, unittest.TestCase): incorrect_dbcreds.port += 1 # test if connecting fails - with mock.patch('lofar.common.postgres.logger') as mocked_logger: - with self.assertRaises(PostgresDBConnectionError): + with mock.patch('lofar.common.database.logger') as mocked_logger: + with self.assertRaises(DatabaseConnectionError): NUM_CONNECT_RETRIES = 2 with PostgresDatabaseConnection(dbcreds=incorrect_dbcreds, connect_retry_interval=0.1, num_connect_retries=NUM_CONNECT_RETRIES) as db: pass @@ -92,7 +92,7 @@ class TestPostgres(MyPostgresTestMixin, unittest.TestCase): logger.info("terminated %s test-postgres-database-instance", self.dbcreds.stringWithHiddenPassword()) # prove that the database is down by trying to connect which results in a PostgresDBConnectionError - with self.assertRaises(PostgresDBConnectionError): + with self.assertRaises(DatabaseConnectionError): with PostgresDatabaseConnection(dbcreds=self.dbcreds, num_connect_retries=0): pass diff --git a/LTA/CMakeLists.txt b/LTA/CMakeLists.txt index 09f2e770c4c8c878e453fd4e28eb084e777ee1e6..42fe8b5ec425b14492dfdda7ff17633c3305ecac 100644 --- a/LTA/CMakeLists.txt +++ b/LTA/CMakeLists.txt @@ -6,3 +6,4 @@ lofar_add_package(LTACommon) lofar_add_package(LTAIngest) lofar_add_package(ltastorageoverview) lofar_add_package(sip) +lofar_add_package(LTACatalogue) diff --git a/LTA/LTACatalogue/CMakeLists.txt b/LTA/LTACatalogue/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..6f06bdabd4ef21ff2509710d566fb7d51c588563 --- /dev/null +++ b/LTA/LTACatalogue/CMakeLists.txt @@ -0,0 +1,4 @@ +lofar_package(LTACatalogue 1.0 DEPENDS PyCommon) + +python_install(lta_catalogue_db.py + DESTINATION lofar/lta) diff --git a/LTA/LTACatalogue/lta_catalogue_db.py b/LTA/LTACatalogue/lta_catalogue_db.py new file mode 100644 index 0000000000000000000000000000000000000000..65eb1813bc7175ef24be55f6bd7431599f131206 --- /dev/null +++ b/LTA/LTACatalogue/lta_catalogue_db.py @@ -0,0 +1,109 @@ +#!/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/>. + +# $Id$ + +''' +Module with nice postgres helper methods and classes. +''' + +import logging +from datetime import datetime, timedelta +from lofar.common.dbcredentials import DBCredentials +from lofar.common.oracle import OracleDatabaseConnection, FETCH_NONE, FETCH_ONE, FETCH_ALL +import cx_Oracle + +logger = logging.getLogger(__name__) + + +class LTACatalogueDatabaseConnection(OracleDatabaseConnection): + '''The LTACatalogueDatabaseConnection is a simple API to query a very limited subset of the full API provided by astrowise. + It is intended to be used by some lofar services, and not as a replacement for astrowise. + It is also highly discouraged to use this connection object to tinker with the oracle database yourself, unless you really know what you're doing.''' + + DEFAULT_RELEASE_DATE = datetime(2050,1,1) + + def get_projects(self): + return self.executeQuery("SELECT * FROM awoper.aweprojects", fetch=FETCH_ALL) + + def get_project_release_date(self, project_name:str) -> datetime: + return self.executeQuery("SELECT RELEASEDATE FROM awoper.aweprojects where NAME=%s", qargs=(project_name,), fetch=FETCH_ONE)['RELEASEDATE'] + + def get_resources(self) -> []: + return self.executeQuery("SELECT * FROM AWOPER.AWERESOURCES_VIEW", fetch=FETCH_ALL) + + def set_project_release_date(self, project_name:str, release_date: datetime): + # we update the release date as if we were the IDM system + # thus, we insert new release date into the lofaridm.project table where it will be picked up by crontabbed the AWOPER.SYNCFROMIDM procedure + # only projects with a default release date of 2050,1,1 are updated in this SYNCFROMIDM procedure, so set it to default first. + # after the new release date has been sync'ed and updated to awoper.aweprojects in AWOPER.SYNCFROMIDM, the new release date will be + # applied to all its observations/pipelines in another 'crontab'/scheduled job 'AWOPER.SYNC_RELEASE_DATE_CHANGES', + # so mind you... Changes in the LTA web UI do not take immediate effect + self.executeQuery("UPDATE awoper.aweprojects SET RELEASEDATE=%s WHERE NAME=%s", qargs=(self.DEFAULT_RELEASE_DATE, project_name), fetch=FETCH_NONE) + self.executeQuery("UPDATE lofaridm.project SET RELEASEDATE=%s WHERE NAME=%s", qargs=(release_date, project_name), fetch=FETCH_NONE) + self.commit() + + def create_project(self, project_name:str, description: str, release_date: datetime=None): + # create a new project in the LTA as if we were the IDM system + # thus, we insert new project into the lofaridm.project table where it will be picked up by crontabbed the AWOPER.SYNCFROMIDM procedure + # so mind you... Changes in the LTA web UI do not take immediate effect + # TODO: at this moment we do not set a PI and/or COI user. Add these when TMSS has/uses a user autorization system. + # raises if the project already exists. + if release_date is None: + release_date = self.DEFAULT_RELEASE_DATE + self.executeQuery("INSERT INTO lofaridm.project (name, description, releasedate) VALUES (%s, %s, %s)", qargs=(project_name, description, release_date), fetch=FETCH_NONE) + self.commit() + + def add_project_storage_resource(self, project_name:str, nr_of_bytes: int, uri: str, remove_existing_resources: bool=False): + # add a new primary storage resource in bytes to the given project. a new project in the LTA as if we were the IDM system + # The URI is usually given as srm://<LTA_SITE_HOST:PORT>/lofar/ops/projects/<PROJECT_NAME_IN_LOWERCASE>/ + # if remove_existing_resources==True then all existing resources for this given project are remove (and replaced by this new resource) + # thus, we insert new project into the lofaridm.project table where it will be picked up by crontabbed the AWOPER.SYNCFROMIDM procedure + # so mind you... Changes in the LTA web UI do not take immediate effect + # raises if the project wit <project_name> does not exist. + project_id = self.executeQuery("SELECT id FROM lofaridm.project WHERE NAME=%s", qargs=(project_name,), fetch=FETCH_ONE)['ID'] + + if remove_existing_resources: + self.executeQuery("DELETE FROM lofaridm.resource$ WHERE id in (SELECT idr FROM lofaridm.resource_project WHERE idp=%s)", qargs=(project_id,), fetch=FETCH_NONE) + + resource_name = "lta_storage_for_%s_%s" % (project_name, datetime.utcnow().isoformat()) + self.executeQuery("INSERT INTO lofaridm.resource$ (NAME, TYPE, UNIT, CATEGORY, ALLOCATION, URI) VALUES (%s, %s, %s, %s, %s, %s)", + qargs=(resource_name, "LTA_STORAGE", "B", "Primary", nr_of_bytes, uri), fetch=FETCH_NONE) + new_resource_id = self.executeQuery("SELECT id FROM lofaridm.resource$ WHERE NAME=%s", qargs=(resource_name,), fetch=FETCH_ONE)['ID'] + + self.executeQuery("INSERT INTO lofaridm.resource_project (IDR, IDP) VALUES (%s, %s)", qargs=(new_resource_id, project_id), fetch=FETCH_NONE) + + self.commit() + + +if __name__ == '__main__': + logging.basicConfig(format = '%(asctime)s %(levelname)s %(message)s', level = logging.INFO) + + dbcreds = DBCredentials().get('LTA') + print(dbcreds.stringWithHiddenPassword()) + + with LTACatalogueDatabaseConnection(dbcreds=dbcreds) as db: + from pprint import pprint + + # pprint(db.create_project("Commissioning_TMSS", "A commissioning project for the TMSS project")) + db.add_project_storage_resource("Commissioning_TMSS", 1, "my_uri", remove_existing_resources=True) + + pprint(db.get_projects()) + pprint(db.get_resources()) + diff --git a/MAC/Deployment/data/StaticMetaData/AntennaFields/NenuFAR-AntennaField.conf b/MAC/Deployment/data/StaticMetaData/AntennaFields/NenuFAR-AntennaField.conf new file mode 100644 index 0000000000000000000000000000000000000000..3a453183492acd454810a16630f6a9d0e03986eb --- /dev/null +++ b/MAC/Deployment/data/StaticMetaData/AntennaFields/NenuFAR-AntennaField.conf @@ -0,0 +1,213 @@ +# Blitz-0.10 formatted +# +# AntennaPositions for the NenuFar array at FR606 +# The LBA parameters are changed, they now contain the +# NenuFar Mini-array phase centers. The HBA positions are still +# those of the HBA tiles. +# LBA reference position is still that of center of LBA array. +# +# ITRF2005 target_date = 2015.5 +# Created: 2018-10-05 18:27:07 +# + +LBA +(0,2) [ 4323979.771620000 165608.826246000 4670303.127 ] +(0,95) x (0,1) x (0,2) [ +-45.584000 -22.692004 42.212000 -45.584000 -22.692004 42.212000 +-30.918000 -40.706002 29.380000 -30.918000 -40.706002 29.380000 +-22.196000 -65.411002 22.244000 -22.196000 -65.411002 22.244000 + -9.974000 -96.194000 12.072000 -9.974000 -96.194000 12.072000 + 6.181010 -113.387999 -2.161990 6.181010 -113.387999 -2.161990 +-40.083000 -73.534003 38.857000 -40.083000 -73.534003 38.857000 +-29.629000 -96.119002 30.102000 -29.629000 -96.119002 30.102000 +-13.664990 -149.466001 17.165000 -13.664990 -149.466001 17.165000 +-29.088990 -132.677002 30.837000 -29.088990 -132.677002 30.837000 +-48.954990 -129.851004 49.088000 -48.954990 -129.851004 49.088000 +-66.844000 -103.576006 64.649000 -66.844000 -103.576006 64.649000 +-89.828000 -97.522008 85.641000 -89.828000 -97.522008 85.641000 +-77.330000 -76.297007 73.283000 -77.330000 -76.297007 73.283000 +-106.119010 -57.655009 99.469000 -106.119010 -57.655009 99.469000 +-130.526010 -40.841012 121.532000 -130.526010 -40.841012 121.532000 +-91.037010 -39.923008 84.859000 -91.037010 -39.923008 84.859000 +-106.100010 -10.763009 97.811000 -106.100010 -10.763009 97.811000 +-118.885010 13.311989 108.798000 -118.885010 13.311989 108.798000 +-126.078020 61.452989 113.745000 -126.078020 61.452989 113.745000 +-140.178010 5.793987 128.829990 -140.178010 5.793987 128.829990 +-155.148020 50.351986 140.832990 -155.148020 50.351986 140.832990 +-180.822020 15.397983 165.868990 -180.822020 15.397983 165.868990 +-92.981000 -131.358008 89.941000 -92.981000 -131.358008 89.941000 +-125.706000 -145.817011 120.611000 -125.706000 -145.817011 120.611000 +-137.752000 -166.225012 132.461000 -137.752000 -166.225012 132.461000 +-139.462000 -101.410013 131.800000 -139.462000 -101.410013 131.800000 +-163.731010 -90.877015 153.802990 -163.731010 -90.877015 153.802990 +-187.865010 -106.297017 177.084990 -187.865010 -106.297017 177.084990 +-204.447010 -37.808019 189.759990 -204.447010 -37.808019 189.759990 +-164.239010 -56.924015 153.116990 -164.239010 -56.924015 153.116990 +-156.157010 -17.355014 144.444990 -156.157010 -17.355014 144.444990 +-180.239010 -24.594016 166.922990 -180.239010 -24.594016 166.922990 +-38.111990 -155.416003 40.065000 -38.111990 -155.416003 40.065000 +-31.254990 -180.539002 34.662000 -31.254990 -180.539002 34.662000 +-21.150980 -218.051001 26.602000 -21.150980 -218.051001 26.602000 +-37.468980 -234.536003 42.188000 -37.468980 -234.536003 42.188000 +-48.321980 -294.194004 54.529000 -48.321980 -294.194004 54.529000 +-66.199980 -255.700006 69.692000 -66.199980 -255.700006 69.692000 +-82.270980 -310.241007 86.653000 -82.270980 -310.241007 86.653000 +-40.808970 -337.452003 49.500000 -40.808970 -337.452003 49.500000 +-18.127980 -307.175001 27.380010 -18.127980 -307.175001 27.380010 +-66.499990 -142.758006 65.789000 -66.499990 -142.758006 65.789000 +-88.927990 -164.584008 87.346000 -88.927990 -164.584008 87.346000 +-106.998990 -175.659010 104.864000 -106.998990 -175.659010 104.864000 +-104.524990 -232.400009 104.708000 -104.524990 -232.400009 104.708000 +-110.021990 -205.459010 108.679000 -110.021990 -205.459010 108.679000 +-134.667990 -193.727012 130.864000 -134.667990 -193.727012 130.864000 +-53.015990 -179.521004 54.799000 -53.015990 -179.521004 54.799000 +-57.444990 -204.300005 59.829000 -57.444990 -204.300005 59.829000 +-84.792990 -214.117007 85.791000 -84.792990 -214.117007 85.791000 +-121.681980 -274.698011 121.985000 -121.681980 -274.698011 121.985000 +-105.608990 -257.807009 106.557000 -105.608990 -257.807009 106.557000 +-88.909000 -127.255908 86.106600 -88.909000 -127.255908 86.106600 +-88.909000 -127.255908 86.106600 -88.909000 -127.255908 86.106600 +-88.909000 -127.255908 86.106600 -88.909000 -127.255908 86.106600 +-88.909000 -127.255908 86.106600 -88.909000 -127.255908 86.106600 +-88.909000 -127.255908 86.106600 -88.909000 -127.255908 86.106600 +-88.909000 -127.255908 86.106600 -88.909000 -127.255908 86.106600 +-88.909000 -127.255908 86.106600 -88.909000 -127.255908 86.106600 +-88.909000 -127.255908 86.106600 -88.909000 -127.255908 86.106600 +-88.909000 -127.255908 86.106600 -88.909000 -127.255908 86.106600 +-88.909000 -127.255908 86.106600 -88.909000 -127.255908 86.106600 +-88.909000 -127.255908 86.106600 -88.909000 -127.255908 86.106600 +-88.909000 -127.255908 86.106600 -88.909000 -127.255908 86.106600 +-88.909000 -127.255908 86.106600 -88.909000 -127.255908 86.106600 +-88.909000 -127.255908 86.106600 -88.909000 -127.255908 86.106600 +-88.909000 -127.255908 86.106600 -88.909000 -127.255908 86.106600 +-88.909000 -127.255908 86.106600 -88.909000 -127.255908 86.106600 +-88.909000 -127.255908 86.106600 -88.909000 -127.255908 86.106600 +-88.909000 -127.255908 86.106600 -88.909000 -127.255908 86.106600 +-88.909000 -127.255908 86.106600 -88.909000 -127.255908 86.106600 +-88.909000 -127.255908 86.106600 -88.909000 -127.255908 86.106600 +-88.909000 -127.255908 86.106600 -88.909000 -127.255908 86.106600 +-88.909000 -127.255908 86.106600 -88.909000 -127.255908 86.106600 +-88.909000 -127.255908 86.106600 -88.909000 -127.255908 86.106600 +-88.909000 -127.255908 86.106600 -88.909000 -127.255908 86.106600 +-88.909000 -127.255908 86.106600 -88.909000 -127.255908 86.106600 +-88.909000 -127.255908 86.106600 -88.909000 -127.255908 86.106600 +-88.909000 -127.255908 86.106600 -88.909000 -127.255908 86.106600 +-88.909000 -127.255908 86.106600 -88.909000 -127.255908 86.106600 +-88.909000 -127.255908 86.106600 -88.909000 -127.255908 86.106600 +-88.909000 -127.255908 86.106600 -88.909000 -127.255908 86.106600 +-88.909000 -127.255908 86.106600 -88.909000 -127.255908 86.106600 +-88.909000 -127.255908 86.106600 -88.909000 -127.255908 86.106600 +-88.909000 -127.255908 86.106600 -88.909000 -127.255908 86.106600 +-88.909000 -127.255908 86.106600 -88.909000 -127.255908 86.106600 +-88.909000 -127.255908 86.106600 -88.909000 -127.255908 86.106600 +-88.909000 -127.255908 86.106600 -88.909000 -127.255908 86.106600 +-88.909000 -127.255908 86.106600 -88.909000 -127.255908 86.106600 +-88.909000 -127.255908 86.106600 -88.909000 -127.255908 86.106600 +-88.909000 -127.255908 86.106600 -88.909000 -127.255908 86.106600 +-88.909000 -127.255908 86.106600 -88.909000 -127.255908 86.106600 +-88.909000 -127.255908 86.106600 -88.909000 -127.255908 86.106600 +-88.909000 -127.255908 86.106600 -88.909000 -127.255908 86.106600 +-88.909000 -127.255908 86.106600 -88.909000 -127.255908 86.106600 +-88.909000 -127.255908 86.106600 -88.909000 -127.255908 86.106600 +] + +HBA +(0,2) [ 4034101.470070000 487012.791760000 4900230.512 ] +(0,95) x (0,1) x (0,2) [ + 20.808000 -15.324998 -15.498990 20.808000 -15.324998 -15.498990 + 21.305000 -10.285998 -16.397990 21.305000 -10.285998 -16.397990 + 21.802000 -5.241998 -17.293990 21.802000 -5.241998 -17.293990 + 22.288000 -0.198998 -18.196990 22.288000 -0.198998 -18.196990 + 22.776000 4.841002 -19.095990 22.776000 4.841002 -19.095990 + 16.351000 -19.412999 -11.454990 16.351000 -19.412999 -11.454990 + 16.852000 -14.368999 -12.354990 16.852000 -14.368999 -12.354990 + 17.343000 -9.320999 -13.253990 17.343000 -9.320999 -13.253990 + 17.842000 -4.273999 -14.158990 17.842000 -4.273999 -14.158990 + 18.326000 0.760001 -15.062990 18.326000 0.760001 -15.062990 + 18.827000 5.786001 -15.954990 18.827000 5.786001 -15.954990 + 19.317000 10.839001 -16.857990 19.317000 10.839001 -16.857990 + 11.894000 -23.508999 -7.425000 11.894000 -23.508999 -7.425000 + 12.394000 -18.456999 -8.332000 12.394000 -18.456999 -8.332000 + 12.898000 -13.395999 -9.235000 12.898000 -13.395999 -9.235000 + 13.392000 -8.355999 -10.119000 13.392000 -8.355999 -10.119000 + 13.875000 -3.311999 -11.017000 13.875000 -3.311999 -11.017000 + 14.359000 1.721001 -11.921000 14.359000 1.721001 -11.921000 + 14.855000 6.744001 -12.818000 14.855000 6.744001 -12.818000 + 15.351000 11.786001 -13.719000 15.351000 11.786001 -13.719000 + 15.834000 16.807001 -14.645000 15.834000 16.807001 -14.645000 + 7.934000 -22.554000 -4.271000 7.934000 -22.554000 -4.271000 + 8.437000 -17.503000 -5.176000 8.437000 -17.503000 -5.176000 + 8.934000 -12.440999 -6.092000 8.934000 -12.440999 -6.092000 + 9.440000 -7.394999 -6.980000 9.440000 -7.394999 -6.980000 + 9.928000 -2.358999 -7.882000 9.928000 -2.358999 -7.882000 + 10.417000 2.678001 -8.790000 10.417000 2.678001 -8.790000 + 10.902000 7.690001 -9.671000 10.902000 7.690001 -9.671000 + 11.384000 12.720001 -10.561000 11.384000 12.720001 -10.561000 + 11.888000 17.759001 -11.479000 11.888000 17.759001 -11.479000 + 3.984000 -21.600000 -1.127000 3.984000 -21.600000 -1.127000 + 4.470000 -16.550000 -2.015000 4.470000 -16.550000 -2.015000 + 4.963000 -11.472000 -2.928000 4.963000 -11.472000 -2.928000 + 5.466000 -6.434000 -3.830000 5.466000 -6.434000 -3.830000 + 5.965000 -1.413000 -4.735000 5.965000 -1.413000 -4.735000 + 6.449000 3.622000 -5.627000 6.449000 3.622000 -5.627000 + 6.933000 8.641000 -6.516000 6.933000 8.641000 -6.516000 + 7.425000 13.669000 -7.410000 7.425000 13.669000 -7.410000 + 7.925000 18.716000 -8.322000 7.925000 18.716000 -8.322000 + 0.024000 -20.662000 2.036000 0.024000 -20.662000 2.036000 + 0.507000 -15.606000 1.134000 0.507000 -15.606000 1.134000 + 1.002000 -10.535000 0.229000 1.002000 -10.535000 0.229000 + 1.491000 -5.481000 -0.670000 1.491000 -5.481000 -0.670000 + 1.978000 -0.461000 -1.574000 1.978000 -0.461000 -1.574000 + 2.471000 4.571000 -2.468000 2.471000 4.571000 -2.468000 + 2.950000 9.595000 -3.354000 2.950000 9.595000 -3.354000 + 3.449000 14.629000 -4.253000 3.449000 14.629000 -4.253000 + 3.950000 19.678000 -5.195000 3.950000 19.678000 -5.195000 + -3.954000 -19.714001 5.204000 -3.954000 -19.714001 5.204000 + -3.468000 -14.631001 4.296000 -3.468000 -14.631001 4.296000 + -2.969000 -9.565001 3.368000 -2.969000 -9.565001 3.368000 + -2.467000 -4.525001 2.461000 -2.467000 -4.525001 2.461000 + -1.964000 0.491999 1.564000 -1.964000 0.491999 1.564000 + -1.471000 5.519000 0.663000 -1.471000 5.519000 0.663000 + -0.986000 10.537000 -0.217000 -0.986000 10.537000 -0.217000 + -0.515000 15.563000 -1.104000 -0.515000 15.563000 -1.104000 + -0.049000 20.637000 -2.040000 -0.049000 20.637000 -2.040000 + -7.912000 -18.767001 8.358000 -7.912000 -18.767001 8.358000 + -7.428000 -13.702001 7.446000 -7.428000 -13.702001 7.446000 + -6.937000 -8.614001 6.532000 -6.937000 -8.614001 6.532000 + -6.432000 -3.568001 5.627000 -6.432000 -3.568001 5.627000 + -5.944000 1.450999 4.725000 -5.944000 1.450999 4.725000 + -5.457000 6.481999 3.828000 -5.457000 6.481999 3.828000 + -4.967000 11.488999 2.937000 -4.967000 11.488999 2.937000 + -4.470000 16.523999 2.032000 -4.470000 16.523999 2.032000 + -4.001000 21.586999 1.087000 -4.001000 21.586999 1.087000 +-11.876000 -17.809001 11.486000 -11.876000 -17.809001 11.486000 +-11.393000 -12.773001 10.589000 -11.393000 -12.773001 10.589000 +-10.907000 -7.687001 9.687000 -10.907000 -7.687001 9.687000 +-10.402000 -2.627001 8.776000 -10.402000 -2.627001 8.776000 + -9.901000 2.407999 7.867000 -9.901000 2.407999 7.867000 + -9.417000 7.426999 6.978000 -9.417000 7.426999 6.978000 + -8.935000 12.445999 6.080000 -8.935000 12.445999 6.080000 + -8.438000 17.501999 5.160000 -8.438000 17.501999 5.160000 + -7.962000 22.533999 4.231000 -7.962000 22.533999 4.231000 +-15.838000 -16.854002 14.637000 -15.838000 -16.854002 14.637000 +-15.353000 -11.833002 13.739000 -15.353000 -11.833002 13.739000 +-14.868000 -6.736002 12.820000 -14.868000 -6.736002 12.820000 +-14.369000 -1.665002 11.915000 -14.369000 -1.665002 11.915000 +-13.872000 3.345998 11.016000 -13.872000 3.345998 11.016000 +-13.397000 8.345998 10.136000 -13.397000 8.345998 10.136000 +-12.905000 13.404998 9.225000 -12.905000 13.404998 9.225000 +-12.395000 18.469998 8.290000 -12.395000 18.469998 8.290000 +-11.899000 23.503999 7.375000 -11.899000 23.503999 7.375000 +-19.300000 -10.876002 16.866000 -19.300000 -10.876002 16.866000 +-18.820000 -5.811002 15.956000 -18.820000 -5.811002 15.956000 +-18.337000 -0.727002 15.059000 -18.337000 -0.727002 15.059000 +-17.845000 4.298998 14.166000 -17.845000 4.298998 14.166000 +-17.352000 9.289998 13.271000 -17.352000 9.289998 13.271000 +-16.843000 14.360998 12.345000 -16.843000 14.360998 12.345000 +-16.365000 19.422998 11.405000 -16.365000 19.422998 11.405000 +-22.772000 -4.894002 19.096000 -22.772000 -4.894002 19.096000 +-22.292000 0.195998 18.197000 -22.292000 0.195998 18.197000 +-21.792000 5.231998 17.306000 -21.792000 5.231998 17.306000 +-21.304000 10.241998 16.409000 -21.304000 10.241998 16.409000 +-20.819000 15.293998 15.500000 -20.819000 15.293998 15.500000 +] diff --git a/RTCP/Cobalt/CoInterface/src/Allocator.cc b/RTCP/Cobalt/CoInterface/src/Allocator.cc index a6b80b9a1d7f8b28f812a2103fa353039df5c72e..2b15c4eccc503f9aabd6da17df32c9950410656f 100644 --- a/RTCP/Cobalt/CoInterface/src/Allocator.cc +++ b/RTCP/Cobalt/CoInterface/src/Allocator.cc @@ -120,6 +120,10 @@ namespace LOFAR { ScopedLock sl(mutex); + // if we allocate 0 bytes, we could end up returning the same pointer for a subsequent allocation. + // since allocations with 0 bytes shouldn't be dereferenced anyway, we return NULL. + if (size == 0) return 0; + // look for a free range large enough for (SparseSet<void *>::const_iterator it = freeList.getRanges().begin(); it != freeList.getRanges().end(); it++) { void *begin = align(it->begin, alignment); diff --git a/RTCP/Cobalt/GPUProc/src/Station/StationInput.cc b/RTCP/Cobalt/GPUProc/src/Station/StationInput.cc index 7a96bc727e347f7a8517865a90acd95780fd4df5..f412199b2a69e33b41ff8b2d5b9101b64fd848ec 100644 --- a/RTCP/Cobalt/GPUProc/src/Station/StationInput.cc +++ b/RTCP/Cobalt/GPUProc/src/Station/StationInput.cc @@ -454,6 +454,7 @@ namespace LOFAR { //copyRSPTimer.stop(); outputQueue.append(rspData); + rspData.reset(); ASSERT(!rspData); } } diff --git a/SAS/TMSS/backend/services/tmss_postgres_listener/bin/tmss_postgres_listener_service b/SAS/TMSS/backend/services/tmss_postgres_listener/bin/tmss_postgres_listener_service old mode 100644 new mode 100755 diff --git a/SAS/TMSS/backend/services/websocket/CMakeLists.txt b/SAS/TMSS/backend/services/websocket/CMakeLists.txt index 7d5ea3a2e9bfb03f75528c83db74cdcb92025ce2..ba899270ef576cc4bff54cdfe1c3ffd4dc69b525 100644 --- a/SAS/TMSS/backend/services/websocket/CMakeLists.txt +++ b/SAS/TMSS/backend/services/websocket/CMakeLists.txt @@ -1,4 +1,4 @@ -lofar_package(TMSSWebSocketService 0.1 DEPENDS TMSSClient PyCommon pyparameterset PyMessaging) +lofar_package(TMSSWebSocketService 0.1 DEPENDS TMSSClient PyCommon pyparameterset PyMessaging) # also depends on TMSSBackend, but that dependency is added implicitely because this is a child package lofar_find_package(PythonInterp 3.6 REQUIRED) diff --git a/SAS/TMSS/backend/services/websocket/lib/websocket_service.py b/SAS/TMSS/backend/services/websocket/lib/websocket_service.py index e87029d4d684fc20f4f9f7d1e6f19c0a94f8ce59..64aa14b82dca4a2c5592e06f8191d2edaa08b6f2 100644 --- a/SAS/TMSS/backend/services/websocket/lib/websocket_service.py +++ b/SAS/TMSS/backend/services/websocket/lib/websocket_service.py @@ -29,13 +29,13 @@ logger = logging.getLogger(__name__) from lofar.common import dbcredentials from lofar.sas.tmss.client.tmssbuslistener import * -from lofar.sas.tmss.client.tmss_http_rest_client import TMSSsession from lofar.common.util import find_free_port from enum import Enum from json import dumps as JSONdumps from SimpleWebSocketServer import SimpleWebSocketServer, WebSocket from threading import Thread, Event +from django.apps import apps DEFAULT_WEBSOCKET_PORT = 5678 @@ -60,14 +60,12 @@ class TMSSEventMessageHandlerForWebsocket(TMSSEventMessageHandler): TASK_BLUEPRINT = 'task_blueprint' TASK_DRAFT = 'task_draft' - def __init__(self, websocket_port: int=DEFAULT_WEBSOCKET_PORT, rest_client_creds_id: str="TMSSClient"): + def __init__(self, websocket_port: int=DEFAULT_WEBSOCKET_PORT): super().__init__(log_event_messages=True) self.websocket_port = websocket_port - self._tmss_client = TMSSsession.create_from_dbcreds_for_ldap(rest_client_creds_id) self._run_ws = True def start_handling(self): - self._tmss_client.open() # Open tmss_client session socket_started_event = Event() # Create and run a simple ws server @@ -87,7 +85,6 @@ class TMSSEventMessageHandlerForWebsocket(TMSSEventMessageHandler): def stop_handling(self): super().stop_handling() - self._tmss_client.close() # Close tmss_client session self._run_ws = False # Stop the ws server self.t.join() @@ -98,10 +95,23 @@ class TMSSEventMessageHandlerForWebsocket(TMSSEventMessageHandler): def _post_update_on_websocket(self, id, object_type, action): # Prepare the json_blob_template - json_blob = {'id': id, 'object_type': object_type.value, 'action': action.value} + json_blob = {'object_details': {'id': id}, 'object_type': object_type.value, 'action': action.value} if action == self.ObjActions.CREATE or action == self.ObjActions.UPDATE: - # Fetch the object from DB using Django model API and add it to json_blob - json_blob['object'] = self._tmss_client.get_path_as_json_object('/%s/%s' % (object_type.value, id)) + try: + model_class = apps.get_model("tmssapp", object_type.value.replace('_','')) + model_instance = model_class.objects.get(id=id) + if hasattr(model_instance, 'start_time') and model_instance.start_time is not None: + json_blob['object_details']['start_time'] = model_instance.start_time.isoformat() + if hasattr(model_instance, 'stop_time') and model_instance.stop_time is not None: + json_blob['object_details']['stop_time'] = model_instance.stop_time.isoformat() + if hasattr(model_instance, 'duration') and model_instance.duration is not None: + json_blob['object_details']['duration'] = model_instance.duration.total_seconds() + if hasattr(model_instance, 'status'): + json_blob['object_details']['status'] = model_instance.status + if hasattr(model_instance, 'state'): + json_blob['object_details']['state'] = model_instance.state.value + except Exception as e: + logger.error("Cannot get object details for %s: %s", json_blob, e) # Send the json_blob as a broadcast message to all connected ws clients self._broadcast_notify_websocket(json_blob) @@ -151,10 +161,9 @@ class TMSSEventMessageHandlerForWebsocket(TMSSEventMessageHandler): def onSchedulingUnitBlueprintDeleted(self, id: int): self._post_update_on_websocket(id, self.ObjTypes.SCHED_UNIT_BLUEPRINT, self.ObjActions.DELETE) -def create_service(websocket_port: int=DEFAULT_WEBSOCKET_PORT, exchange: str=DEFAULT_BUSNAME, broker: str=DEFAULT_BROKER, rest_client_creds_id: str="TMSSClient"): +def create_service(websocket_port: int=DEFAULT_WEBSOCKET_PORT, exchange: str=DEFAULT_BUSNAME, broker: str=DEFAULT_BROKER): return TMSSBusListener(handler_type=TMSSEventMessageHandlerForWebsocket, - handler_kwargs={'websocket_port': websocket_port, - 'rest_client_creds_id': rest_client_creds_id}, + handler_kwargs={'websocket_port': websocket_port}, exchange=exchange, broker=broker) @@ -180,13 +189,14 @@ def main(): group = OptionGroup(parser, 'Django options') parser.add_option_group(group) - group.add_option('-R', '--rest_credentials', dest='rest_credentials', type='string', default='TMSSClient', help='django REST API credentials name, default: %default') + group.add_option('-C', '--credentials', dest='dbcredentials', type='string', default=os.environ.get('TMSS_DBCREDENTIALS', 'TMSS'), help='django dbcredentials name, default: %default') (options, args) = parser.parse_args() - TMSSsession.check_connection_and_exit_on_error(options.rest_credentials) + from lofar.sas.tmss.tmss import setup_and_check_tmss_django_database_connection_and_exit_on_error + setup_and_check_tmss_django_database_connection_and_exit_on_error(options.dbcredentials) - with create_service(options.websocket_port, options.exchange, options.broker, rest_client_creds_id=options.rest_credentials): + with create_service(options.websocket_port, options.exchange, options.broker): waitForInterrupt() if __name__ == '__main__': diff --git a/SAS/TMSS/backend/services/websocket/test/t_websocket_service.py b/SAS/TMSS/backend/services/websocket/test/t_websocket_service.py index 4454c6eec265334f334dfef331eb06d7293e971c..f3f8388cb9b361665964ba3660f926b2653bbfc0 100755 --- a/SAS/TMSS/backend/services/websocket/test/t_websocket_service.py +++ b/SAS/TMSS/backend/services/websocket/test/t_websocket_service.py @@ -107,7 +107,7 @@ class TestSubtaskSchedulingService(unittest.TestCase): websocket_port = find_free_port(DEFAULT_WEBSOCKET_PORT) # create and start the service (the object under test) - service = create_service(websocket_port=websocket_port, exchange=self.tmp_exchange.address, rest_client_creds_id=self.tmss_test_env.client_credentials.dbcreds_id) + service = create_service(websocket_port=websocket_port, exchange=self.tmp_exchange.address) with BusListenerJanitor(service): self.start_ws_client(websocket_port) # Start ws client @@ -118,9 +118,13 @@ class TestSubtaskSchedulingService(unittest.TestCase): raise TimeoutError() self.sync_event.clear() # Assert json_blobs - json_blob = {'id': json_test['id'], 'object_type': obj_type.value, 'action': action.value} + json_blob = {'object_details': {'id': json_test['id']}, 'object_type': obj_type.value, 'action': action.value} if action == self.ObjActions.CREATE or action == self.ObjActions.UPDATE: - json_blob['object'] = json_test + for key in ('start_time', 'stop_time', 'duration', 'status'): + if json_test.get(key) is not None: + json_blob['object_details'][key] = json_test[key] + if json_test.get('state_value') is not None: + json_blob['object_details']['state'] = json_test['state_value'] self.assertEqual(json_blob, self.msg_queue.popleft()) # Test creations diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/serializers/common.py b/SAS/TMSS/backend/src/tmss/tmssapp/serializers/common.py index 4758646f1d9a4c619bfe5dd87a2c0a06fc31bf3f..c12e879675249229317935fbcd6b883bd18239b0 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/serializers/common.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/serializers/common.py @@ -8,6 +8,13 @@ from django.core.exceptions import ImproperlyConfigured from .widgets import JSONEditorField from rest_flex_fields.serializers import FlexFieldsSerializerMixin +class FloatDurationField(serializers.FloatField): + + # Turn datetime to float representation in seconds. + # (Timedeltas are otherwise by default turned into a string representation) + def to_representation(self, value): + return value.total_seconds() + class RelationalHyperlinkedModelSerializer(serializers.HyperlinkedModelSerializer): _accepted_pk_names = ('id', 'name') diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/serializers/scheduling.py b/SAS/TMSS/backend/src/tmss/tmssapp/serializers/scheduling.py index 717833448d6a408247f2006d04ab067ea9d8cf4b..7c8bd8c29ee090cf6af7f48d6431e03418830c61 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/serializers/scheduling.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/serializers/scheduling.py @@ -8,7 +8,7 @@ logger = logging.getLogger(__name__) from rest_framework import serializers from .. import models from .widgets import JSONEditorField -from .common import RelationalHyperlinkedModelSerializer, AbstractTemplateSerializer, DynamicRelationalHyperlinkedModelSerializer +from .common import FloatDurationField, RelationalHyperlinkedModelSerializer, AbstractTemplateSerializer, DynamicRelationalHyperlinkedModelSerializer class SubtaskStateSerializer(DynamicRelationalHyperlinkedModelSerializer): class Meta: @@ -75,11 +75,12 @@ class SubtaskSerializer(DynamicRelationalHyperlinkedModelSerializer): # If this is OK then we can extend API with NO url ('flat' values) on more places if required cluster_value = serializers.StringRelatedField(source='cluster', label='cluster_value', read_only=True) specifications_doc = JSONEditorField(schema_source='specifications_template.schema') + duration = FloatDurationField(read_only=True) class Meta: model = models.Subtask fields = '__all__' - extra_fields = ['cluster_value'] + extra_fields = ['cluster_value', 'duration'] class SubtaskInputSerializer(DynamicRelationalHyperlinkedModelSerializer): diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/serializers/specification.py b/SAS/TMSS/backend/src/tmss/tmssapp/serializers/specification.py index d281087554ecbd86d4dd6c60753d9c977fab084e..7ac1a29773ff0b569b11ddd7db01ca73eb160bc8 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/serializers/specification.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/serializers/specification.py @@ -5,17 +5,10 @@ This file contains the serializers (for the elsewhere defined data models) from rest_framework import serializers from .. import models from .scheduling import SubtaskSerializer -from .common import RelationalHyperlinkedModelSerializer, AbstractTemplateSerializer, DynamicRelationalHyperlinkedModelSerializer +from .common import FloatDurationField, RelationalHyperlinkedModelSerializer, AbstractTemplateSerializer, DynamicRelationalHyperlinkedModelSerializer from .widgets import JSONEditorField from django.contrib.auth.models import User -class FloatDurationField(serializers.FloatField): - - # Turn datetime to float representation in seconds. - # (Timedeltas are otherwise by default turned into a string representation) - def to_representation(self, value): - return value.total_seconds() - # This is required for keeping a user reference as ForeignKey in other models # (I think so that the HyperlinkedModelSerializer can generate a URI) class UserSerializer(serializers.Serializer): diff --git a/SAS/TMSS/backend/src/tmss/workflowapp/migrations/0001_initial.py b/SAS/TMSS/backend/src/tmss/workflowapp/migrations/0001_initial.py index 9b78279183edf2b6a9a666365f8263f2770c9e36..7b5662531eebc0af1775682642c4f166cde2f02a 100644 --- a/SAS/TMSS/backend/src/tmss/workflowapp/migrations/0001_initial.py +++ b/SAS/TMSS/backend/src/tmss/workflowapp/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 3.0.9 on 2021-01-21 08:10 +# Generated by Django 3.0.9 on 2021-02-11 14:08 from django.db import migrations, models import django.db.models.deletion @@ -9,8 +9,8 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('tmssapp', '0001_initial'), ('viewflow', '0008_jsonfield_and_artifact'), + ('tmssapp', '0001_initial'), ] operations = [ @@ -25,7 +25,7 @@ class Migration(migrations.Migration): name='PIVerification', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('pi_report', models.CharField(max_length=150)), + ('pi_report', models.TextField()), ('pi_accept', models.BooleanField(default=False)), ], ), @@ -33,7 +33,7 @@ class Migration(migrations.Migration): name='QAReportingSOS', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('sos_report', models.CharField(max_length=150)), + ('sos_report', models.TextField()), ('quality_within_policy', models.BooleanField(default=False)), ('sos_accept_show_pi', models.BooleanField(default=False)), ], @@ -42,7 +42,7 @@ class Migration(migrations.Migration): name='QAReportingTO', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('operator_report', models.CharField(max_length=150)), + ('operator_report', models.TextField()), ('operator_accept', models.BooleanField(default=False)), ], ), diff --git a/SAS/TMSS/backend/src/tmss/workflowapp/models/schedulingunitflow.py b/SAS/TMSS/backend/src/tmss/workflowapp/models/schedulingunitflow.py index 3c1ed87a6036fe77867ad2cce289a7a3776a65b1..392a7168f104a48d9996eaa276fc4f7f34e10ffe 100644 --- a/SAS/TMSS/backend/src/tmss/workflowapp/models/schedulingunitflow.py +++ b/SAS/TMSS/backend/src/tmss/workflowapp/models/schedulingunitflow.py @@ -1,6 +1,6 @@ # Create your models here. -from django.db.models import CharField, IntegerField,BooleanField, ForeignKey, CASCADE, Model,NullBooleanField +from django.db.models import TextField, IntegerField,BooleanField, ForeignKey, CASCADE, Model,NullBooleanField from viewflow.models import Process, Task from viewflow.fields import FlowReferenceField from viewflow.compat import _ @@ -9,18 +9,18 @@ from lofar.sas.tmss.tmss.tmssapp.models import SchedulingUnitBlueprint class QAReportingTO(Model): - operator_report = CharField(max_length=150) + operator_report = TextField() operator_accept = BooleanField(default=False) class QAReportingSOS(Model): - sos_report = CharField(max_length=150) + sos_report = TextField() quality_within_policy = BooleanField(default=False) sos_accept_show_pi = BooleanField(default=False) class PIVerification(Model): - pi_report = CharField(max_length=150) + pi_report = TextField() pi_accept = BooleanField(default=False) diff --git a/SAS/TMSS/backend/test/test_utils.py b/SAS/TMSS/backend/test/test_utils.py index 0a100a41c23f8b74c884dac85d007dd12978c09c..c7d1aaa6823ff1c03e4488724965ec539faccfe3 100644 --- a/SAS/TMSS/backend/test/test_utils.py +++ b/SAS/TMSS/backend/test/test_utils.py @@ -396,7 +396,7 @@ class TMSSTestEnvironment: # this implies that _start_pg_listener should be true as well self._start_pg_listener = True from lofar.sas.tmss.services.websocket_service import create_service - self.websocket_service = create_service(exchange=self._exchange, broker=self._broker, rest_client_creds_id=self.client_credentials.dbcreds_id) + self.websocket_service = create_service(exchange=self._exchange, broker=self._broker) service_threads.append(threading.Thread(target=self.websocket_service.start_listening)) service_threads[-1].start() diff --git a/SAS/TMSS/frontend/tmss_webapp/.env b/SAS/TMSS/frontend/tmss_webapp/.env index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..fe03ed51b3f8a9b3bc3afcf2850ab7271f31ec22 100644 --- a/SAS/TMSS/frontend/tmss_webapp/.env +++ b/SAS/TMSS/frontend/tmss_webapp/.env @@ -0,0 +1 @@ +REACT_APP_WEBSOCKET_URL=ws://localhost:5678/ \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/package.json b/SAS/TMSS/frontend/tmss_webapp/package.json index b7c244ab27e1be39f21d882199581a491b5efbcb..4086fab8ac565363e54d1271f53e688091fcc5e5 100644 --- a/SAS/TMSS/frontend/tmss_webapp/package.json +++ b/SAS/TMSS/frontend/tmss_webapp/package.json @@ -51,6 +51,7 @@ "react-table": "^7.2.1", "react-table-plugins": "^1.3.1", "react-transition-group": "^2.5.1", + "react-websocket": "^2.1.0", "reactstrap": "^8.5.1", "styled-components": "^5.1.1", "suneditor-react": "^2.14.4", diff --git a/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/BetweenEditor.js b/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/BetweenEditor.js index 644620bc7655a8ab4a00b9e3e2ee1aff7d5e44bf..f82de002593b6c8555798e5a41e9f1c778e060dc 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/BetweenEditor.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/BetweenEditor.js @@ -61,56 +61,50 @@ export default class BetweenEditor extends Component { }); } - - /*isCancelAfterEnd(){console.log('after') - console.log('called') - this.copyDateValue(); - }*/ /** * Call the function on click Esc or Close the dialog */ -async copyDateValue(){ - let consolidateDates = ''; - this.state.rowData.map(row =>{ - if((row['from'] !== '' && row['from'] !== 'undefined') && (row['until'] !== '' && row['until'] !== 'undefined')){ - consolidateDates += ((row['from'] !== '')?moment(row['from']).format(DATE_TIME_FORMAT):'' )+","+((row['until'] !== '')?moment(row['until']).format(DATE_TIME_FORMAT):'')+"|"; - } - }); - await this.props.context.componentParent.updateTime( - this.props.node.rowIndex,this.props.colDef.field, consolidateDates - ); - this.setState({ showDialog: false}); - -} + async copyDateValue(){ + let consolidateDates = ''; + this.state.rowData.map(row =>{ + if((row['from'] !== '' && row['from'] !== 'undefined') && (row['until'] !== '' && row['until'] !== 'undefined')){ + consolidateDates += ((row['from'] !== '')?moment(row['from']).format(DATE_TIME_FORMAT):'' )+","+((row['until'] !== '')?moment(row['until']).format(DATE_TIME_FORMAT):'')+"|"; + } + }); + await this.props.context.componentParent.updateTime( + this.props.node.rowIndex,this.props.colDef.field, consolidateDates + ); + this.setState({ showDialog: false}); + } -/* - Set value in relevant field - */ -updateDateChanges(rowIndex, field, e){ - let tmpRows = this.state.rowData; - let row = tmpRows[rowIndex]; - row[field] = e.value; - tmpRows[rowIndex] = row; - if(this.state.rowData.length === rowIndex+1){ - let line = {'from': '', 'until': ''}; - tmpRows.push(line); + /* + Set value in relevant field + */ + updateDateChanges(rowIndex, field, e){ + let tmpRows = this.state.rowData; + let row = tmpRows[rowIndex]; + row[field] = e.value; + tmpRows[rowIndex] = row; + if(this.state.rowData.length === rowIndex+1){ + let line = {'from': '', 'until': ''}; + tmpRows.push(line); + } + this.setState({ + rowData: tmpRows + }); } - this.setState({ - rowData: tmpRows - }) -} -/* - Remove the the row from dialog -*/ -removeInput(rowIndex){ - let tmpRows = this.state.rowData; - delete tmpRows[rowIndex]; - this.setState({ - rowData: tmpRows - }) -} + /* + Remove the the row from dialog + */ + removeInput(rowIndex){ + let tmpRows = this.state.rowData; + delete tmpRows[rowIndex]; + this.setState({ + rowData: tmpRows + } ); + } render() { return ( diff --git a/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/BetweenRenderer.js b/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/BetweenRenderer.js index 90a8ca3d7fc4ca9f22084fd5c9e6db063e80366d..dbcdfad52b86b2a5647f3dab592d96ee1d6e9d90 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/BetweenRenderer.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/BetweenRenderer.js @@ -9,10 +9,22 @@ export default class BetweenRenderer extends Component { Show cell value in grid */ render() { - let row = this.props.agGridReact.props.rowData[this.props.node.rowIndex]; - let value = row[this.props.colDef.field]; - return <> {value && - value - }</>; + let row = []; + let value = ''; + if (this.props.colDef.field.startsWith('gdef_')) { + row = this.props.agGridReact.props.context.componentParent.state.commonRowData[0]; + value = row[this.props.colDef.field]; + } + else { + row = this.props.agGridReact.props.rowData[this.props.node.rowIndex]; + value = row[this.props.colDef.field]; + } + // let row = this.props.agGridReact.props.rowData[this.props.node.rowIndex]; + // let value = row[this.props.colDef.field]; + return <> + {value && + value + } + </>; } } \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/CustomDateComponent.js b/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/CustomDateComponent.js index 7e0c18e9b6926bb138c3c6b7667d67f7fa76d930..5d90aeb89f6636d1b733d66910ce716e89a79d35 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/CustomDateComponent.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/CustomDateComponent.js @@ -103,12 +103,9 @@ export default class CustomDateComponent extends Component { // LINKING THE UI, THE STATE AND AG-GRID //********************************************************************************* onDateChanged = (selectedDates) => { - //console.log('>>', selectedDates[0]) this.props.context.componentParent.updateTime( this.props.node.rowIndex,this.props.colDef.field,selectedDates[0] ); - - // this.updateAndNotifyAgGrid(selectedDates[0]); }; } \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/MultiSelector.js b/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/MultiSelector.js index 25c412381d7d0de2d58b7eef01211fa649f4029d..d3a2ed7731855b86d38c030c219cac7a28932223 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/MultiSelector.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/MultiSelector.js @@ -6,45 +6,47 @@ export default class SkySllector extends Component { constructor(props) { super(props); - this.dailyOptions= [ - {name: 'require_day', value: 'require_day'}, - {name: 'require_night', value: 'require_night'}, - {name: 'avoid_twilight', value: 'avoid_twilight'}, - ]; + this.dailyOptions= []; this.state= { daily: [], - + dailyOptions: [], } - this.callbackUpdateDailyCell = this.callbackUpdateDailyCell.bind(this); } async componentDidMount(){ - let selectedValues = this.props.data['daily']; + let selectedValues = null; + if (this.props.colDef.field.startsWith('gdef_')) { + selectedValues = this.props.data['gdef_daily']; + } + else { + selectedValues = this.props.data['daily']; + } + let tmpDailyValue = []; if(selectedValues && selectedValues.length>0){ - let tmpDailyValue = _.split(selectedValues, ","); - await this.setState({ - daily: tmpDailyValue, - }); + tmpDailyValue = _.split(selectedValues, ","); } - + await this.setState({ + daily: tmpDailyValue, + dailyOptions: this.props.context.componentParent.state.dailyOption + }); } async callbackUpdateDailyCell(e) { this.setState({ - daily: e.value + daily: e.value }) let dailyValue = ''; let selectedValues = e.value; await selectedValues.forEach( key =>{ - dailyValue += key+","; + dailyValue += key+","; }) dailyValue = _.trim(dailyValue) dailyValue = dailyValue.replace(/,([^,]*)$/, '' + '$1') this.props.context.componentParent.updateCell( - this.props.node.rowIndex,this.props.colDef.field,dailyValue - ); + this.props.node.rowIndex,this.props.colDef.field,dailyValue + ); } @@ -56,12 +58,14 @@ export default class SkySllector extends Component { } render() { return ( - <div className="col-sm-6"> - <MultiSelect optionLabel="name" value={this.state.daily} options={this.dailyOptions} - optionValue="value" filter={true} - onChange={this.callbackUpdateDailyCell} - /> - </div> + <div className="col-sm-6"> + {this.state.dailyOptions.length > 0 && + <MultiSelect optionLabel="name" value={this.state.daily} options={this.state.dailyOptions} + optionValue="value" filter={true} + onChange={this.callbackUpdateDailyCell} + /> + } + </div> ); } } \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/StationEditor.js b/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/StationEditor.js index b5280aeab0f64605fdbe20a536648d8169eed3f3..8b97161aac1c61d6ed917b39ba8d046c8243c995 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/StationEditor.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/StationEditor.js @@ -3,13 +3,20 @@ import React, { Component } from 'react'; import { Dialog } from 'primereact/dialog'; import { Button } from 'primereact/button'; import Stations from '../../routes/Scheduling/Stations'; + +//import moment from 'moment'; import _ from 'lodash'; +//const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; + export default class StationEditor extends Component { constructor(props) { super(props); this.tmpRowData = []; - + this.isDelete = false; + this.showDelete = false; + this.previousValue= ''; + this.doCancel = true; this.state = { schedulingUnit: {}, showDialog: false, @@ -18,10 +25,7 @@ export default class StationEditor extends Component { stationGroup: [], customSelectedStations: [] }; - this.formRules = { - name: {required: true, message: "Name can not be empty"}, - description: {required: true, message: "Description can not be empty"}, - }; + this.formRules = {}; } isPopup() { @@ -34,12 +38,14 @@ export default class StationEditor extends Component { async componentDidMount(){ let tmpStationGroups = []; let tmpStationGroup = {}; - + if ( this.props.colDef.field.startsWith('gdef_')) { + this.showDelete = true; + } let rowSU = this.props.agGridReact.props.rowData[this.props.node.rowIndex]; - let sgCellValue = rowSU[this.props.colDef.field]; + this.previousValue = rowSU[this.props.colDef.field]; - if(sgCellValue && sgCellValue.length >0){ - let stationGroups = _.split(sgCellValue, "|"); + if(this.previousValue && this.previousValue.length >0){ + let stationGroups = _.split(this.previousValue, "|"); stationGroups.map(stationGroup =>{ tmpStationGroup = {}; let sgValue = _.split(stationGroup, ":"); @@ -68,74 +74,58 @@ export default class StationEditor extends Component { } } -validateForm(fieldName) { - let validForm = false; - let errors = this.state.errors; - let validFields = this.state.validFields; - if (fieldName) { - delete errors[fieldName]; - delete validFields[fieldName]; - if (this.formRules[fieldName]) { - const rule = this.formRules[fieldName]; - const fieldValue = this.state.schedulingUnit[fieldName]; - if (rule.required) { - if (!fieldValue) { - errors[fieldName] = rule.message?rule.message:`${fieldName} is required`; - } else { - validFields[fieldName] = true; - } - } - } - } else { - errors = {}; - validFields = {}; - for (const fieldName in this.formRules) { - const rule = this.formRules[fieldName]; - const fieldValue = this.state.schedulingUnit[fieldName]; - if (rule.required) { - if (!fieldValue) { - errors[fieldName] = rule.message?rule.message:`${fieldName} is required`; - } else { - validFields[fieldName] = true; - } - } - } - } - this.setState({errors: errors, validFields: validFields}); - if (Object.keys(validFields).length === Object.keys(this.formRules).length) { - validForm = true; - } - return validForm && !this.state.missingStationFieldsErrors; +async deleteStationGroup() { + this.isDelete = true; + this.setState({ showDialog: false}); } -async updateStationGroup(){ +async closeStationGroup() { + this.isDelete = false; + this.doCancel = false; + this.setState({ showDialog: false}); +} + +async cancelStationGroup() { + this.isDelete = false; + this.doCancel = true; + this.setState({ showDialog: false}); +} + +async updateStationGroup() { let stationValue = ''; const station_groups = []; - (this.state.selectedStations || []).forEach(key => { - let station_group = {}; - const stations = this.state[key] ? this.state[key].stations : []; - const max_nr_missing = parseInt(this.state[key] ? this.state[key].missing_StationFields : 0); - station_group = { - stations, - max_nr_missing - }; - station_groups.push(station_group); - }); - this.state.customSelectedStations.forEach(station => { - station_groups.push({ - stations: station.stations, - max_nr_missing: parseInt(station.max_nr_missing) - }); - }); - if(station_groups){ - station_groups.map(stationGroup =>{ - stationValue += stationGroup.stations+':'+stationGroup.max_nr_missing+"|"; - }); + if (!this.isDelete) { + if (!this.doCancel) { + (this.state.selectedStations || []).forEach(key => { + let station_group = {}; + const stations = this.state[key] ? this.state[key].stations : []; + const max_nr_missing = parseInt(this.state[key] ? this.state[key].missing_StationFields : 0); + station_group = { + stations, + max_nr_missing + }; + station_groups.push(station_group); + }); + this.state.customSelectedStations.forEach(station => { + station_groups.push({ + stations: station.stations, + max_nr_missing: parseInt(station.max_nr_missing) + }); + }); + if(station_groups){ + station_groups.map(stationGroup =>{ + stationValue += stationGroup.stations+':'+stationGroup.max_nr_missing+"|"; + }); + } + } else { + stationValue = this.previousValue; + } } - this.setState({ showDialog: false}); + await this.props.context.componentParent.updateCell( this.props.node.rowIndex,this.props.colDef.field, stationValue ); + this.setState({ showDialog: false}); } onUpdateStations = (state, selectedStations, missingStationFieldsErrors, customSelectedStations) => { @@ -146,31 +136,34 @@ onUpdateStations = (state, selectedStations, missingStationFieldsErrors, customS customSelectedStations }, () => { this.setState({ - validForm: this.validateForm() + validForm: !missingStationFieldsErrors }); }); }; render() { - return ( - <> - <Dialog header={_.startCase(this.state.dialogTitle)} visible={this.state.showDialog} maximized={false} - onHide={() => {this.updateStationGroup()}} inputId="confirm_dialog" className="stations-dialog" - footer={<div> - <Button key="back" label="Close" onClick={() => {this.updateStationGroup()}} /> - </div> - } > - <div className="ag-theme-balham" style={{ height: '90%', width: '1000px', paddingLeft: '20px' }}> - <Stations - stationGroup={this.state.stationGroup} - onUpdateStations={this.onUpdateStations.bind(this)} - height={'30em'} - /> - </div> - </Dialog> - - </> - ); - } + return ( + <> + <Dialog header={_.startCase(this.state.dialogTitle)} visible={this.state.showDialog} maximized={false} + onHide={() => {this.updateStationGroup()}} inputId="confirm_dialog" className="stations_dialog" + footer={<div> + {this.showDelete && + <Button className="p-button-danger" icon="pi pi-trash" label="Clear All" onClick={() => {this.deleteStationGroup()}} /> + } + <Button label="OK" icon="pi pi-check" onClick={() => {this.closeStationGroup()}} disabled={!this.state.validForm} style={{width: '6em'}} /> + <Button className="p-button-danger" icon="pi pi-times" label="Cancel" onClick={() => {this.cancelStationGroup()}} /> + </div> + } > + <div className="ag-theme-balham" style={{ height: '90%', width: '1000px', paddingLeft: '20px' }}> + <Stations + stationGroup={this.state.stationGroup} + onUpdateStations={this.onUpdateStations.bind(this)} + height={'30em'} + /> + </div> + </Dialog> + </> + ); +} } \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/TimeInputmask.js b/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/TimeInputmask.js index ef773a00181db906bc02b311308c08e8cab813d0..d8047ddebd03812dffeaafefc1d4cfe711b8a44a 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/TimeInputmask.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/TimeInputmask.js @@ -32,12 +32,12 @@ export default class TimeInputMask extends Component { render() { return ( <InputMask - value={this.props.value} - mask="99:99:99" - placeholder="HH:mm:ss" - className="inputmask" - onComplete={this.callbackUpdateAngle} - ref={input =>{this.input = input}} + value={this.props.value} + mask="99:99:99" + placeholder="HH:mm:ss" + className="inputmask" + onComplete={this.callbackUpdateAngle} + ref={input =>{this.input = input}} /> ); } 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 b148112a09416472a9093ba56d3a547f163724f3..d57f1f7ea36f59bd3877a84e4cb5c9b01bc06a31 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/components/Timeline/CalendarTimeline.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/components/Timeline/CalendarTimeline.js @@ -675,13 +675,13 @@ export class CalendarTimeline extends Component { itemContext.dimensions.height -= 3; if (!this.props.showSunTimings && this.state.viewType === UIConstants.timeline.types.NORMAL) { if (item.type === "RESERVATION") { - itemContext.dimensions.top -= 20; - itemContext.dimensions.height += 20; + // itemContext.dimensions.top -= 20; + // itemContext.dimensions.height += 20; } else { - itemContext.dimensions.top -= 20; + // itemContext.dimensions.top -= 20; } } else if (this.state.viewType === UIConstants.timeline.types.WEEKVIEW) { - itemContext.dimensions.top -= (this.props.rowHeight-5); + // itemContext.dimensions.top -= (this.props.rowHeight-5); } else { if (item.type === "TASK") { itemContext.dimensions.top += 6; @@ -1248,12 +1248,15 @@ export class CalendarTimeline extends Component { * @param {Object} props */ async updateTimeline(props) { + let group = DEFAULT_GROUP.concat(props.group); if (!this.props.showSunTimings && this.state.viewType === UIConstants.timeline.types.NORMAL) { props.items = await this.addStationSunTimes(this.state.defaultStartTime, this.state.defaultEndTime, props.group, props.items); } else if(this.props.showSunTimings && this.state.viewType === UIConstants.timeline.types.NORMAL) { this.setNormalSuntimings(this.state.defaultStartTime, this.state.defaultEndTime); + } else if (this.state.viewType === UIConstants.timeline.types.WEEKVIEW) { + props.items = await this.addWeekSunTimes(this.state.defaultStartTime, this.state.defaultEndTime, group, props.items); } - this.setState({group: DEFAULT_GROUP.concat(props.group), items: _.orderBy(props.items, ['type'], ['desc'])}); + this.setState({group: group, items: _.orderBy(props.items, ['type'], ['desc'])}); } render() { diff --git a/SAS/TMSS/frontend/tmss_webapp/src/components/ViewTable.js b/SAS/TMSS/frontend/tmss_webapp/src/components/ViewTable.js index 582d29c631c6d11e07937cfb5556041ec1910900..a420bdbbfc193ec1771b8c0c1f0ea3952e28424a 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/components/ViewTable.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/components/ViewTable.js @@ -1,27 +1,27 @@ -import React, {useRef, useState } from "react"; -import { useSortBy, useTable, useFilters, useGlobalFilter, useAsyncDebounce, usePagination, useRowSelect } from 'react-table' +import React, { useRef, useState } from "react"; +import { useSortBy, useTable, useFilters, useGlobalFilter, useAsyncDebounce, usePagination, useRowSelect, useColumnOrder } from 'react-table' import matchSorter from 'match-sorter' import _ from 'lodash'; import moment from 'moment'; import { useHistory } from "react-router-dom"; -import {OverlayPanel} from 'primereact/overlaypanel'; +import { OverlayPanel } from 'primereact/overlaypanel'; //import {InputSwitch} from 'primereact/inputswitch'; -import {InputText} from 'primereact/inputtext'; +import { InputText } from 'primereact/inputtext'; import { Calendar } from 'primereact/calendar'; -import {Paginator} from 'primereact/paginator'; -import {TriStateCheckbox} from 'primereact/tristatecheckbox'; +import { Paginator } from 'primereact/paginator'; +import { TriStateCheckbox } from 'primereact/tristatecheckbox'; import { Slider } from 'primereact/slider'; import { Button } from "react-bootstrap"; import { Link } from "react-router-dom"; import { InputNumber } from "primereact/inputnumber"; -import {MultiSelect} from 'primereact/multiselect'; +import { MultiSelect } from 'primereact/multiselect'; import { RadioButton } from 'primereact/radiobutton'; import { useExportData } from "react-table-plugins"; import Papa from "papaparse"; import JsPDF from "jspdf"; import "jspdf-autotable"; -let tbldata =[], filteredData = [] ; +let tbldata = [], filteredData = []; let selectedRows = []; let isunittest = false; let showTopTotal = true; @@ -29,21 +29,21 @@ let showGlobalFilter = true; let showColumnFilter = true; let allowColumnSelection = true; let allowRowSelection = false; -let columnclassname =[]; +let columnclassname = []; let parentCallbackFunction, parentCBonSelection; let showCSV = false; let anyOfFilter = ''; // Define a default UI for filtering function GlobalFilter({ - preGlobalFilteredRows, - globalFilter, - setGlobalFilter, - }) { + preGlobalFilteredRows, + globalFilter, + setGlobalFilter, +}) { const [value, setValue] = React.useState(globalFilter) - const onChange = useAsyncDebounce(value => {setGlobalFilter(value || undefined)}, 200) + const onChange = useAsyncDebounce(value => { setGlobalFilter(value || undefined) }, 200) return ( - <span style={{marginLeft:"-10px"}}> + <span style={{ marginLeft: "-10px" }}> <input value={value || ""} onChange={e => { @@ -75,11 +75,11 @@ function DefaultColumnFilter({ setFilter(e.target.value || undefined) // Set undefined to remove the filter entirely }} /> - {value && <i onClick={() => {setFilter(undefined); setValue('') }} className="table-reset fa fa-times" />} + {value && <i onClick={() => { setFilter(undefined); setValue('') }} className="table-reset fa fa-times" />} </div> ) } - + /* Generate and download csv */ @@ -94,7 +94,7 @@ function getExportFileBlob({ columns, data, fileType, fileName }) { const headerNames = columns.map((column) => column.exportValue); const doc = new JsPDF(); var index = headerNames.indexOf('Action'); - if (index > -1) { + if (index > -1) { headerNames.splice(index, 1); } doc.autoTable({ @@ -117,35 +117,36 @@ function SelectColumnFilter({ setValue(''); } }, [filterValue, value]); - const options = React.useMemo(() => { - const options = new Set() + const options = React.useMemo(() => { + const options = new Set() preFilteredRows.forEach(row => { options.add(row.values[id]) }) return [...options.values()] }, [id, preFilteredRows]) - // Render a multi-select box + // Render a multi-select box return ( <div onClick={e => { e.stopPropagation() }}> - <select - style={{ - height: '24.2014px', - width: '60px', - border:'1px solid lightgrey', - }} - value={value} - onChange={e => { setValue(e.target.value); - setFilter(e.target.value|| undefined) - }} - > - <option value="">All</option> - {options.map((option, i) => ( - <option key={i} value={option}> - {option} - </option> - ))} + <select + style={{ + height: '24.2014px', + width: '60px', + border: '1px solid lightgrey', + }} + value={value} + onChange={e => { + setValue(e.target.value); + setFilter(e.target.value || undefined) + }} + > + <option value="">All</option> + {options.map((option, i) => ( + <option key={i} value={option}> + {option} + </option> + ))} </select> - </div> + </div> ) } @@ -157,63 +158,64 @@ function MultiSelectColumnFilter({ const [filtertype, setFiltertype] = useState('Any'); // Set Any / All Filter type const setSelectTypeOption = (option) => { - setFiltertype(option); - anyOfFilter = option - if(value !== ''){ - setFilter(value); - } + setFiltertype(option); + anyOfFilter = option + if (value !== '') { + setFilter(value); + } }; React.useEffect(() => { if (!filterValue && value) { - setValue(''); - setFiltertype('Any'); + setValue(''); + setFiltertype('Any'); } }, [filterValue, value, filtertype]); - anyOfFilter = filtertype; - const options = React.useMemo(() => { + anyOfFilter = filtertype; + const options = React.useMemo(() => { let options = new Set(); preFilteredRows.forEach(row => { - row.values[id].split(',').forEach( value => { - if ( value !== '') { - let hasValue = false; - options.forEach( option => { - if(option.name === value){ - hasValue = true; - } - }); - if(!hasValue) { - let option = { 'name': value, 'value':value}; - options.add(option); - } + row.values[id].split(',').forEach(value => { + if (value !== '') { + let hasValue = false; + options.forEach(option => { + if (option.name === value) { + hasValue = true; } - }); - }); + }); + if (!hasValue) { + let option = { 'name': value, 'value': value }; + options.add(option); + } + } + }); + }); return [...options.values()] }, [id, preFilteredRows]); - // Render a multi-select box + // Render a multi-select box return ( <div onClick={e => { e.stopPropagation() }} > - <div className="p-field-radiobutton"> - <RadioButton inputId="filtertype1" name="filtertype" value="Any" onChange={(e) => setSelectTypeOption(e.value)} checked={filtertype === 'Any'} /> - <label htmlFor="filtertype1">Any</label> - </div> - <div className="p-field-radiobutton"> - <RadioButton inputId="filtertype2" name="filtertype" value="All" onChange={(e) => setSelectTypeOption(e.value)} checked={filtertype === 'All'} /> - <label htmlFor="filtertype2">All</label> - </div> - <div style={{position: 'relative'}} > + <div className="p-field-radiobutton"> + <RadioButton inputId="filtertype1" name="filtertype" value="Any" onChange={(e) => setSelectTypeOption(e.value)} checked={filtertype === 'Any'} /> + <label htmlFor="filtertype1">Any</label> + </div> + <div className="p-field-radiobutton"> + <RadioButton inputId="filtertype2" name="filtertype" value="All" onChange={(e) => setSelectTypeOption(e.value)} checked={filtertype === 'All'} /> + <label htmlFor="filtertype2">All</label> + </div> + <div style={{ position: 'relative' }} > <MultiSelect data-testid="multi-select" id="multi-select" optionLabel="value" optionValue="value" filter={true} - value={value} - options={options} - onChange={e => { setValue(e.target.value); - setFilter(e.target.value|| undefined, filtertype) - }} - className="multi-select" + value={value} + options={options} + onChange={e => { + setValue(e.target.value); + setFilter(e.target.value || undefined, filtertype) + }} + className="multi-select" /> - </div> - </div> + </div> + </div> ) } @@ -238,7 +240,7 @@ function SliderColumnFilter({ return ( <div onClick={e => { e.stopPropagation() }} className="table-slider"> - <Slider value={value} onChange={(e) => { setFilter(e.value);setValue(e.value)}} /> + <Slider value={value} onChange={(e) => { setFilter(e.value); setValue(e.value) }} /> </div> ) } @@ -246,7 +248,7 @@ function SliderColumnFilter({ // This is a custom filter UI that uses a // switch to set the value function BooleanColumnFilter({ - column: { setFilter, filterValue}, + column: { setFilter, filterValue }, }) { // Calculate the min and max // using the preFilteredRows @@ -258,7 +260,7 @@ function BooleanColumnFilter({ }, [filterValue, value]); return ( <div onClick={e => { e.stopPropagation() }}> - <TriStateCheckbox value={value} style={{'width':'15px','height':'24.2014px'}} onChange={(e) => { setValue(e.value); setFilter(e.value === null ? undefined : e.value); }} /> + <TriStateCheckbox value={value} style={{ 'width': '15px', 'height': '24.2014px' }} onChange={(e) => { setValue(e.value); setFilter(e.value === null ? undefined : e.value); }} /> </div> ) } @@ -266,7 +268,7 @@ function BooleanColumnFilter({ // This is a custom filter UI that uses a // calendar to set the value function CalendarColumnFilter({ - column: { setFilter, filterValue}, + column: { setFilter, filterValue }, }) { // Calculate the min and max // using the preFilteredRows @@ -277,21 +279,21 @@ function CalendarColumnFilter({ } }, [filterValue, value]); return ( - + <div className="table-filter" onClick={e => { e.stopPropagation() }}> - <Calendar value={value} appendTo={document.body} onChange={(e) => { + <Calendar value={value} appendTo={document.body} onChange={(e) => { const value = moment(e.value, moment.ISO_8601).format("YYYY-MMM-DD") - setValue(value); setFilter(value); - }} showIcon></Calendar> - {value && <i onClick={() => {setFilter(undefined); setValue('') }} className="tb-cal-reset fa fa-times" />} - </div> + setValue(value); setFilter(value); + }} showIcon></Calendar> + {value && <i onClick={() => { setFilter(undefined); setValue('') }} className="tb-cal-reset fa fa-times" />} + </div> ) } // This is a custom filter UI that uses a // calendar to set the value function DateTimeColumnFilter({ - column: { setFilter, filterValue}, + column: { setFilter, filterValue }, }) { const [value, setValue] = useState(''); React.useEffect(() => { @@ -300,18 +302,18 @@ function DateTimeColumnFilter({ } }, [filterValue, value]); return ( - + <div className="table-filter" onClick={e => { e.stopPropagation() }}> - <Calendar value={value} appendTo={document.body} onChange={(e) => { + <Calendar value={value} appendTo={document.body} onChange={(e) => { const value = moment(e.value, moment.ISO_8601).format("YYYY-MMM-DD HH:mm:SS") - setValue(value); setFilter(value); - }} showIcon - // showTime= {true} - //showSeconds= {true} - // hourFormat= "24" - ></Calendar> - {value && <i onClick={() => {setFilter(undefined); setValue('') }} className="tb-cal-reset fa fa-times" />} - </div> + setValue(value); setFilter(value); + }} showIcon + // showTime= {true} + //showSeconds= {true} + // hourFormat= "24" + ></Calendar> + {value && <i onClick={() => { setFilter(undefined); setValue('') }} className="tb-cal-reset fa fa-times" />} + </div> ) } @@ -322,21 +324,21 @@ function DateTimeColumnFilter({ * @param {String} filterValue */ function fromDatetimeFilterFn(rows, id, filterValue) { - const filteredRows = _.filter(rows, function(row) { - // If cell value is null or empty - if (!row.values[id]) { - return false; - } - //Remove microsecond if value passed is UTC string in format "YYYY-MM-DDTHH:mm:ss.sssss" - let rowValue = moment.utc(row.values[id].split('.')[0]); - if (!rowValue.isValid()) { - // For cell data in format 'YYYY-MMM-DD' - rowValue = moment.utc(moment(row.values[id], 'YYYY-MMM-DDTHH:mm:SS').format("YYYY-MM-DDTHH:mm:SS")); - } - const start = moment.utc(moment(filterValue, 'YYYY-MMM-DDTHH:mm:SS').format("YYYY-MM-DDTHH:mm:SS")); - - return (start.isSameOrBefore(rowValue)); - } ); + const filteredRows = _.filter(rows, function (row) { + // If cell value is null or empty + if (!row.values[id]) { + return false; + } + //Remove microsecond if value passed is UTC string in format "YYYY-MM-DDTHH:mm:ss.sssss" + let rowValue = moment.utc(row.values[id].split('.')[0]); + if (!rowValue.isValid()) { + // For cell data in format 'YYYY-MMM-DD' + rowValue = moment.utc(moment(row.values[id], 'YYYY-MMM-DDTHH:mm:SS').format("YYYY-MM-DDTHH:mm:SS")); + } + const start = moment.utc(moment(filterValue, 'YYYY-MMM-DDTHH:mm:SS').format("YYYY-MM-DDTHH:mm:SS")); + + return (start.isSameOrBefore(rowValue)); + }); return filteredRows; } @@ -348,36 +350,36 @@ function fromDatetimeFilterFn(rows, id, filterValue) { */ function multiSelectFilterFn(rows, id, filterValue) { if (filterValue) { - const filteredRows = _.filter(rows, function(row) { - if ( filterValue.length === 0){ - return true; - } - // If cell value is null or empty - if (!row.values[id]) { - return false; - } - let rowValue = row.values[id]; - let hasData = false; - if ( anyOfFilter === 'Any' ) { - hasData = false; - filterValue.forEach(filter => { - if( rowValue.includes( filter )) { - hasData = true; - } - }); + const filteredRows = _.filter(rows, function (row) { + if (filterValue.length === 0) { + return true; + } + // If cell value is null or empty + if (!row.values[id]) { + return false; + } + let rowValue = row.values[id]; + let hasData = false; + if (anyOfFilter === 'Any') { + hasData = false; + filterValue.forEach(filter => { + if (rowValue.includes(filter)) { + hasData = true; } - else { - hasData = true; - filterValue.forEach(filter => { - if( !rowValue.includes( filter )) { - hasData = false; - } - }); + }); + } + else { + hasData = true; + filterValue.forEach(filter => { + if (!rowValue.includes(filter)) { + hasData = false; } - return hasData; - } ); - return filteredRows; - } + }); + } + return hasData; + }); + return filteredRows; + } } /** @@ -388,20 +390,20 @@ function multiSelectFilterFn(rows, id, filterValue) { */ function toDatetimeFilterFn(rows, id, filterValue) { let end = moment.utc(moment(filterValue, 'YYYY-MMM-DDTHH:mm:SS').format("YYYY-MM-DDTHH:mm:SS")); - end = moment(end, "DD-MM-YYYY").add(1, 'days'); - const filteredRows = _.filter(rows, function(row) { - // If cell value is null or empty - if (!row.values[id]) { - return false; - } - //Remove microsecond if value passed is UTC string in format "YYYY-MM-DDTHH:mm:ss.sssss" - let rowValue = moment.utc(row.values[id].split('.')[0]); - if (!rowValue.isValid()) { - // For cell data in format 'YYYY-MMM-DD' - rowValue = moment.utc(moment(row.values[id], 'YYYY-MMM-DDTHH:mm:SS').format("YYYY-MM-DDTHH:mm:SS")); - } - return (end.isSameOrAfter(rowValue)); - } ); + end = moment(end, "DD-MM-YYYY").add(1, 'days'); + const filteredRows = _.filter(rows, function (row) { + // If cell value is null or empty + if (!row.values[id]) { + return false; + } + //Remove microsecond if value passed is UTC string in format "YYYY-MM-DDTHH:mm:ss.sssss" + let rowValue = moment.utc(row.values[id].split('.')[0]); + if (!rowValue.isValid()) { + // For cell data in format 'YYYY-MMM-DD' + rowValue = moment.utc(moment(row.values[id], 'YYYY-MMM-DDTHH:mm:SS').format("YYYY-MM-DDTHH:mm:SS")); + } + return (end.isSameOrAfter(rowValue)); + }); return filteredRows; } @@ -412,28 +414,28 @@ function toDatetimeFilterFn(rows, id, filterValue) { * @param {String} filterValue */ function dateFilterFn(rows, id, filterValue) { - const filteredRows = _.filter(rows, function(row) { - // If cell value is null or empty - if (!row.values[id]) { - return false; - } - //Remove microsecond if value passed is UTC string in format "YYYY-MM-DDTHH:mm:ss.sssss" - let rowValue = moment.utc(row.values[id].split('.')[0]); - if (!rowValue.isValid()) { - // For cell data in format 'YYYY-MMM-DD' - rowValue = moment.utc(moment(row.values[id], 'YYYY-MMM-DD').format("YYYY-MM-DDT00:00:00")); - } - const start = moment.utc(moment(filterValue, 'YYYY-MMM-DD').format("YYYY-MM-DDT00:00:00")); - const end = moment.utc(moment(filterValue, 'YYYY-MMM-DD').format("YYYY-MM-DDT23:59:59")); - return (start.isSameOrBefore(rowValue) && end.isSameOrAfter(rowValue)); - } ); + const filteredRows = _.filter(rows, function (row) { + // If cell value is null or empty + if (!row.values[id]) { + return false; + } + //Remove microsecond if value passed is UTC string in format "YYYY-MM-DDTHH:mm:ss.sssss" + let rowValue = moment.utc(row.values[id].split('.')[0]); + if (!rowValue.isValid()) { + // For cell data in format 'YYYY-MMM-DD' + rowValue = moment.utc(moment(row.values[id], 'YYYY-MMM-DD').format("YYYY-MM-DDT00:00:00")); + } + const start = moment.utc(moment(filterValue, 'YYYY-MMM-DD').format("YYYY-MM-DDT00:00:00")); + const end = moment.utc(moment(filterValue, 'YYYY-MMM-DD').format("YYYY-MM-DDT23:59:59")); + return (start.isSameOrBefore(rowValue) && end.isSameOrAfter(rowValue)); + }); return filteredRows; } // This is a custom UI for our 'between' or number range // filter. It uses slider to filter between min and max values. function RangeColumnFilter({ - column: { filterValue = [], preFilteredRows, setFilter, id}, + column: { filterValue = [], preFilteredRows, setFilter, id }, }) { const [min, max] = React.useMemo(() => { let min = 0; @@ -442,8 +444,8 @@ function RangeColumnFilter({ min = preFilteredRows[0].values[id]; } preFilteredRows.forEach(row => { - min = Math.min(row.values[id]?row.values[id]:0, min); - max = Math.max(row.values[id]?row.values[id]:0, max); + min = Math.min(row.values[id] ? row.values[id] : 0, min); + max = Math.max(row.values[id] ? row.values[id] : 0, max); }); return [min, max]; }, [id, preFilteredRows]); @@ -454,12 +456,12 @@ function RangeColumnFilter({ return ( <> <div className="filter-slider-label"> - <span style={{float: "left"}}>{filterValue[0]}</span> - <span style={{float: "right"}}>{min!==max?filterValue[1]:""}</span> + <span style={{ float: "left" }}>{filterValue[0]}</span> + <span style={{ float: "right" }}>{min !== max ? filterValue[1] : ""}</span> </div> <Slider value={filterValue} min={min} max={max} className="filter-slider" - style={{}} - onChange={(e) => { setFilter(e.value); }} range /> + style={{}} + onChange={(e) => { setFilter(e.value); }} range /> </> ); } @@ -470,9 +472,9 @@ function RangeColumnFilter({ function NumberRangeColumnFilter({ column: { filterValue = [], preFilteredRows, setFilter, id }, }) { - const [errorProps, setErrorProps] = useState({}); - const [maxErr, setMaxErr] = useState(false); - const [min, max] = React.useMemo(() => { + const [errorProps, setErrorProps] = useState({}); + const [maxErr, setMaxErr] = useState(false); + const [min, max] = React.useMemo(() => { let min = preFilteredRows.length ? preFilteredRows[0].values[id] : 0 let max = preFilteredRows.length ? preFilteredRows[0].values[id] : 0 preFilteredRows.forEach(row => { @@ -485,8 +487,8 @@ function NumberRangeColumnFilter({ return ( <div style={{ - // display: 'flex', - // flexdirection:'column', + // display: 'flex', + // flexdirection:'column', alignItems: 'center' }} > @@ -495,16 +497,16 @@ function NumberRangeColumnFilter({ type="number" onChange={e => { const val = e.target.value; - setFilter((old = []) => [val ? parseFloat (val, 10) : undefined, old[1]]); + setFilter((old = []) => [val ? parseFloat(val, 10) : undefined, old[1]]); }} placeholder={`Min (${min})`} style={{ width: '55px', - height:'25px' - // marginRight: '0.5rem', + height: '25px' + // marginRight: '0.5rem', }} /> - <InputText + <InputText value={filterValue[1] || ''} type="number" {...errorProps} @@ -516,19 +518,19 @@ function NumberRangeColumnFilter({ setMaxErr(true); setErrorProps({ tooltip: "Max value should be greater than Min", - tooltipOptions: { event: 'hover'} + tooltipOptions: { event: 'hover' } }); } else { setMaxErr(false); setErrorProps({}); } - setFilter((old = []) => [old[0], val ? parseFloat (val, 10) : undefined]) + setFilter((old = []) => [old[0], val ? parseFloat(val, 10) : undefined]) }} placeholder={`Max (${max})`} style={{ width: '55px', - height:'25px' - // marginLeft: '0.5rem', + height: '25px' + // marginLeft: '0.5rem', }} /> </div> @@ -541,12 +543,12 @@ function fuzzyTextFilterFn(rows, id, filterValue) { } const filterTypes = { - 'select': { + 'select': { fn: SelectColumnFilter, }, - 'multiselect': { + 'multiselect': { fn: MultiSelectColumnFilter, - type: multiSelectFilterFn + type: multiSelectFilterFn }, 'switch': { fn: BooleanColumnFilter @@ -570,7 +572,7 @@ const filterTypes = { fn: RangeColumnFilter, type: 'between' }, - 'minMax': { + 'minMax': { fn: NumberRangeColumnFilter, type: 'between' } @@ -590,8 +592,8 @@ const IndeterminateCheckbox = React.forwardRef( ) // Our table component -function Table({ columns, data, defaultheader, optionalheader, tablename, defaultSortColumn,defaultpagesize }) { - +function Table({ columns, data, defaultheader, optionalheader, tablename, defaultSortColumn, defaultpagesize, columnOrders, showAction }) { + const filterTypes = React.useMemo( () => ({ // Add a new fuzzyTextFilterFn filter type. @@ -603,8 +605,8 @@ function Table({ columns, data, defaultheader, optionalheader, tablename, defaul const rowValue = row.values[id] return rowValue !== undefined ? String(rowValue) - .toLowerCase() - .startsWith(String(filterValue).toLowerCase()) + .toLowerCase() + .startsWith(String(filterValue).toLowerCase()) : true }) }, @@ -613,73 +615,87 @@ function Table({ columns, data, defaultheader, optionalheader, tablename, defaul ) const defaultColumn = React.useMemo( - () => ({ - // Let's set up our default Filter UI - Filter: DefaultColumnFilter, - - }), - [] - ) - - const { - getTableProps, - getTableBodyProps, - headerGroups, - rows, - prepareRow, - setAllFilters, - allColumns, - getToggleHideAllColumnsProps, - state, - page, - preGlobalFilteredRows, - setGlobalFilter, - setHiddenColumns, - gotoPage, - setPageSize, - selectedFlatRows, - exportData, - } = useTable( - { - columns, - data, - defaultColumn, - filterTypes, - initialState: { pageIndex: 0, - pageSize: (defaultpagesize && defaultpagesize>0)?defaultpagesize:10, - sortBy: defaultSortColumn }, - getExportFileBlob, - }, - useFilters, - useGlobalFilter, - useSortBy, - usePagination, - useRowSelect, - useExportData - ); - React.useEffect(() => { - setHiddenColumns( - columns.filter(column => !column.isVisible).map(column => column.accessor) - ); - }, [setHiddenColumns, columns]); + () => ({ + // Let's set up our default Filter UI + Filter: DefaultColumnFilter, - let op = useRef(null); + }), + [] + ) - const [currentpage, setcurrentPage] = React.useState(0); - const [currentrows, setcurrentRows] = React.useState(defaultpagesize); - const [custompagevalue,setcustompagevalue] = React.useState(); + const { + getTableProps, + getTableBodyProps, + headerGroups, + rows, + prepareRow, + setAllFilters, + allColumns, + getToggleHideAllColumnsProps, + visibleColumns, + state, + page, + preGlobalFilteredRows, + setGlobalFilter, + setHiddenColumns, + gotoPage, + setPageSize, + selectedFlatRows, + setColumnOrder, + exportData, + } = useTable( + { + columns, + data, + defaultColumn, + filterTypes, + initialState: { + pageIndex: 0, + pageSize: (defaultpagesize && defaultpagesize > 0) ? defaultpagesize : 10, + sortBy: defaultSortColumn + }, + getExportFileBlob, + }, + useFilters, + useGlobalFilter, + useSortBy, + usePagination, + useRowSelect, + useColumnOrder, + useExportData + ); + React.useEffect(() => { + setHiddenColumns( + columns.filter(column => !column.isVisible).map(column => column.accessor) + ); + // console.log('columns List', visibleColumns.map((d) => d.id)); + if (columnOrders && columnOrders.length) { + if (showAction === 'true') { + setColumnOrder(['Select', 'Action', ...columnOrders]); + } else { + setColumnOrder(['Select', ...columnOrders]); + } + } + + }, [setHiddenColumns, columns]); + + let op = useRef(null); + + const [currentpage, setcurrentPage] = React.useState(0); + const [currentrows, setcurrentRows] = React.useState(defaultpagesize); + const [custompagevalue, setcustompagevalue] = React.useState(); const onPagination = (e) => { - gotoPage(e.page); - setcurrentPage(e.first); - setcurrentRows(e.rows); - setPageSize(e.rows) - if([10,25,50,100].includes(e.rows)){ - setcustompagevalue(); - } - }; + gotoPage(e.page); + setcurrentPage(e.first); + setcurrentRows(e.rows); + setPageSize(e.rows) + if ([10, 25, 50, 100].includes(e.rows)) { + setcustompagevalue(); + } + }; const onCustomPage = (e) => { - if(typeof custompagevalue === 'undefined' || custompagevalue == null) return; + if (typeof custompagevalue === 'undefined' || custompagevalue == null) return; gotoPage(0); setcurrentPage(0); setcurrentRows(custompagevalue); @@ -689,7 +705,7 @@ function Table({ columns, data, defaultheader, optionalheader, tablename, defaul const onChangeCustompagevalue = (e) => { setcustompagevalue(e.target.value); } - + const onShowAllPage = (e) => { gotoPage(e.page); setcurrentPage(e.first); @@ -698,16 +714,16 @@ function Table({ columns, data, defaultheader, optionalheader, tablename, defaul setcustompagevalue(); }; - const onToggleChange = (e) =>{ + const onToggleChange = (e) => { let lsToggleColumns = []; - allColumns.forEach( acolumn =>{ + allColumns.forEach(acolumn => { let jsonobj = {}; - let visible = (acolumn.Header === e.target.id) ? ((acolumn.isVisible)?false:true) :acolumn.isVisible + let visible = (acolumn.Header === e.target.id) ? ((acolumn.isVisible) ? false : true) : acolumn.isVisible jsonobj['Header'] = acolumn.Header; jsonobj['isVisible'] = visible; - lsToggleColumns.push(jsonobj) + lsToggleColumns.push(jsonobj) }) - localStorage.setItem(tablename,JSON.stringify(lsToggleColumns)) + localStorage.setItem(tablename, JSON.stringify(lsToggleColumns)) } filteredData = _.map(rows, 'values'); @@ -716,75 +732,75 @@ function Table({ columns, data, defaultheader, optionalheader, tablename, defaul } /* Select only rows than can be selected. This is required when ALL is selected */ - selectedRows = _.filter(selectedFlatRows, selectedRow => { return (selectedRow.original.canSelect===undefined || selectedRow.original.canSelect)}); + selectedRows = _.filter(selectedFlatRows, selectedRow => { return (selectedRow.original.canSelect === undefined || selectedRow.original.canSelect) }); /* Take only the original values passed to the component */ selectedRows = _.map(selectedRows, 'original'); /* Callback the parent function if available to pass the selected records on selection */ if (parentCBonSelection) { parentCBonSelection(selectedRows) } - + return ( <> - <div style={{display: 'flex', justifyContent: 'space-between'}}> - <div id="block_container" > - { allowColumnSelection && - <div style={{textAlign:'left', marginRight:'30px'}}> - <i className="fa fa-columns col-filter-btn" label="Toggle Columns" onClick={(e) => op.current.toggle(e)} /> - {showColumnFilter && - <div style={{position:"relative",top: "-25px",marginLeft: "50px",color: "#005b9f"}} onClick={() => setAllFilters([])} > - <i class="fas fa-sync-alt" title="Clear All Filters"></i></div>} - <OverlayPanel ref={op} id="overlay_panel" showCloseIcon={false} > - <div> - <div style={{textAlign: 'center'}}> - <label>Select column(s) to view</label> - </div> - <div style={{float: 'left', backgroundColor: '#d1cdd936', width: '250px', height: '400px', overflow: 'auto', marginBottom:'10px', padding:'5px'}}> - <div id="tagleid" > - <div > - <div style={{marginBottom:'5px'}}> - <IndeterminateCheckbox {...getToggleHideAllColumnsProps()} /> Select All + <div style={{ display: 'flex', justifyContent: 'space-between' }}> + <div id="block_container" > + {allowColumnSelection && + <div style={{ textAlign: 'left', marginRight: '30px' }}> + <i className="fa fa-columns col-filter-btn" label="Toggle Columns" onClick={(e) => op.current.toggle(e)} /> + {showColumnFilter && + <div style={{ position: "relative", top: "-25px", marginLeft: "50px", color: "#005b9f" }} onClick={() => setAllFilters([])} > + <i class="fas fa-sync-alt" title="Clear All Filters"></i></div>} + <OverlayPanel ref={op} id="overlay_panel" showCloseIcon={false} > + <div> + <div style={{ textAlign: 'center' }}> + <label>Select column(s) to view</label> + </div> + <div style={{ float: 'left', backgroundColor: '#d1cdd936', width: '250px', height: '400px', overflow: 'auto', marginBottom: '10px', padding: '5px' }}> + <div id="tagleid" > + <div > + <div style={{ marginBottom: '5px' }}> + <IndeterminateCheckbox {...getToggleHideAllColumnsProps()} /> Select All </div> - {allColumns.map(column => ( - <div key={column.id} style={{'display':column.id !== 'actionpath'?'block':'none'}}> - <input type="checkbox" {...column.getToggleHiddenProps()} - id={(defaultheader[column.id])?defaultheader[column.id]:(optionalheader[column.id]?optionalheader[column.id]:column.id)} - onClick={onToggleChange} - /> { - (defaultheader[column.id]) ? defaultheader[column.id] : (optionalheader[column.id] ? optionalheader[column.id] : column.id)} - </div> - ))} - <br /> + {allColumns.map(column => ( + <div key={column.id} style={{ 'display': column.id !== 'actionpath' ? 'block' : 'none' }}> + <input type="checkbox" {...column.getToggleHiddenProps()} + id={(defaultheader[column.id]) ? defaultheader[column.id] : (optionalheader[column.id] ? optionalheader[column.id] : column.id)} + onClick={onToggleChange} + /> { + (defaultheader[column.id]) ? defaultheader[column.id] : (optionalheader[column.id] ? optionalheader[column.id] : column.id)} </div> - </div> + ))} + <br /> </div> </div> - </OverlayPanel> - </div> - } - <div style={{textAlign:'right'}}> - {tbldata.length>0 && !isunittest && showGlobalFilter && + </div> + </div> + </OverlayPanel> + </div> + } + <div style={{ textAlign: 'right' }}> + {tbldata.length > 0 && !isunittest && showGlobalFilter && <GlobalFilter preGlobalFilteredRows={preGlobalFilteredRows} globalFilter={state.globalFilter} setGlobalFilter={setGlobalFilter} /> } - </div> - - - { showTopTotal && filteredData.length === data.length && - <div className="total_records_top_label"> <label >Total records ({data.length})</label></div> - } - - { showTopTotal && filteredData.length < data.length && + </div> + + + {showTopTotal && filteredData.length === data.length && + <div className="total_records_top_label"> <label >Total records ({data.length})</label></div> + } + + {showTopTotal && filteredData.length < data.length && <div className="total_records_top_label" ><label >Filtered {filteredData.length} from {data.length}</label></div>} - - </div> - {showCSV && - <div className="total_records_top_label" style={{marginTop: '20px'}} > - <a href="#" onClick={() => {exportData("csv", false);}} title="Download CSV" style={{verticalAlign: 'middle'}}> - <i class="fas fa-file-csv" style={{color: 'green', fontSize: '20px'}} ></i> + + </div> + {showCSV && + <div className="total_records_top_label" style={{ marginTop: '20px' }} > + <a href="#" onClick={() => { exportData("csv", false); }} title="Download CSV" style={{ verticalAlign: 'middle' }}> + <i class="fas fa-file-csv" style={{ color: 'green', fontSize: '20px' }} ></i> </a> </div> /* @@ -794,80 +810,80 @@ function Table({ columns, data, defaultheader, optionalheader, tablename, defaul </a> </div> */ } - </div> - + </div> + <div className="tmss-table table_container"> <table {...getTableProps()} data-testid="viewtable" className="viewtable" > - <thead> - {headerGroups.map(headerGroup => ( - <tr {...headerGroup.getHeaderGroupProps()}> - {headerGroup.headers.map(column => ( - <th> - <div {...column.getHeaderProps(column.getSortByToggleProps())}> - {column.Header !== 'actionpath' && column.render('Header')} - {column.Header !== 'Action'? - column.isSorted ? (column.isSortedDesc ? <i className="pi pi-sort-down" aria-hidden="true"></i> : <i className="pi pi-sort-up" aria-hidden="true"></i>) : "" - : "" - } - </div> + <thead> + {headerGroups.map(headerGroup => ( + <tr {...headerGroup.getHeaderGroupProps()}> + {headerGroup.headers.map(column => ( + <th> + <div {...column.getHeaderProps(column.getSortByToggleProps())}> + {column.Header !== 'actionpath' && column.render('Header')} + {column.Header !== 'Action' ? + column.isSorted ? (column.isSortedDesc ? <i className="pi pi-sort-down" aria-hidden="true"></i> : <i className="pi pi-sort-up" aria-hidden="true"></i>) : "" + : "" + } + </div> - {/* Render the columns filter UI */} - {column.Header !== 'actionpath' && - <div className={columnclassname[0][column.Header]} > - {column.canFilter && column.Header !== 'Action' ? column.render('Filter') : null} + {/* Render the columns filter UI */} + {column.Header !== 'actionpath' && + <div className={columnclassname[0][column.Header]} > + {column.canFilter && column.Header !== 'Action' ? column.render('Filter') : null} - </div> - } - </th> - ))} - </tr> - ))} - </thead> - <tbody {...getTableBodyProps()}> - {page.map((row, i) => { - prepareRow(row) - return ( - <tr {...row.getRowProps()}> - {row.cells.map(cell => { - if(cell.column.id !== 'actionpath'){ - return <td {...cell.getCellProps()}> - {(cell.row.original.links || []).includes(cell.column.id) ? <Link to={cell.row.original.linksURL[cell.column.id]}>{cell.render('Cell')}</Link> : cell.render('Cell')} - </td> - } - else { - return ""; - } - } - )} - </tr> - ); - })} - </tbody> - </table> + </div> + } + </th> + ))} + </tr> + ))} + </thead> + <tbody {...getTableBodyProps()}> + {page.map((row, i) => { + prepareRow(row) + return ( + <tr {...row.getRowProps()}> + {row.cells.map(cell => { + if (cell.column.id !== 'actionpath') { + return <td {...cell.getCellProps()}> + {(cell.row.original.links || []).includes(cell.column.id) ? <Link to={cell.row.original.linksURL[cell.column.id]}>{cell.render('Cell')}</Link> : cell.render('Cell')} + </td> + } + else { + return ""; + } + } + )} + </tr> + ); + })} + </tbody> + </table> + </div> + <div className="pagination p-grid" > + {filteredData.length === data.length && + <div className="total_records_bottom_label" ><label >Total records ({data.length})</label></div> + } + {filteredData.length < data.length && + <div className="total_records_bottom_label" ><label >Filtered {filteredData.length} from {data.length}</label></div> + } + <div> + <Paginator rowsPerPageOptions={[10, 25, 50, 100]} first={currentpage} rows={currentrows} totalRecords={rows.length} onPageChange={onPagination}></Paginator> + </div> + <div> + <InputNumber id="custompage" value={custompagevalue} onChange={onChangeCustompagevalue} + min={0} style={{ width: '100px' }} /> + <label >Records/Page</label> + <Button onClick={onCustomPage}> Show </Button> + <Button onClick={onShowAllPage} style={{ marginLeft: "1em" }}> Show All </Button> </div> - <div className="pagination p-grid" > - {filteredData.length === data.length && - <div className="total_records_bottom_label" ><label >Total records ({data.length})</label></div> - } - {filteredData.length < data.length && - <div className="total_records_bottom_label" ><label >Filtered {filteredData.length} from {data.length}</label></div> - } - <div> - <Paginator rowsPerPageOptions={[10,25,50,100]} first={currentpage} rows={currentrows} totalRecords={rows.length} onPageChange={onPagination}></Paginator> - </div> - <div> - <InputNumber id="custompage" value={custompagevalue} onChange ={onChangeCustompagevalue} - min={0} style={{width:'100px'}} /> - <label >Records/Page</label> - <Button onClick={onCustomPage}> Show </Button> - <Button onClick={onShowAllPage} style={{marginLeft: "1em"}}> Show All </Button> - </div> </div> </> ) } - + // Define a custom filter filter function! function filterGreaterThan(rows, id, filterValue) { @@ -884,90 +900,94 @@ function filterGreaterThan(rows, id, filterValue) { filterGreaterThan.autoRemove = val => typeof val !== 'number' function ViewTable(props) { - const history = useHistory(); - // Data to show in table - tbldata = props.data; - showCSV= (props.showCSV)?props.showCSV:false; - - parentCallbackFunction = props.filterCallback; - parentCBonSelection = props.onRowSelection; - isunittest = props.unittest; - columnclassname = props.columnclassname; - showTopTotal = props.showTopTotal===undefined?true:props.showTopTotal; - showGlobalFilter = props.showGlobalFilter===undefined?true:props.showGlobalFilter; - showColumnFilter = props.showColumnFilter===undefined?true:props.showColumnFilter; - allowColumnSelection = props.allowColumnSelection===undefined?true:props.allowColumnSelection; - allowRowSelection = props.allowRowSelection===undefined?false:props.allowRowSelection; - // Default Header to show in table and other columns header will not show until user action on UI - let defaultheader = props.defaultcolumns; - let optionalheader = props.optionalcolumns; - let defaultSortColumn = props.defaultSortColumn; - let tablename = (props.tablename)?props.tablename:window.location.pathname; - - if(!defaultSortColumn){ - defaultSortColumn =[{}]; - } - let defaultpagesize = (typeof props.defaultpagesize === 'undefined' || props.defaultpagesize == null)?10:props.defaultpagesize; - let columns = []; - let defaultdataheader = Object.keys(defaultheader[0]); - let optionaldataheader = Object.keys(optionalheader[0]); - - /* If allowRowSelection property is true for the component, add checkbox column as 1st column. - If the record has property to select, enable the checkbox */ - if (allowRowSelection) { - columns.push({ - Header: ({ getToggleAllRowsSelectedProps }) => { return ( + const history = useHistory(); + // Data to show in table + tbldata = props.data; + showCSV = (props.showCSV) ? props.showCSV : false; + + parentCallbackFunction = props.filterCallback; + parentCBonSelection = props.onRowSelection; + isunittest = props.unittest; + columnclassname = props.columnclassname; + showTopTotal = props.showTopTotal === undefined ? true : props.showTopTotal; + showGlobalFilter = props.showGlobalFilter === undefined ? true : props.showGlobalFilter; + showColumnFilter = props.showColumnFilter === undefined ? true : props.showColumnFilter; + allowColumnSelection = props.allowColumnSelection === undefined ? true : props.allowColumnSelection; + allowRowSelection = props.allowRowSelection === undefined ? false : props.allowRowSelection; + // Default Header to show in table and other columns header will not show until user action on UI + let defaultheader = props.defaultcolumns; + let optionalheader = props.optionalcolumns; + let defaultSortColumn = props.defaultSortColumn; + let tablename = (props.tablename) ? props.tablename : window.location.pathname; + + if (!defaultSortColumn) { + defaultSortColumn = [{}]; + } + let defaultpagesize = (typeof props.defaultpagesize === 'undefined' || props.defaultpagesize == null) ? 10 : props.defaultpagesize; + let columns = []; + let defaultdataheader = Object.keys(defaultheader[0]); + let optionaldataheader = Object.keys(optionalheader[0]); + + /* If allowRowSelection property is true for the component, add checkbox column as 1st column. + If the record has property to select, enable the checkbox */ + if (allowRowSelection) { + columns.push({ + Header: ({ getToggleAllRowsSelectedProps }) => { + return ( <div> - <IndeterminateCheckbox {...getToggleAllRowsSelectedProps()} style={{width:'15px', height:'15px'}}/> + <IndeterminateCheckbox {...getToggleAllRowsSelectedProps()} style={{ width: '15px', height: '15px' }} /> </div> - )}, - id:'Select', - accessor: props.keyaccessor, - Cell: ({ row }) => { return ( + ) + }, + id: 'Select', + accessor: props.keyaccessor, + Cell: ({ row }) => { + return ( <div> - {(row.original.canSelect===undefined || row.original.canSelect) && - <IndeterminateCheckbox {...row.getToggleRowSelectedProps()} style={{width:'15px', height:'15px'}}/> + {(row.original.canSelect === undefined || row.original.canSelect) && + <IndeterminateCheckbox {...row.getToggleRowSelectedProps()} style={{ width: '15px', height: '15px' }} /> } - {row.original.canSelect===false && - <input type="checkbox" checked={false} disabled style={{width:'15px', height:'15px'}}></input> + {row.original.canSelect === false && + <input type="checkbox" checked={false} disabled style={{ width: '15px', height: '15px' }}></input> } </div> - )}, - disableFilters: true, - disableSortBy: true, - isVisible: defaultdataheader.includes(props.keyaccessor), - }); - } - - if(props.showaction === 'true') { - columns.push({ - Header: 'Action', - id:'Action', - accessor: props.keyaccessor, - Cell: props => <button className='p-link' onClick={navigateTo(props)} ><i className="fa fa-eye" style={{cursor: 'pointer'}}></i></button>, - disableFilters: true, - disableSortBy: true, - isVisible: defaultdataheader.includes(props.keyaccessor), - }) - } - - const navigateTo = (props) => () => { - if(props.cell.row.values['actionpath']){ - return history.push({ - pathname: props.cell.row.values['actionpath'], - state: { - "id": props.value, - } - }) - } - // Object.entries(props.paths[0]).map(([key,value]) =>{}) + ) + }, + disableFilters: true, + disableSortBy: true, + isVisible: defaultdataheader.includes(props.keyaccessor), + }); + } + + if (props.showaction === 'true') { + columns.push({ + Header: 'Action', + id: 'Action', + accessor: props.keyaccessor, + Cell: props => <button className='p-link' onClick={navigateTo(props)} ><i className="fa fa-eye" style={{ cursor: 'pointer' }}></i></button>, + disableFilters: true, + disableSortBy: true, + isVisible: defaultdataheader.includes(props.keyaccessor), + }) + } + + const navigateTo = (props) => () => { + if (props.cell.row.values['actionpath']) { + return history.push({ + pathname: props.cell.row.values['actionpath'], + state: { + "id": props.value, + } + }) } + // Object.entries(props.paths[0]).map(([key,value]) =>{}) + } //Default Columns defaultdataheader.forEach(header => { const isString = typeof defaultheader[0][header] === 'string'; - const filterFn = (showColumnFilter?(isString ? DefaultColumnFilter : (filterTypes[defaultheader[0][header].filter].fn ? filterTypes[defaultheader[0][header].filter].fn : DefaultColumnFilter)):""); - const filtertype = (showColumnFilter?(!isString && filterTypes[defaultheader[0][header].filter].type) ? filterTypes[defaultheader[0][header].filter].type : 'fuzzyText':""); + const filterFn = (showColumnFilter ? (isString ? DefaultColumnFilter : (filterTypes[defaultheader[0][header].filter].fn ? filterTypes[defaultheader[0][header].filter].fn : DefaultColumnFilter)) : ""); + const filtertype = (showColumnFilter ? (!isString && filterTypes[defaultheader[0][header].filter].type) ? filterTypes[defaultheader[0][header].filter].type : 'fuzzyText' : ""); columns.push({ Header: isString ? defaultheader[0][header] : defaultheader[0][header].name, id: isString ? defaultheader[0][header] : defaultheader[0][header].name, @@ -979,72 +999,72 @@ function ViewTable(props) { // Filter: (showColumnFilter?(isString ? DefaultColumnFilter : (filterTypes[defaultheader[0][header].filter] ? filterTypes[defaultheader[0][header].filter] : DefaultColumnFilter)):""), isVisible: true, Cell: props => <div> {updatedCellvalue(header, props.value)} </div>, - }) -}) - -//Optional Columns -optionaldataheader.forEach(header => { - const isString = typeof optionalheader[0][header] === 'string'; - const filterFn = (showColumnFilter?(isString ? DefaultColumnFilter : (filterTypes[optionalheader[0][header].filter].fn ? filterTypes[optionalheader[0][header].filter].fn : DefaultColumnFilter)):""); - const filtertype = (showColumnFilter?(!isString && filterTypes[optionalheader[0][header].filter].type) ? filterTypes[optionalheader[0][header].filter].type : 'fuzzyText':""); + }) + }) + + //Optional Columns + optionaldataheader.forEach(header => { + const isString = typeof optionalheader[0][header] === 'string'; + const filterFn = (showColumnFilter ? (isString ? DefaultColumnFilter : (filterTypes[optionalheader[0][header].filter].fn ? filterTypes[optionalheader[0][header].filter].fn : DefaultColumnFilter)) : ""); + const filtertype = (showColumnFilter ? (!isString && filterTypes[optionalheader[0][header].filter].type) ? filterTypes[optionalheader[0][header].filter].type : 'fuzzyText' : ""); columns.push({ Header: isString ? optionalheader[0][header] : optionalheader[0][header].name, id: isString ? header : optionalheader[0][header].name, - accessor: isString ? header : optionalheader[0][header].name, + accessor: isString ? header : optionalheader[0][header].name, filter: filtertype, Filter: filterFn, isVisible: false, Cell: props => <div> {updatedCellvalue(header, props.value)} </div>, + }) + }); + + let togglecolumns = localStorage.getItem(tablename); + if (togglecolumns) { + togglecolumns = JSON.parse(togglecolumns) + columns.forEach(column => { + togglecolumns.filter(tcol => { + column.isVisible = (tcol.Header === column.Header) ? tcol.isVisible : column.isVisible; + return tcol; }) - }); - - let togglecolumns = localStorage.getItem(tablename); - if(togglecolumns){ - togglecolumns = JSON.parse(togglecolumns) - columns.forEach(column =>{ - togglecolumns.filter(tcol => { - column.isVisible = (tcol.Header === column.Header)?tcol.isVisible:column.isVisible; - return tcol; - }) - }) - } + }) + } - function updatedCellvalue(key, value){ - try{ - if(key === 'blueprint_draft' && _.includes(value,'/task_draft/')){ - // 'task_draft/' -> len = 12 - var taskid = _.replace(value.substring((value.indexOf('/task_draft/')+12), value.length),'/',''); - return <a href={'/task/view/draft/'+taskid}>{' '+taskid+' '}</a> - }else if(key === 'blueprint_draft'){ - var retval= []; - value.forEach((link, index) =>{ - // 'task_blueprint/' -> len = 16 - if(_.includes(link,'/task_blueprint/')){ - var bpid = _.replace(link.substring((link.indexOf('/task_blueprint/')+16), link.length),'/',''); - retval.push( <a href={'/task/view/blueprint/'+bpid} key={bpid+index} >{' '+bpid+' '}</a> ) - } - }) - return retval; - }else if(typeof value == "boolean"){ - return value.toString(); - } else if(typeof value == "string"){ - const dateval = moment(value, moment.ISO_8601).format("YYYY-MMM-DD HH:mm:ss"); - if(dateval !== 'Invalid date'){ - return dateval; - } - } - }catch(err){ - console.error('Error',err) + function updatedCellvalue(key, value) { + try { + if (key === 'blueprint_draft' && _.includes(value, '/task_draft/')) { + // 'task_draft/' -> len = 12 + var taskid = _.replace(value.substring((value.indexOf('/task_draft/') + 12), value.length), '/', ''); + return <a href={'/task/view/draft/' + taskid}>{' ' + taskid + ' '}</a> + } else if (key === 'blueprint_draft') { + var retval = []; + value.forEach((link, index) => { + // 'task_blueprint/' -> len = 16 + if (_.includes(link, '/task_blueprint/')) { + var bpid = _.replace(link.substring((link.indexOf('/task_blueprint/') + 16), link.length), '/', ''); + retval.push(<a href={'/task/view/blueprint/' + bpid} key={bpid + index} >{' ' + bpid + ' '}</a>) + } + }) + return retval; + } else if (typeof value == "boolean") { + return value.toString(); + } else if (typeof value == "string") { + const dateval = moment(value, moment.ISO_8601).format("YYYY-MMM-DD HH:mm:ss"); + if (dateval !== 'Invalid date') { + return dateval; + } } - return value; + } catch (err) { + console.error('Error', err) } - + return value; + } + return ( <div> - <Table columns={columns} data={tbldata} defaultheader={defaultheader[0]} optionalheader={optionalheader[0]} - defaultSortColumn={defaultSortColumn} tablename={tablename} defaultpagesize={defaultpagesize}/> + <Table columns={columns} data={tbldata} defaultheader={defaultheader[0]} optionalheader={optionalheader[0]} showAction={props.showaction} + defaultSortColumn={defaultSortColumn} tablename={tablename} defaultpagesize={defaultpagesize} columnOrders={props.columnOrders} /> </div> ) } -export default ViewTable +export default ViewTable \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/layout/_overrides.scss b/SAS/TMSS/frontend/tmss_webapp/src/layout/_overrides.scss index 32ef8d9e42d3593cb7e77f324b8443fc2032fdfb..ab16a8fe25ea5667cbcd77dd8ba08c208af9057d 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/layout/_overrides.scss +++ b/SAS/TMSS/frontend/tmss_webapp/src/layout/_overrides.scss @@ -200,8 +200,38 @@ .p-growl { z-index: 3000 !important; } +.viewtable .p-hidden-accessible { + position: relative; +} + .data-product { label { display: block; } -} \ No newline at end of file +} + +/** +In Excel View the for Accordion background color override +*/ +.p-accordion .p-accordion-header:not(.p-disabled).p-highlight a { + background-color: lightgray !important; + border: 1px solid gray !important; +} +.p-accordion .p-accordion-header:not(.p-disabled).p-highlight span { + color: black; +} +.p-accordion .p-accordion-header:not(.p-disabled).p-highlight a .p-accordion-toggle-icon { + color: black; +} + +/** + In Custom Dialog - to the message section + */ +.p-dialog-content { + min-height: 7em; + align-items: center; + display: flex !important; +} +.p-grid { + width: -webkit-fill-available; +} diff --git a/SAS/TMSS/frontend/tmss_webapp/src/layout/components/CustomDialog.js b/SAS/TMSS/frontend/tmss_webapp/src/layout/components/CustomDialog.js index ea013dca232d1dd5a0cc4e1dcda11542f79af1ce..db545a15577062ce1458d1ad96ac8f27ebd9acba 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/layout/components/CustomDialog.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/layout/components/CustomDialog.js @@ -19,6 +19,7 @@ export class CustomDialog extends Component { const isConfirm = this.props.type.toLowerCase()==='confirmation'; const isWarning = this.props.type.toLowerCase()==='warning'; const isSuccess = this.props.type.toLowerCase()==='success'; + const showIcon = (typeof this.props.showIcon === "undefined") ? true : this.props.showIcon; // const isError = this.props.type.toLowerCase()==='error'; let iconClass = isConfirm?"pi-question-circle pi-warning":(isWarning?"pi-info-circle pi-warning": (isSuccess?"pi-check-circle pi-success":"pi-times-circle pi-danger")); return ( @@ -30,25 +31,29 @@ export class CustomDialog extends Component { {/* Action buttons based on 'type' props. If 'actions' passed as props, then type is ignored */} {!this.props.actions && <> + <Button key="submit" type="primary" onClick={this.props.onSubmit?this.props.onSubmit:this.props.onClose} label={isConfirm?'Yes':'Ok'} /> {isConfirm && <Button key="back" onClick={this.props.onCancel} label="No" /> } - <Button key="submit" type="primary" onClick={this.props.onSubmit?this.props.onSubmit:this.props.onClose} label={isConfirm?'Yes':'Ok'} /> + </> } {/* Action button based on the 'actions' props */} {this.props.actions && this.props.actions.map((action, index) => { return ( - <Button key={action.id} label={action.title} onClick={action.callback} />); + <Button key={action.id} label={action.title} onClick={action.callback} /> + ); })} </div> } > <div className="p-grid"> - <div className="col-lg-2 col-md-2 col-sm-2"> - <span style={{position: 'absolute', top: '50%', '-ms-transform': 'translateY(-50%)', transform: 'translateY(-50%)'}}> - <i className={`pi pi-large ${iconClass}`}></i> - </span> - </div> + {showIcon && + <div className="col-lg-2 col-md-2 col-sm-2"> + <span style={{position: 'absolute', top: '50%', '-ms-transform': 'translateY(-50%)', transform: 'translateY(-50%)'}}> + <i className={`pi pi-large ${iconClass}`}></i> + </span> + </div> + } <div className="col-lg-10 col-md-10 col-sm-10"> {/* Display message passed */} {this.props.message?this.props.message:""} diff --git a/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/reservation.scss b/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/reservation.scss index 5031872c644480c2ff08332d8092bbaec8e69a1b..b14c70faab0443a625ae568ecb0cea846472802f 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/reservation.scss +++ b/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/reservation.scss @@ -7,6 +7,6 @@ .p-select{ margin-left: 27em; position: relative; - top: 3.5em; + top: 2.2em; width: 40em; } \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/create.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/create.js index 8e11645ca22f57e71bf20c836ac45bf76423c39a..9f9128ecfb678939717cbe9027cee09c226981ae 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/create.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/create.js @@ -1,13 +1,14 @@ import React, {Component} from 'react'; import { Redirect } from 'react-router-dom'; -import {InputText} from 'primereact/inputtext'; -import {Calendar} from 'primereact/calendar'; -import {InputTextarea} from 'primereact/inputtextarea'; -import {Dropdown} from 'primereact/dropdown'; -import {Button} from 'primereact/button'; -import {Dialog} from 'primereact/components/dialog/Dialog'; -import {Growl} from 'primereact/components/growl/Growl'; -import {ResourceInputList} from './ResourceInputList'; +import { InputText } from 'primereact/inputtext'; +import { Calendar } from 'primereact/calendar'; +import { InputTextarea } from 'primereact/inputtextarea'; +import { Dropdown } from 'primereact/dropdown'; +import { Button } from 'primereact/button'; +import { Dialog } from 'primereact/components/dialog/Dialog'; +import { Growl } from 'primereact/components/growl/Growl'; +import { ResourceInputList } from './ResourceInputList'; +import { CustomDialog } from '../../layout/components/CustomDialog'; import moment from 'moment' import _ from 'lodash'; @@ -24,9 +25,13 @@ export class CycleCreate extends Component { constructor(props) { super(props); this.state = { + showDialog: false, + isDirty: false, isLoading: true, dialog: { header: '', detail: ''}, cycle: { + name: '', + description: '', projects: [], quota: [], start: "", @@ -68,6 +73,8 @@ export class CycleCreate extends Component { this.saveCycle = this.saveCycle.bind(this); this.cancelCreate = this.cancelCreate.bind(this); this.reset = this.reset.bind(this); + this.checkIsDirty = this.checkIsDirty.bind(this); + this.close = this.close.bind(this); } componentDidMount() { @@ -111,11 +118,15 @@ export class CycleCreate extends Component { */ addNewResource(){ if (this.state.newResource) { - let resourceList = this.state.resourceList; + let resourceList = _.cloneDeep(this.state.resourceList); const newResource = _.remove(resourceList, {'name': this.state.newResource}); let resources = this.state.resources; resources.push(newResource[0]); - this.setState({resources: resources, resourceList: resourceList, newResource: null}); + if ( !this.state.isDirty && !_.isEqual(this.state.resourceList, resourceList) ) { + this.setState({resources: resources, resourceList: resourceList, newResource: null, isDirty: true}); + } else { + this.setState({resources: resources, resourceList: resourceList, newResource: null}); + } } } @@ -126,12 +137,16 @@ export class CycleCreate extends Component { removeResource(name) { let resources = this.state.resources; let resourceList = this.state.resourceList; - let cycleQuota = this.state.cycleQuota; + let cycleQuota = _.cloneDeep(this.state.cycleQuota); const removedResource = _.remove(resources, (resource) => { return resource.name === name }); resourceList.push(removedResource[0]); resourceList = _.sortBy(resourceList, 'name'); delete cycleQuota[name]; - this.setState({resourceList: resourceList, resources: resources, cycleQuota: cycleQuota}); + if ( !this.state.isDirty && !_.isEqual(this.state.cycleQuota, cycleQuota) ) { + this.setState({resourceList: resourceList, resources: resources, cycleQuota: cycleQuota, isDirty: true}); + } else { + this.setState({resourceList: resourceList, resources: resources, cycleQuota: cycleQuota}); + } } /** @@ -140,7 +155,7 @@ export class CycleCreate extends Component { * @param {any} value */ setCycleParams(key, value, type) { - let cycle = this.state.cycle; + let cycle = _.cloneDeep(this.state.cycle); switch(type) { case 'NUMBER': { cycle[key] = value?parseInt(value):0; @@ -150,9 +165,12 @@ export class CycleCreate extends Component { cycle[key] = value; break; } - } - this.setState({cycle: cycle, validForm: this.validateForm(key)}); + if ( !this.state.isDirty && !_.isEqual(this.state.cycle, cycle) ) { + this.setState({cycle: cycle, validForm: this.validateForm(key), isDirty: true}); + } else { + this.setState({cycle: cycle, validForm: this.validateForm(key)}); + } } /** @@ -161,23 +179,28 @@ export class CycleCreate extends Component { * @param {InputEvent} event */ setCycleQuotaParams(key, event) { - let cycleQuota = this.state.cycleQuota; + let cycleQuota = _.cloneDeep(this.state.cycleQuota); if (event.target.value) { let resource = _.find(this.state.resources, {'name': key}); let newValue = 0; if (this.resourceUnitMap[resource.quantity_value] && event.target.value.toString().indexOf(this.resourceUnitMap[resource.quantity_value].display)>=0) { - newValue = event.target.value.replace(this.resourceUnitMap[resource.quantity_value].display,''); + newValue = _.trim(event.target.value.replace(this.resourceUnitMap[resource.quantity_value].display,'','')); } else { - newValue = event.target.value; + newValue = _.trim(event.target.value); } - cycleQuota[key] = (newValue==="NaN" || isNaN(newValue))?0:newValue; + cycleQuota[key] = (newValue==="NaN" || isNaN(newValue))?0:Number(newValue); } else { let cycleQuota = this.state.cycleQuota; cycleQuota[key] = 0; } - this.setState({cycleQuota: cycleQuota}); + + if ( !this.state.isDirty && !_.isEqual(this.state.cycleQuota, cycleQuota) ) { + this.setState({cycleQuota: cycleQuota, isDirty: true}); + } else { + this.setState({cycleQuota: cycleQuota}); + } } /** @@ -246,7 +269,7 @@ export class CycleCreate extends Component { let stoptime = _.replace(this.state.cycle['stop'],'00:00:00', '23:59:59'); cycle['start'] = moment(cycle['start']).format("YYYY-MM-DDTHH:mm:ss"); cycle['stop'] = moment(stoptime).format("YYYY-MM-DDTHH:mm:ss"); - this.setState({cycle: cycle}); + this.setState({cycle: cycle, isDirty: false}); for (const resource in this.state.cycleQuota) { let resourceType = _.find(this.state.resources, {'name': resource}); if(resourceType){ @@ -276,6 +299,21 @@ export class CycleCreate extends Component { } } + /** + * warn before cancel the page if any changes detected + */ + checkIsDirty() { + if( this.state.isDirty ){ + this.setState({showDialog: true}); + } else { + this.cancelCreate(); + } + } + + close() { + this.setState({showDialog: false}); + } + /** * Function to cancel form creation and navigate to other page/component */ @@ -341,7 +379,7 @@ export class CycleCreate extends Component { <PageHeader location={this.props.location} title={'Cycle - Add'} actions={[{icon:'fa-window-close', title:'Click to Close Add Cycle', - props:{pathname: '/cycle' }}]}/> + type: 'button', actOn: 'click', props:{ callback: this.checkIsDirty }}]}/> { this.state.isLoading ? <AppLoader /> : <> <div> @@ -382,7 +420,7 @@ export class CycleCreate extends Component { <label htmlFor="cycleName" className="col-lg-2 col-md-2 col-sm-12">Start Date <span style={{color:'red'}}>*</span></label> <div className="col-lg-3 col-md-3 col-sm-12"> <Calendar - d dateFormat="dd-M-yy" + d dateFormat="dd-M-yy" value= {this.state.cycle.start} onChange= {e => this.setCycleParams('start',e.value)} onBlur= {e => this.setCycleParams('start',e.value)} @@ -435,8 +473,8 @@ export class CycleCreate extends Component { </div> <div className="p-field p-grid resource-input-grid"> <ResourceInputList list={this.state.resources} unitMap={this.resourceUnitMap} - cycleQuota={this.state.cycleQuota} callback={this.setCycleQuotaParams} - removeInputCallback={this.removeResource} /> + cycleQuota={this.state.cycleQuota} callback={this.setCycleQuotaParams} + removeInputCallback={this.removeResource} /> </div> </div> } @@ -447,7 +485,7 @@ export class CycleCreate extends Component { <Button label="Save" className="p-button-primary" id="save-btn" data-testid="save-btn" icon="pi pi-check" onClick={this.saveCycle} disabled={!this.state.validForm} /> </div> <div className="p-col-1"> - <Button label="Cancel" className="p-button-danger" icon="pi pi-times" onClick={this.cancelCreate} /> + <Button label="Cancel" className="p-button-danger" icon="pi pi-times" onClick={this.checkIsDirty} /> </div> </div> </> @@ -471,6 +509,11 @@ export class CycleCreate extends Component { </div> </div> </Dialog> + + <CustomDialog type="confirmation" visible={this.state.showDialog} width="40vw" + header={'Add Cycle'} message={'Do you want to leave this page? Your changes may not be saved.'} + content={''} onClose={this.close} onCancel={this.close} onSubmit={this.cancelCreate}> + </CustomDialog> </div> </React.Fragment> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/edit.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/edit.js index d04ac553d56581a384a458044384e5b64a349845..ce35f79a059ea97f43acc334c394927107f82a55 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/edit.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/edit.js @@ -3,26 +3,30 @@ import { Redirect } from 'react-router-dom'; import _ from 'lodash'; import moment from 'moment' -import {InputText} from 'primereact/inputtext'; -import {Calendar} from 'primereact/calendar'; -import {InputTextarea} from 'primereact/inputtextarea'; -import {Dropdown} from 'primereact/dropdown'; +import { InputText } from 'primereact/inputtext'; +import { Calendar } from 'primereact/calendar'; +import { InputTextarea } from 'primereact/inputtextarea'; +import { Dropdown } from 'primereact/dropdown'; import { Button } from 'primereact/button'; -import {Dialog} from 'primereact/components/dialog/Dialog'; -import {Growl} from 'primereact/components/growl/Growl'; - -import {ResourceInputList} from './ResourceInputList'; +import { Dialog } from 'primereact/components/dialog/Dialog'; +import { Growl } from 'primereact/components/growl/Growl'; +import { ResourceInputList } from './ResourceInputList'; +import { CustomDialog } from '../../layout/components/CustomDialog'; import AppLoader from '../../layout/components/AppLoader'; import PageHeader from '../../layout/components/PageHeader'; import CycleService from '../../services/cycle.service'; import UnitConverter from '../../utils/unit.converter'; import UIConstants from '../../utils/ui.constants'; + + export class CycleEdit extends Component { constructor(props) { super(props); this.state = { + showDialog: false, + isDirty: false, isLoading: true, dialog: { header: '', detail: ''}, cycle: { @@ -60,6 +64,8 @@ export class CycleEdit extends Component { this.resourceUnitMap = UnitConverter.resourceUnitMap; this.tooltipOptions = UIConstants.tooltipOptions; + this.checkIsDirty = this.checkIsDirty.bind(this); + this.close = this.close.bind(this); this.getCycleDetails = this.getCycleDetails.bind(this); this.cycleOptionTemplate = this.cycleOptionTemplate.bind(this); this.setCycleQuotaDefaults = this.setCycleQuotaDefaults.bind(this); @@ -99,15 +105,18 @@ export class CycleEdit extends Component { let resourceList = this.state.resourceList; let cycleQuota = {}; if (cycle) { - // Get cycle_quota for the cycle and asssign to the component variable - for (const id of cycle.quota_ids) { - let quota = await CycleService.getCycleQuota(id); - let resource = _.find(resourceList, ['name', quota.resource_type_id]); - quota.resource = resource; - this.cycleQuota.push(quota); - const conversionFactor = this.resourceUnitMap[resource.quantity_value]?this.resourceUnitMap[resource.quantity_value].conversionFactor:1; - cycleQuota[quota.resource_type_id] = quota.value / conversionFactor; - }; + if(cycle.quota_ids){ + // Get cycle_quota for the cycle and asssign to the component variable + for (const id of cycle.quota_ids) { + let quota = await CycleService.getCycleQuota(id); + let resource = _.find(resourceList, ['name', quota.resource_type_id]); + quota.resource = resource; + this.cycleQuota.push(quota); + const conversionFactor = this.resourceUnitMap[resource.quantity_value]?this.resourceUnitMap[resource.quantity_value].conversionFactor:1; + cycleQuota[quota.resource_type_id] = quota.value / conversionFactor; + }; + } + // Remove the already assigned resources from the resoureList const resources = _.remove(resourceList, (resource) => { return _.find(this.cycleQuota, {'resource_type_id': resource.name})!=null }); this.setState({cycle: cycle, resourceList: resourceList, resources: resources, @@ -149,11 +158,16 @@ export class CycleEdit extends Component { */ addNewResource(){ if (this.state.newResource) { - let resourceList = this.state.resourceList; + let resourceList = _.cloneDeep(this.state.resourceList); const newResource = _.remove(resourceList, {'name': this.state.newResource}); let resources = this.state.resources?this.state.resources:[]; resources.push(newResource[0]); - this.setState({resources: resources, resourceList: resourceList, newResource: null}); + if ( !this.state.isDirty && !_.isEqual(this.state.resourceList, resourceList)) { + this.setState({resources: resources, resourceList: resourceList, newResource: null, isDirty: true}); + } else { + this.setState({resources: resources, resourceList: resourceList, newResource: null}); + } + } } @@ -164,11 +178,16 @@ export class CycleEdit extends Component { removeResource(name) { let resources = this.state.resources; let resourceList = this.state.resourceList; - let cycleQuota = this.state.cycleQuota; + let cycleQuota = _.cloneDeep(this.state.cycleQuota); const removedResource = _.remove(resources, (resource) => { return resource.name === name }); resourceList.push(removedResource[0]); delete cycleQuota[name]; - this.setState({resourceList: resourceList, resources: resources, cycleQuota: cycleQuota}); + if ( !this.state.isDirty && !_.isEqual(this.state.cycleQuota, cycleQuota)) { + this.setState({resourceList: resourceList, resources: resources, cycleQuota: cycleQuota, isDirty: true}); + } else { + this.setState({resourceList: resourceList, resources: resources, cycleQuota: cycleQuota}); + } + } /** @@ -177,7 +196,7 @@ export class CycleEdit extends Component { * @param {any} value */ setCycleParams(key, value, type) { - let cycle = this.state.cycle; + let cycle = _.cloneDeep(this.state.cycle); switch(type) { case 'NUMBER': { cycle[key] = value?parseInt(value):0; @@ -188,7 +207,12 @@ export class CycleEdit extends Component { break; } } - this.setState({cycle: cycle, validForm: this.validateForm(key)}); + if ( !this.state.isDirty && !_.isEqual(this.state.cycle, cycle)) { + this.setState({cycle: cycle, validForm: this.validateForm(key), isDirty: true}); + } else { + this.setState({cycle: cycle, validForm: this.validateForm(key)}); + } + } /** @@ -197,22 +221,26 @@ export class CycleEdit extends Component { * @param {InputEvent} event */ setCycleQuotaParams(key, event) { - let cycleQuota = this.state.cycleQuota; + let cycleQuota = _.cloneDeep(this.state.cycleQuota); if (event.target.value) { let resource = _.find(this.state.resources, {'name': key}); let newValue = 0; if (this.resourceUnitMap[resource.quantity_value] && event.target.value.toString().indexOf(this.resourceUnitMap[resource.quantity_value].display)>=0) { - newValue = event.target.value.replace(this.resourceUnitMap[resource.quantity_value].display,''); + newValue = _.trim(event.target.value.replace(this.resourceUnitMap[resource.quantity_value].display,'')); } else { - newValue = event.target.value; + newValue = _.trim(event.target.value); } - cycleQuota[key] = (newValue==="NaN" || isNaN(newValue))?0:newValue; + cycleQuota[key] = (newValue==="NaN" || isNaN(newValue))?0:Number(newValue); } else { let cycleQuota = this.state.cycleQuota; cycleQuota[key] = 0; } - this.setState({cycleQuota: cycleQuota}); + if ( !this.state.isDirty && !_.isEqual(this.state.cycleQuota, cycleQuota)) { + this.setState({cycleQuota: cycleQuota, isDirty: true}); + } else { + this.setState({cycleQuota: cycleQuota}); + } } /** @@ -281,7 +309,7 @@ export class CycleEdit extends Component { let stoptime = _.replace(this.state.cycle['stop'],'00:00:00', '23:59:59'); cycle['start'] = moment(this.state.cycle['start']).format("YYYY-MM-DDTHH:mm:ss"); cycle['stop'] = moment(stoptime).format("YYYY-MM-DDTHH:mm:ss"); - this.setState({cycle: cycle}); + this.setState({cycle: cycle, isDirty: false}); CycleService.updateCycle(this.props.match.params.id, this.state.cycle) .then(async (cycle) => { if (cycle && this.state.cycle.updated_at !== cycle.updated_at) { @@ -348,6 +376,20 @@ export class CycleEdit extends Component { this.setState({dialogVisible: true, dialog: dialog}); } + /** + * warn before cancel the page if any changes detected + */ + checkIsDirty() { + if( this.state.isDirty ){ + this.setState({showDialog: true}); + } else { + this.cancelEdit(); + } + } + + close() { + this.setState({showDialog: false}); + } /** * Cancel edit and redirect to Cycle View page */ @@ -374,8 +416,7 @@ export class CycleEdit extends Component { </div> </div> */} <PageHeader location={this.props.location} title={'Cycle - Edit'} actions={[{icon:'fa-window-close', - link: this.props.history.goBack,title:'Click to Close Cycle-Edit', - props:{ pathname: `/cycle/view/${this.state.cycle.name}`}}]}/> + title:'Click to Close Cycle-Edit', type: 'button', actOn: 'click', props:{ callback: this.checkIsDirty }}]}/> { this.state.isLoading ? <AppLoader/> : <> @@ -478,7 +519,7 @@ export class CycleEdit extends Component { <Button label="Save" className="p-button-primary" id="save-btn" data-testid="save-btn" icon="pi pi-check" onClick={this.saveCycle} disabled={!this.state.validForm} /> </div> <div className="p-col-1"> - <Button label="Cancel" className="p-button-danger" icon="pi pi-times" onClick={this.cancelEdit} /> + <Button label="Cancel" className="p-button-danger" icon="pi pi-times" onClick={this.checkIsDirty} /> </div> </div> @@ -502,6 +543,12 @@ export class CycleEdit extends Component { </div> </div> </Dialog> + + <CustomDialog type="confirmation" visible={this.state.showDialog} width="40vw" + header={'Edit Cycle'} message={'Do you want to leave this page? Your changes may not be saved.'} + content={''} onClose={this.close} onCancel={this.close} onSubmit={this.cancelEdit}> + </CustomDialog> + </div> </React.Fragment> ); diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/create.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/create.js index 734294c72903e650f2bca44471ff75a0ff02ed06..c21f5afcd834388b99fe3836cd7d598be970a608 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/create.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/create.js @@ -1,17 +1,17 @@ -import React, {Component} from 'react'; +import React, { Component } from 'react'; import { Redirect } from 'react-router-dom'; import _ from 'lodash'; -import {InputText} from 'primereact/inputtext'; -import {InputNumber} from 'primereact/inputnumber'; -import {InputTextarea} from 'primereact/inputtextarea'; -import {Checkbox} from 'primereact/checkbox'; -import {Dropdown} from 'primereact/dropdown'; -import {MultiSelect} from 'primereact/multiselect'; +import { InputText } from 'primereact/inputtext'; +import { InputNumber } from 'primereact/inputnumber'; +import { InputTextarea } from 'primereact/inputtextarea'; +import { Checkbox } from 'primereact/checkbox'; +import { Dropdown } from 'primereact/dropdown'; +import { MultiSelect } from 'primereact/multiselect'; import { Button } from 'primereact/button'; -import {Dialog} from 'primereact/components/dialog/Dialog'; -import {Growl} from 'primereact/components/growl/Growl'; - -import {ResourceInputList} from './ResourceInputList'; +import { Dialog } from 'primereact/components/dialog/Dialog'; +import { Growl } from 'primereact/components/growl/Growl'; +import { CustomDialog } from '../../layout/components/CustomDialog'; +import { ResourceInputList } from './ResourceInputList'; import AppLoader from '../../layout/components/AppLoader'; import PageHeader from '../../layout/components/PageHeader'; @@ -28,10 +28,15 @@ export class ProjectCreate extends Component { constructor(props) { super(props); this.state = { + showDialog: false, + isDirty: false, ltaStorage: [], isLoading: true, dialog: { header: '', detail: ''}, project: { + archive_subdirectory: '', + name: '', + description: '', trigger_priority: 1000, priority_rank: null, quota: [], // Mandatory Field in the back end, so an empty array is passed @@ -78,6 +83,8 @@ export class ProjectCreate extends Component { this.saveProject = this.saveProject.bind(this); this.cancelCreate = this.cancelCreate.bind(this); this.reset = this.reset.bind(this); + this.checkIsDirty = this.checkIsDirty.bind(this); + this.close = this.close.bind(this); } componentDidMount() { @@ -151,11 +158,15 @@ export class ProjectCreate extends Component { */ addNewResource(){ if (this.state.newResource) { - let resourceList = this.state.resourceList; + let resourceList = _.cloneDeep(this.state.resourceList); const newResource = _.remove(resourceList, {'name': this.state.newResource}); let resources = this.state.resources; resources.push(newResource[0]); - this.setState({resources: resources, resourceList: resourceList, newResource: null}); + if ( !this.state.isDirty && !_.isEqual(this.state.resourceList, resourceList) ) { + this.setState({resources: resources, resourceList: resourceList, newResource: null, isDirty: true}); + } else { + this.setState({resources: resources, resourceList: resourceList, newResource: null}); + } } } @@ -166,12 +177,17 @@ export class ProjectCreate extends Component { removeResource(name) { let resources = this.state.resources; let resourceList = this.state.resourceList; - let projectQuota = this.state.projectQuota; + let projectQuota = _.cloneDeep(this.state.projectQuota); const removedResource = _.remove(resources, (resource) => { return resource.name === name }); resourceList.push(removedResource[0]); resourceList = _.sortBy(resourceList, 'name'); delete projectQuota[name]; - this.setState({resourceList: resourceList, resources: resources, projectQuota: projectQuota}); + if ( !this.state.isDirty && !_.isEqual(this.state.projectQuota, projectQuota) ) { + this.setState({resourceList: resourceList, resources: resources, projectQuota: projectQuota, isDirty: true}); + } else { + this.setState({resourceList: resourceList, resources: resources, projectQuota: projectQuota}); + } + } /** @@ -180,7 +196,7 @@ export class ProjectCreate extends Component { * @param {any} value */ setProjectParams(key, value, type) { - let project = this.state.project; + let project = _.cloneDeep(this.state.project); switch(type) { case 'NUMBER': { console.log("Parsing Number"); @@ -211,7 +227,11 @@ export class ProjectCreate extends Component { if (type==='PROJECT_NAME' & value!=="") { validForm = this.validateForm('archive_subdirectory'); } - this.setState({project: project, validForm: validForm}); + if ( !this.state.isDirty && !_.isEqual(this.state.project, project) ) { + this.setState({project: project, validForm: validForm, isDirty: true}); + } else { + this.setState({project: project, validForm: validForm}); + } } /** @@ -221,22 +241,26 @@ export class ProjectCreate extends Component { */ setProjectQuotaParams(key, event) { let projectQuota = this.state.projectQuota; + const previousValue = projectQuota[key]; if (event.target.value) { let resource = _.find(this.state.resources, {'name': key}); - let newValue = 0; if (this.resourceUnitMap[resource.quantity_value] && event.target.value.toString().indexOf(this.resourceUnitMap[resource.quantity_value].display)>=0) { - newValue = event.target.value.replace(this.resourceUnitMap[resource.quantity_value].display,''); + newValue = _.trim(event.target.value.replace(this.resourceUnitMap[resource.quantity_value].display,'')); } else { - newValue = event.target.value; + newValue = _.trim(event.target.value); } - projectQuota[key] = (newValue==="NaN" || isNaN(newValue))?0:newValue; + projectQuota[key] = (newValue==="NaN" || isNaN(newValue))?0:Number(newValue); } else { - let projectQuota = this.state.projectQuota; + // let projectQuota = this.state.projectQuota; projectQuota[key] = 0; } - this.setState({projectQuota: projectQuota}); + if ( !this.state.isDirty && !_.isEqual(previousValue, projectQuota[key]) ) { + this.setState({projectQuota: projectQuota, isDirty: true}); + } else { + this.setState({projectQuota: projectQuota}); + } } /** @@ -308,15 +332,30 @@ export class ProjectCreate extends Component { } else { dialog = {header: 'Success', detail: 'Project saved successfully with default Resource allocations. Do you want to view and edit them?'}; } - this.setState({project:project, dialogVisible: true, dialog: dialog}) + this.setState({project:project, dialogVisible: true, dialog: dialog, isDirty: false}); } else { this.growl.show({severity: 'error', summary: 'Error Occured', detail: 'Unable to save Project'}); - this.setState({errors: project}); + this.setState({errors: project, isDirty: false}); } }); } } + /** + * warn before cancel the page if any changes detected + */ + checkIsDirty() { + if( this.state.isDirty ){ + this.setState({showDialog: true}); + } else { + this.cancelCreate(); + } + } + + close() { + this.setState({showDialog: false}); + } + /** * Function to cancel form creation and navigate to other page/component */ @@ -374,7 +413,8 @@ export class ProjectCreate extends Component { return ( <React.Fragment> <Growl ref={(el) => this.growl = el} /> - <PageHeader location={this.props.location} title={'Project - Add'} actions={[{icon:'fa-window-close',link:this.props.history.goBack, title:'Click to Close Project', props:{ pathname: '/project'}}]}/> + <PageHeader location={this.props.location} title={'Project - Add'} actions={[{icon:'fa-window-close', title:'Click to Close Project', + type: 'button', actOn: 'click', props:{ callback: this.checkIsDirty }}]}/> { this.state.isLoading ? <AppLoader /> : <> <div> @@ -543,7 +583,7 @@ export class ProjectCreate extends Component { <Button label="Save" className="p-button-primary" id="save-btn" data-testid="save-btn" icon="pi pi-check" onClick={this.saveProject} disabled={!this.state.validForm} /> </div> <div className="col-lg-1 col-md-2 col-sm-6"> - <Button label="Cancel" className="p-button-danger" icon="pi pi-times" onClick={this.cancelCreate} /> + <Button label="Cancel" className="p-button-danger" icon="pi pi-times" onClick={this.checkIsDirty} /> </div> </div> </> @@ -567,6 +607,11 @@ export class ProjectCreate extends Component { </div> </div> </Dialog> + + <CustomDialog type="confirmation" visible={this.state.showDialog} width="40vw" + header={'Add Project'} message={'Do you want to leave this page? Your changes may not be saved.'} + content={''} onClose={this.close} onCancel={this.close} onSubmit={this.cancelCreate}> + </CustomDialog> </div> </React.Fragment> ); diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/edit.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/edit.js index 5e7ae2b315fa1d92f5efbccd0232646bec6074a4..b8bd0f3e2f9833bf6290e035240e88bad4d9695d 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/edit.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/edit.js @@ -1,18 +1,18 @@ -import React, {Component} from 'react'; +import React, { Component } from 'react'; import { Redirect } from 'react-router-dom'; import _ from 'lodash'; -import {InputText} from 'primereact/inputtext'; -import {InputNumber} from 'primereact/inputnumber'; -import {InputTextarea} from 'primereact/inputtextarea'; -import {Checkbox} from 'primereact/checkbox'; -import {Dropdown} from 'primereact/dropdown'; -import {MultiSelect} from 'primereact/multiselect'; +import { InputText } from 'primereact/inputtext'; +import { InputNumber } from 'primereact/inputnumber'; +import { InputTextarea } from 'primereact/inputtextarea'; +import { Checkbox } from 'primereact/checkbox'; +import { Dropdown } from 'primereact/dropdown'; +import { MultiSelect } from 'primereact/multiselect'; import { Button } from 'primereact/button'; -import {Dialog} from 'primereact/components/dialog/Dialog'; -import {Growl} from 'primereact/components/growl/Growl'; - -import {ResourceInputList} from './ResourceInputList'; +import { Dialog } from 'primereact/components/dialog/Dialog'; +import { Growl } from 'primereact/components/growl/Growl'; +import { CustomDialog } from '../../layout/components/CustomDialog'; +import { ResourceInputList } from './ResourceInputList'; import AppLoader from '../../layout/components/AppLoader'; import PageHeader from '../../layout/components/PageHeader'; @@ -25,6 +25,8 @@ export class ProjectEdit extends Component { constructor(props) { super(props); this.state = { + showDialog: false, + isDirty: false, isLoading: true, ltaStorage: [], dialog: { header: '', detail: ''}, @@ -73,6 +75,8 @@ export class ProjectEdit extends Component { this.saveProject = this.saveProject.bind(this); this.saveProjectQuota = this.saveProjectQuota.bind(this); this.cancelEdit = this.cancelEdit.bind(this); + this.checkIsDirty = this.checkIsDirty.bind(this); + this.close = this.close.bind(this); } componentDidMount() { @@ -171,12 +175,16 @@ export class ProjectEdit extends Component { */ addNewResource(){ if (this.state.newResource) { - let resourceList = this.state.resourceList; + let resourceList = _.cloneDeep(this.state.resourceList); const newResource = _.remove(resourceList, {'name': this.state.newResource}); let resources = this.state.resources?this.state.resources:[]; resources.push(newResource[0]); console.log(resources); - this.setState({resources: resources, resourceList: resourceList, newResource: null}); + if ( !this.state.isDirty && !_.isEqual(this.state.resourceList, resourceList) ) { + this.setState({resources: resources, resourceList: resourceList, newResource: null, isDirty: true}); + } else { + this.setState({resources: resources, resourceList: resourceList, newResource: null}); + } } } @@ -187,11 +195,15 @@ export class ProjectEdit extends Component { removeResource(name) { let resources = this.state.resources; let resourceList = this.state.resourceList; - let projectQuota = this.state.projectQuota; + let projectQuota = _.cloneDeep(this.state.projectQuota); const removedResource = _.remove(resources, (resource) => { return resource.name === name }); resourceList.push(removedResource[0]); delete projectQuota[name]; - this.setState({resourceList: resourceList, resources: resources, projectQuota: projectQuota}); + if ( !this.state.isDirty && !_.isEqual(this.state.projectQuota, projectQuota) ) { + this.setState({resourceList: resourceList, resources: resources, projectQuota: projectQuota, isDirty: true}); + } else { + this.setState({resourceList: resourceList, resources: resources, projectQuota: projectQuota}); + } } /** @@ -200,7 +212,7 @@ export class ProjectEdit extends Component { * @param {any} value */ setProjectParams(key, value, type) { - let project = this.state.project; + let project = _.cloneDeep(this.state.project); switch(type) { case 'NUMBER': { console.log("Parsing Number"); @@ -231,7 +243,11 @@ export class ProjectEdit extends Component { if (type==='PROJECT_NAME' & value!=="") { validForm = this.validateForm('archive_subdirectory'); } - this.setState({project: project, validForm: validForm}); + if ( !this.state.isDirty && !_.isEqual(this.state.project, project) ) { + this.setState({project: project, validForm: validForm, isDirty: true}); + } else { + this.setState({project: project, validForm: validForm}); + } } /** @@ -241,22 +257,27 @@ export class ProjectEdit extends Component { */ setProjectQuotaParams(key, event) { let projectQuota = this.state.projectQuota; + const previousValue = projectQuota[key]; if (event.target.value) { let resource = _.find(this.state.resources, {'name': key}); let newValue = 0; if (this.resourceUnitMap[resource.quantity_value] && event.target.value.toString().indexOf(this.resourceUnitMap[resource.quantity_value].display)>=0) { - newValue = event.target.value.replace(this.resourceUnitMap[resource.quantity_value].display,''); + newValue = _.trim(event.target.value.replace(this.resourceUnitMap[resource.quantity_value].display,'')); } else { - newValue = event.target.value; + newValue = _.trim(event.target.value); } - projectQuota[key] = (newValue==="NaN" || isNaN(newValue))?0:newValue; + projectQuota[key] = (newValue==="NaN" || isNaN(newValue))?0:Number(newValue); } else { - let projectQuota = this.state.projectQuota; + // let projectQuota = this.state.projectQuota; projectQuota[key] = 0; } - this.setState({projectQuota: projectQuota}); + if ( !this.state.isDirty && !_.isEqual(previousValue, projectQuota[key]) ) { + this.setState({projectQuota: projectQuota, isDirty: true}); + } else { + this.setState({projectQuota: projectQuota}); + } } /** @@ -375,7 +396,22 @@ export class ProjectEdit extends Component { } else { dialog = {header: 'Error', detail: 'Project updated successfully but resource allocation not updated properly. Try again!'}; } - this.setState({dialogVisible: true, dialog: dialog}); + this.setState({dialogVisible: true, dialog: dialog, isDirty: false}); + } + + /** + * warn before cancel the page if any changes detected + */ + checkIsDirty() { + if( this.state.isDirty ){ + this.setState({showDialog: true}); + } else { + this.cancelEdit(); + } + } + + close() { + this.setState({showDialog: false}); } /** @@ -393,7 +429,8 @@ export class ProjectEdit extends Component { return ( <React.Fragment> <Growl ref={(el) => this.growl = el} /> - <PageHeader location={this.props.location} title={'Project - Edit'} actions={[{icon:'fa-window-close',link: this.props.history.goBack,title:'Click to Close Project Edit Page', props : { pathname: `/project/view/${this.state.project.name}`}}]}/> + <PageHeader location={this.props.location} title={'Project - Edit'} actions={[{icon:'fa-window-close', + title:'Click to Close Project Edit Page', type: 'button', actOn: 'click', props:{ callback: this.checkIsDirty }}]}/> { this.state.isLoading ? <AppLoader/> : <> @@ -559,7 +596,7 @@ export class ProjectEdit extends Component { <Button label="Save" className="p-button-primary" id="save-btn" data-testid="save-btn" icon="pi pi-check" onClick={this.saveProject} disabled={!this.state.validForm} /> </div> <div className="p-col-1"> - <Button label="Cancel" className="p-button-danger" icon="pi pi-times" onClick={this.cancelEdit} /> + <Button label="Cancel" className="p-button-danger" icon="pi pi-times" onClick={this.checkIsDirty} /> </div> </div> @@ -583,6 +620,11 @@ export class ProjectEdit extends Component { </div> </div> </Dialog> + + <CustomDialog type="confirmation" visible={this.state.showDialog} width="40vw" + header={'Edit Project'} message={'Do you want to leave this page? Your changes may not be saved.'} + content={''} onClose={this.close} onCancel={this.close} onSubmit={this.cancelEdit}> + </CustomDialog> </div> </React.Fragment> ); 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 9151a93343a56e6aacadabd5b4a7bd4912a12a73..641ab1a84853bf87650bbc2637f096fff0cc1ea3 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/SchedulingUnitList.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/SchedulingUnitList.js @@ -4,42 +4,72 @@ import moment from 'moment'; import AppLoader from "./../../layout/components/AppLoader"; import ViewTable from './../../components/ViewTable'; import UnitConverter from '../../utils/unit.converter'; - +import _ from 'lodash'; import ScheduleService from '../../services/schedule.service'; +import { Link } from 'react-router-dom'; +import WorkflowService from '../../services/workflow.service'; class SchedulingUnitList extends Component{ - constructor(props){ super(props); this.defaultcolumns = { + status: { + name: "Status", + filter: "select" + }, type:{ name:"Type", filter:"select" }, - name:"Name", - description:"Description", - project:"Project", - created_at:{ - name:"Created At", - filter: "date" - }, - updated_at:{ - name:"Updated At", - filter: "date" - }, requirements_template_id:{ - name: "Template", + name: "Template ID", filter: "select" }, + project:"Project", + name:"Name", start_time:"Start Time", stop_time:"End time", duration:"Duration (HH:mm:ss)", - status:"Status" - }; + + }; if (props.hideProjectColumn) { delete this.defaultcolumns['project']; } + this.STATUS_BEFORE_SCHEDULED = ['defining', 'defined', 'schedulable']; // Statuses before scheduled to get station_group + this.mainStationGroups = {}; this.state = { + columnOrders: [ + "Status", + "Type", + // "Workflow Status", + "workflowStatus", + "id", + "linked_bp_draft", + "Template ID", + "template_description", + "priority", + "Project", + "suSet", + "Name", + "description", + "Start Time", + "End time", + "Duration (HH:mm:ss)", + "station_group", + "task_content", + "target_observation_sap", + "target0angle1", + "target0angle2", + // "Target 1 - Reference Frame", + "target0referenceframe", + "target1angle1", + "target1angle2", + // "Target 2 - Reference Frame", + "target1referenceframe", + "Cancelled", + "created_at", + "updated_at", + ], scheduleunit: [], paths: [{ "View": "/schedulingunit/view", @@ -48,104 +78,274 @@ class SchedulingUnitList extends Component{ defaultcolumns: [this.defaultcolumns], optionalcolumns: [{ actionpath:"actionpath", + // workflowStatus: { + // name: "Workflow Status", + // filter: 'select' + // }, + workflowStatus: 'Workflow Status', + id: "Scheduling Unit ID", + linked_bp_draft:"Linked Blueprint/ Draft ID", + template_description: "Template Description", + priority:"Priority", + suSet:"Scheduling set", + description:"Description", + station_group: 'Stations (CS/RS/IS)', + task_content: 'Tasks content (O/P/I)', + target_observation_sap: "Number of SAPs in the target observation", + do_cancel: { + name: "Cancelled", + filter: "switch", + }, + created_at:"Created_At", + updated_at:"Updated_At" }], columnclassname: [{ + "Scheduling Unit ID":"filter-input-50", "Template":"filter-input-50", + "Project":"filter-input-50", + "Priority":"filter-input-50", "Duration (HH:mm:ss)":"filter-input-75", + "Linked Blueprint/ Draft ID":"filter-input-50", "Type": "filter-input-75", - "Status":"filter-input-100" + "Status":"filter-input-100", + "Workflow Status":"filter-input-100", + "Scheduling unit ID":"filter-input-50", + "Stations (CS/RS/IS)":"filter-input-50", + "Tasks content (O/P/I)":"filter-input-50", + "Number of SAPs in the target observation":"filter-input-50" }], defaultSortColumn: [{id: "Name", desc: false}], } + this.onRowSelection = this.onRowSelection.bind(this); this.reloadData = this.reloadData.bind(this); + this.addTargetColumns = this.addTargetColumns.bind(this); + } + + /** + * Get count of tasks grouped by type (observation, pipeline, ingest) + * @param {Array} tasks - array of task(draft or blueprint) objects + */ + getTaskTypeGroupCounts(tasks = []) { + const observation = tasks.filter(task => task.specifications_template.type_value === 'observation'); + const pipeline = tasks.filter(task => task.specifications_template.type_value === 'pipeline'); + const ingest = tasks.filter(task => task.specifications_template.type_value === 'ingest'); + return `${observation.length}/${pipeline.length}/${ingest.length}`; + } + + /** + * Get all stations of the SUs from the observation task or subtask based on the SU status. + * @param {Object} schedulingUnit + */ + getSUStations(schedulingUnit) { + let stations = []; + let tasks = schedulingUnit.task_blueprints?schedulingUnit.task_blueprints:schedulingUnit.task_drafts; + /* Get all observation tasks */ + const observationTasks = _.filter(tasks, (task) => { return task.specifications_template.type_value.toLowerCase() === "observation"}); + for (const observationTask of observationTasks) { + /** If the status of SU is before scheduled, get all stations from the station_groups from the task specification_docs */ + if ((!schedulingUnit.status || this.STATUS_BEFORE_SCHEDULED.indexOf(schedulingUnit.status.toLowerCase()) >= 0) + && observationTask.specifications_doc.station_groups) { + for (const grpStations of _.map(observationTask.specifications_doc.station_groups, "stations")) { + stations = _.concat(stations, grpStations); + } + } else if (schedulingUnit.status && this.STATUS_BEFORE_SCHEDULED.indexOf(schedulingUnit.status.toLowerCase()) < 0 + && observationTask.subtasks) { + /** If the status of SU is scheduled or after get the stations from the subtask specification tasks */ + for (const subtask of observationTask.subtasks) { + if (subtask.specifications_doc.stations) { + stations = _.concat(stations, subtask.specifications_doc.stations.station_list); + } + } + } + } + return _.uniq(stations); + } + + /** + * Group the SU stations to main groups Core, Remote, International + * @param {Object} stationList + */ + groupSUStations(stationList) { + let suStationGroups = {}; + for (const group in this.mainStationGroups) { + suStationGroups[group] = _.intersection(this.mainStationGroups[group], stationList); + } + return suStationGroups; + } + + getStationGroup(itemSU) { + const item = {}; + const itemStations = this.getSUStations(itemSU); + const itemStationGroups = this.groupSUStations(itemStations); + item.stations = {groups: "", counts: ""}; + item.suName = itemSU.name; + for (const stationgroup of _.keys(itemStationGroups)) { + let groups = item.stations.groups; + let counts = item.stations.counts; + if (groups) { + groups = groups.concat("/"); + counts = counts.concat("/"); + } + // Get station group 1st character and append 'S' to get CS,RS,IS + groups = groups.concat(stationgroup.substring(0,1).concat('S')); + counts = counts.concat(itemStationGroups[stationgroup].length); + item.stations.groups = groups; + item.stations.counts = counts; + } + return item.stations; + } + + /** + * Function to get a component with list of links to a list of ids + * @param {Array} linkedItems - list of ids + * @param {String} type - blueprint or draft + */ + getLinksList = (linkedItems, type) => { + return ( + <> + {linkedItems.length>0 && linkedItems.map((item, index) => ( + <Link style={{paddingRight: '3px'}} to={`/schedulingunit/view/${type}/${item}`}>{item}</Link> + ))} + </> + ); } async getSchedulingUnitList () { //Get SU Draft/Blueprints for the Project ID. This request is coming from view Project page. Otherwise it will show all SU - let project = this.props.project; - if(project) { - let scheduleunits = await ScheduleService.getSchedulingListByProject(project); - if(scheduleunits){ - this.setState({ - scheduleunit: scheduleunits, isLoading: false - }); - } - } else{ + // let project = this.props.project; + // if(project) { + // let scheduleunits = await ScheduleService.getSchedulingListByProject(project); + // if(scheduleunits){ + // this.setState({ + // scheduleunit: scheduleunits, isLoading: false + // }); + // } + // } else { + const suTemplate = {}; const schedulingSet = await ScheduleService.getSchedulingSets(); const projects = await ScheduleService.getProjectList(); const promises = [ScheduleService.getSchedulingUnitsExtended('blueprint'), - ScheduleService.getSchedulingUnitsExtended('draft')]; - Promise.all(promises).then(responses => { + ScheduleService.getSchedulingUnitsExtended('draft'), + ScheduleService.getMainGroupStations(), + WorkflowService.getWorkflowProcesses()]; + Promise.all(promises).then(async responses => { const blueprints = responses[0]; let scheduleunits = responses[1]; + this.mainStationGroups = responses[2]; + let workflowProcesses = responses[3]; const output = []; for( const scheduleunit of scheduleunits){ const suSet = schedulingSet.find((suSet) => { return scheduleunit.scheduling_set_id === suSet.id }); const project = projects.find((project) => { return suSet.project_id === project.name}); - const blueprintdata = blueprints.filter(i => i.draft_id === scheduleunit.id); - blueprintdata.map(blueP => { - blueP.duration = moment.utc((blueP.duration || 0)*1000).format('HH:mm:ss'); - blueP.type="Blueprint"; - blueP['actionpath'] ='/schedulingunit/view/blueprint/'+blueP.id; - blueP['created_at'] = moment(blueP['created_at'], moment.ISO_8601).format("YYYY-MMM-DD HH:mm:ss"); - blueP['updated_at'] = moment(blueP['updated_at'], moment.ISO_8601).format("YYYY-MMM-DD HH:mm:ss"); - blueP.project = project.name; - blueP.canSelect = false; - // blueP.links = ['Project']; - // blueP.linksURL = { - // 'Project': `/project/view/${project.name}` - // } - return blueP; - }); - output.push(...blueprintdata); - scheduleunit['actionpath']='/schedulingunit/view/draft/'+scheduleunit.id; - scheduleunit['type'] = 'Draft'; - scheduleunit['duration'] = moment.utc((scheduleunit.duration || 0)*1000).format('HH:mm:ss'); - scheduleunit['created_at'] = moment(scheduleunit['created_at'], moment.ISO_8601).format("YYYY-MMM-DD HH:mm:ss"); - scheduleunit['updated_at'] = moment(scheduleunit['updated_at'], moment.ISO_8601).format("YYYY-MMM-DD HH:mm:ss"); - scheduleunit.project = project.name; - scheduleunit.canSelect = true; - // scheduleunit.links = ['Project']; - // scheduleunit.linksURL = { - // 'Project': `/project/view/${project.name}` - // } - output.push(scheduleunit); + if (!this.props.project || (this.props.project && project.name===this.props.project)) { + scheduleunit['status'] = null; + scheduleunit['workflowStatus'] = null; + if (!suTemplate[scheduleunit.requirements_template_id]) { + const response = await ScheduleService.getSchedulingUnitTemplate(scheduleunit.requirements_template_id); + scheduleunit['template_description'] = response.description; + suTemplate[scheduleunit.requirements_template_id] = response; + } else { + scheduleunit['template_description'] = suTemplate[scheduleunit.requirements_template_id].description; + } + scheduleunit['linked_bp_draft'] = this.getLinksList(scheduleunit.scheduling_unit_blueprints_ids, 'blueprint'); + scheduleunit['task_content'] = this.getTaskTypeGroupCounts(scheduleunit['task_drafts']); + scheduleunit['station_group'] = this.getStationGroup(scheduleunit).counts; + const blueprintdata = blueprints.filter(i => i.draft_id === scheduleunit.id); + blueprintdata.map(blueP => { + const workflowProcess = _.find(workflowProcesses, ['su', blueP.id]); + blueP['workflowStatus'] = workflowProcess?workflowProcess.status: null; + blueP.duration = moment.utc((blueP.duration || 0)*1000).format('HH:mm:ss'); + blueP.type="Blueprint"; + blueP['actionpath'] ='/schedulingunit/view/blueprint/'+blueP.id; + blueP['created_at'] = moment(blueP['created_at'], moment.ISO_8601).format("YYYY-MMM-DD HH:mm:ss"); + blueP['updated_at'] = moment(blueP['updated_at'], moment.ISO_8601).format("YYYY-MMM-DD HH:mm:ss"); + blueP['task_content'] = this.getTaskTypeGroupCounts(blueP['task_blueprints']); + blueP['linked_bp_draft'] = this.getLinksList([blueP.draft_id], 'draft'); + blueP['template_description'] = suTemplate[blueP.requirements_template_id].description; + blueP['station_group'] = this.getStationGroup(blueP).counts; + blueP.project = project.name; + blueP.canSelect = false; + blueP.suSet = suSet.name; + // blueP.links = ['Project']; + // blueP.linksURL = { + // 'Project': `/project/view/${project.name}` + // } + blueP.links = ['Project', 'id']; + blueP.linksURL = { + 'Project': `/project/view/${project.name}`, + 'id': `/schedulingunit/view/blueprint/${blueP.id}` + } + return blueP; + }); + output.push(...blueprintdata); + scheduleunit['actionpath']='/schedulingunit/view/draft/'+scheduleunit.id; + scheduleunit['type'] = 'Draft'; + scheduleunit['duration'] = moment.utc((scheduleunit.duration || 0)*1000).format('HH:mm:ss'); + scheduleunit['created_at'] = moment(scheduleunit['created_at'], moment.ISO_8601).format("YYYY-MMM-DD HH:mm:ss"); + scheduleunit['updated_at'] = moment(scheduleunit['updated_at'], moment.ISO_8601).format("YYYY-MMM-DD HH:mm:ss"); + scheduleunit.project = project.name; + scheduleunit.canSelect = true; + scheduleunit.suSet = suSet.name; + scheduleunit.links = ['Project', 'id']; + scheduleunit.linksURL = { + 'Project': `/project/view/${project.name}`, + 'id': `/schedulingunit/view/draft/${scheduleunit.id}` + } + output.push(scheduleunit); + } } - const defaultColumns = this.defaultcolumns; - let columnclassname = this.state.columnclassname[0]; - output.map(su => { - su.taskDetails = su.type==="Draft"?su.task_drafts:su.task_blueprints; - const targetObserv = su.taskDetails.find(task => task.specifications_template.type_value==='observation' && task.specifications_doc.SAPs); - // Constructing targets in single string to make it clear display - targetObserv.specifications_doc.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; - defaultColumns[`target${index}angle1`] = `Target ${index + 1} - Angle 1`; - defaultColumns[`target${index}angle2`] = `Target ${index + 1} - Angle 2`; - defaultColumns[`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; - }); - this.setState({ - scheduleunit: output, isLoading: false, defaultColumns: defaultColumns, - columnclassname: [columnclassname] - }); + this.addTargetColumns(output); this.selectedRows = []; }); - } + // } } + addTargetColumns(schedulingUnits) { + let optionalColumns = this.state.optionalcolumns[0]; + let columnclassname = this.state.columnclassname[0]; + schedulingUnits.map(su => { + su.taskDetails = su.type==="Draft"?su.task_drafts:su.task_blueprints; + const targetObserv = su.taskDetails.find(task => task.specifications_template.type_value==='observation' && task.specifications_doc.SAPs); + // 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) { + if (targetObserv) { + su['target_observation_sap'] = targetObserv.specifications_doc.SAPs.length; + targetObserv.specifications_doc.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; + optionalColumns[`target${index}angle1`] = `Target ${index + 1} - Angle 1`; + optionalColumns[`target${index}angle2`] = `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`] = `Target ${index + 1} - Reference Frame`; + 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; + }); + } else { + su['target_observation_sap'] = 0; + } + return su; + }); + this.setState({ + scheduleunit: schedulingUnits, isLoading: false, optionalColumns: [optionalColumns], + columnclassname: [columnclassname] + }); + } + componentDidMount(){ this.getSchedulingUnitList(); - } /** @@ -188,6 +388,7 @@ class SchedulingUnitList extends Component{ defaultcolumns={this.state.defaultcolumns} optionalcolumns={this.state.optionalcolumns} columnclassname={this.state.columnclassname} + columnOrders={this.state.columnOrders} defaultSortColumn={this.state.defaultSortColumn} showaction="true" keyaccessor="id" diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/Stations.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/Stations.js index d2c7e01c63d7f72528eb66d0791b5f0a51a9f2ca..b773b1de101889e2ffe5f735074a1d8493f6020f 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/Stations.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/Stations.js @@ -94,7 +94,7 @@ export default (props) => { ...stationState, [StationName]: { stations: response.stations, - missing_StationFields: missing_StationFields ? missing_StationFields.max_nr_missing : '' + missing_StationFields: missing_StationFields ? isNaN(missing_StationFields.max_nr_missing)? 0: missing_StationFields.max_nr_missing : '' }, Custom: { stations: [...stationState['Custom'].stations, ...response.stations], @@ -163,7 +163,7 @@ export default (props) => { */ const setNoOfmissing_StationFields = (key, value) => { let cpmissing_StationFieldsErrors = [...missing_StationFieldsErrors]; - if (value > state[key].stations.length || value === '') { + if (isNaN(value) || value > state[key].stations.length || value.trim() === '') { if (!cpmissing_StationFieldsErrors.includes(key)) { cpmissing_StationFieldsErrors.push(key); } @@ -176,7 +176,7 @@ export default (props) => { [key]: { ...state[key], missing_StationFields: value, - error: value > state[key].stations.length || value === '' + error: isNaN(value) || value > state[key].stations.length || value.trim() === '' }, }; setState(stationState); @@ -192,7 +192,7 @@ export default (props) => { */ const setMissingFieldsForCustom = (value, index) => { const custom_selected_options = [...customStations]; - if (value > custom_selected_options[index].stations.length || value === '' || !custom_selected_options[index].stations.length) { + if (isNaN(value) || value > custom_selected_options[index].stations.length || value.trim() === '' || !custom_selected_options[index].stations.length) { custom_selected_options[index].error = true; } else { custom_selected_options[index].error = false; 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 89217c6cbba5f19854aeed647f34ebd380819f21..8638bb966b71cb53d88bf88d3e84e2bfb5ea5b1e 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/ViewSchedulingUnit.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/ViewSchedulingUnit.js @@ -18,6 +18,7 @@ import { CustomDialog } from '../../layout/components/CustomDialog'; import { CustomPageSpinner } from '../../components/CustomPageSpinner'; import { Growl } from 'primereact/components/growl/Growl'; import Schedulingtaskrelation from './Scheduling.task.relation'; +import UnitConverter from '../../utils/unit.converter'; import TaskService from '../../services/task.service'; @@ -35,8 +36,36 @@ class ViewSchedulingUnit extends Component{ }], ingestGroup: {}, missingStationFieldsErrors: [], + columnOrders: [ + "Status Logs", + "Status", + "Type", + "ID", + "Control ID", + "Name", + "Description", + "Start Time", + "End Time", + "Duration (HH:mm:ss)", + "Relative Start Time (HH:mm:ss)", + "Relative End Time (HH:mm:ss)", + "#Dataproducts", + "size", + "dataSizeOnDisk", + "subtaskContent", + "tags", + "blueprint_draft", + "url", + "Cancelled", + "Created at", + "Updated at" + ], defaultcolumns: [ { status_logs: "Status Logs", + status:{ + name:"Status", + filter: "select" + }, tasktype:{ name:"Type", filter:"select" @@ -45,29 +74,38 @@ class ViewSchedulingUnit extends Component{ subTaskID: 'Control ID', name:"Name", description:"Description", - created_at:{ - name: "Created at", + start_time:{ + name:"Start Time", filter: "date" }, - updated_at:{ - name: "Updated at", + stop_time:{ + name:"End Time", filter: "date" }, + duration:"Duration (HH:mm:ss)", + relative_start_time:"Relative Start Time (HH:mm:ss)", + relative_stop_time:"Relative End Time (HH:mm:ss)", + noOfOutputProducts: "#Dataproducts", do_cancel:{ name: "Cancelled", filter: "switch" }, - start_time:"Start Time", - stop_time:"End Time", - duration:"Duration (HH:mm:ss)", - status:"Status" }], optionalcolumns: [{ - relative_start_time:"Relative Start Time (HH:mm:ss)", - relative_stop_time:"Relative End Time (HH:mm:ss)", + size: "Data size", + dataSizeOnDisk: "Data size on Disk", + subtaskContent: "Subtask Content", tags:"Tags", blueprint_draft:"BluePrint / Task Draft link", - url:"URL", + url:"API URL", + created_at:{ + name: "Created at", + filter: "date" + }, + updated_at:{ + name: "Updated at", + filter: "date" + }, actionpath:"actionpath" }], columnclassname: [{ @@ -78,10 +116,15 @@ class ViewSchedulingUnit extends Component{ "Cancelled":"filter-input-50", "Duration (HH:mm:ss)":"filter-input-75", "Template ID":"filter-input-50", - "BluePrint / Task Draft link": "filter-input-100", + // "BluePrint / Task Draft link": "filter-input-100", "Relative Start Time (HH:mm:ss)": "filter-input-75", "Relative End Time (HH:mm:ss)": "filter-input-75", - "Status":"filter-input-100" + "Status":"filter-input-100", + "#Dataproducts":"filter-input-75", + "Data size":"filter-input-50", + "Data size on Disk":"filter-input-50", + "Subtask Content":"filter-input-75", + "BluePrint / Task Draft link":"filter-input-50", }], stationGroup: [], dialog: {header: 'Confirm', detail: 'Do you want to create a Scheduling Unit Blueprint?'}, @@ -128,7 +171,7 @@ class ViewSchedulingUnit extends Component{ </button> ); }; - + getSchedulingUnitDetails(schedule_type, schedule_id) { ScheduleService.getSchedulingUnitExtended(schedule_type, schedule_id) .then(async(schedulingUnit) =>{ @@ -140,13 +183,28 @@ class ViewSchedulingUnit extends Component{ let tasks = schedulingUnit.task_drafts?(await this.getFormattedTaskDrafts(schedulingUnit)):this.getFormattedTaskBlueprints(schedulingUnit); let ingestGroup = tasks.map(task => ({name: task.name, canIngest: task.canIngest, type_value: task.type_value, id: task.id })); ingestGroup = _.groupBy(_.filter(ingestGroup, 'type_value'), 'type_value'); - tasks.map(task => { + await Promise.all(tasks.map(async task => { task.status_logs = task.tasktype === "Blueprint"?this.subtaskComponent(task):""; //Displaying SubTask ID of the 'control' Task - const subTaskIds = task.subTasks?task.subTasks.filter(sTask => sTask.subTaskTemplate.name.indexOf('control') > 1):[]; + const subTaskIds = task.subTasks?task.subTasks.filter(sTask => sTask.subTaskTemplate.name.indexOf('control') >= 0):[]; + const promise = []; + subTaskIds.map(subTask => promise.push(ScheduleService.getSubtaskOutputDataproduct(subTask.id))); + const dataProducts = promise.length > 0? await Promise.all(promise):[]; + task.dataProducts = []; + task.size = 0; + task.dataSizeOnDisk = 0; + task.noOfOutputProducts = 0; + if (dataProducts.length && dataProducts[0].length) { + task.dataProducts = dataProducts[0]; + task.noOfOutputProducts = dataProducts[0].length; + task.size = _.sumBy(dataProducts[0], 'size'); + task.dataSizeOnDisk = _.sumBy(dataProducts[0], function(product) { return product.deletedSince?0:product.size}); + task.size = UnitConverter.getUIResourceUnit('bytes', (task.size)); + task.dataSizeOnDisk = UnitConverter.getUIResourceUnit('bytes', (task.dataSizeOnDisk)); + } task.subTaskID = subTaskIds.length ? subTaskIds[0].id : ''; return task; - }); + })); const targetObservation = _.find(tasks, (task)=> {return task.template.type_value==='observation' && task.tasktype.toLowerCase()===schedule_type && task.specifications_doc.station_groups}); this.setState({ scheduleunitId: schedule_id, @@ -243,10 +301,12 @@ class ViewSchedulingUnit extends Component{ } //Ingest Task Relation const ingestTask = scheduletasklist.find(task => task.type_value === 'ingest' && task.tasktype.toLowerCase() === 'draft'); - for (const producer_id of ingestTask.produced_by_ids) { - const taskRelation = await ScheduleService.getTaskRelation(producer_id); - const producerTask = scheduletasklist.find(task => task.id === taskRelation.producer_id && task.tasktype.toLowerCase() === 'draft'); - producerTask.canIngest = true; + if (ingestTask) { + for (const producer_id of ingestTask.produced_by_ids) { + const taskRelation = await ScheduleService.getTaskRelation(producer_id); + const producerTask = scheduletasklist.find(task => task.id === taskRelation.producer_id && task.tasktype.toLowerCase() === 'draft'); + producerTask.canIngest = true; + } } return scheduletasklist; } @@ -419,6 +479,7 @@ class ViewSchedulingUnit extends Component{ defaultcolumns={this.state.defaultcolumns} optionalcolumns={this.state.optionalcolumns} columnclassname={this.state.columnclassname} + columnOrders={this.state.columnOrders} defaultSortColumn={this.state.defaultSortColumn} showaction="true" keyaccessor="id" 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 082a6912336dcc03b2fe3c78f89f69b659e7a598..cf5baa7e9a48890f47a9cbda3e4feced52f9af6f 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/create.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/create.js @@ -1,15 +1,14 @@ -import React, {Component} from 'react'; +import React, { Component } from 'react'; import { Redirect } from 'react-router-dom'; import _ from 'lodash'; import $RefParser from "@apidevtools/json-schema-ref-parser"; import moment from 'moment'; -import {InputText} from 'primereact/inputtext'; -import {InputTextarea} from 'primereact/inputtextarea'; -import {Dropdown} from 'primereact/dropdown'; +import { InputText } from 'primereact/inputtext'; +import { InputTextarea } from 'primereact/inputtextarea'; +import { Dropdown } from 'primereact/dropdown'; import { Button } from 'primereact/button'; -import {Dialog} from 'primereact/components/dialog/Dialog'; -import {Growl} from 'primereact/components/growl/Growl'; - +import { Dialog } from 'primereact/components/dialog/Dialog'; +import { Growl } from 'primereact/components/growl/Growl'; import AppLoader from '../../layout/components/AppLoader'; import Jeditor from '../../components/JSONEditor/JEditor'; import UnitConversion from '../../utils/unit.converter'; @@ -21,6 +20,8 @@ import UIConstants from '../../utils/ui.constants'; import PageHeader from '../../layout/components/PageHeader'; import SchedulingConstraint from './Scheduling.Constraints'; import Stations from './Stations'; +import { CustomDialog } from '../../layout/components/CustomDialog'; +import SchedulingSet from './schedulingset.create'; /** * Component to create a new SchedulingUnit from Observation strategy template @@ -29,6 +30,10 @@ export class SchedulingUnitCreate extends Component { constructor(props) { super(props); this.state = { + selectedProject: {}, + showAddSet: false, + showDialog: false, + isDirty: false, isLoading: true, // Flag for loading spinner dialog: { header: '', detail: ''}, // Dialog properties touched: {}, @@ -39,7 +44,9 @@ export class SchedulingUnitCreate extends Component { stationOptions: [], stationGroup: [], customSelectedStations: [], // custom stations - schedulingUnit: { + schedulingUnit: { + name: '', + description: '', project: (props.match?props.match.params.project:null) || null, }, projectDisabled: (props.match?(props.match.params.project? true:false):false), // Disable project selection if @@ -76,6 +83,9 @@ export class SchedulingUnitCreate extends Component { this.saveSchedulingUnit = this.saveSchedulingUnit.bind(this); this.cancelCreate = this.cancelCreate.bind(this); this.reset = this.reset.bind(this); + this.refreshSchedulingSet = this.refreshSchedulingSet.bind(this); + this.checkIsDirty = this.checkIsDirty.bind(this); + this.close = this.close.bind(this); } componentDidMount() { @@ -95,8 +105,9 @@ export class SchedulingUnitCreate extends Component { // Setting first value as constraint template this.constraintStrategy(this.constraintTemplates[0]); if (this.state.schedulingUnit.project) { + const selectedProject = _.filter(this.projects, {'name': this.state.schedulingUnit.project}); const projectSchedSets = _.filter(this.schedulingSets, {'project_id': this.state.schedulingUnit.project}); - this.setState({isLoading: false, schedulingSets: projectSchedSets}); + this.setState({isLoading: false, schedulingSets: projectSchedSets,selectedProject:selectedProject}); } else { this.setState({isLoading: false}); } @@ -111,7 +122,8 @@ export class SchedulingUnitCreate extends Component { const projectSchedSets = _.filter(this.schedulingSets, {'project_id': projectName}); let schedulingUnit = this.state.schedulingUnit; schedulingUnit.project = projectName; - this.setState({schedulingUnit: schedulingUnit, schedulingSets: projectSchedSets, validForm: this.validateForm('project')}); + const selectedProject = _.filter(this.projects, {'name': projectName}); + this.setState({selectedProject: selectedProject, schedulingUnit: schedulingUnit, schedulingSets: projectSchedSets, validForm: this.validateForm('project'), isDirty: true}); } /** @@ -176,7 +188,7 @@ export class SchedulingUnitCreate extends Component { } } - this.setState({observStrategy: observStrategy, paramsSchema: schema, paramsOutput: paramsOutput, stationGroup: station_group}); + this.setState({observStrategy: observStrategy, paramsSchema: schema, paramsOutput: paramsOutput, stationGroup: station_group, isDirty: true}); // Function called to clear the JSON Editor fields and reload with new schema if (this.state.editorFunction) { @@ -203,12 +215,16 @@ export class SchedulingUnitCreate extends Component { if (jsonOutput.scheduler === 'online' || jsonOutput.scheduler === 'dynamic') { err = err.filter(e => e.path !== 'root.time.at'); } - this.constraintParamsOutput = jsonOutput; + // this.constraintParamsOutput = jsonOutput; // condition goes here.. this.constraintValidEditor = err.length === 0; - this.setState({ constraintParamsOutput: jsonOutput, - constraintValidEditor: err.length === 0, - validForm: this.validateForm()}); + 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}); + } else { + this.setState({ constraintParamsOutput: jsonOutput, constraintValidEditor: err.length === 0, validForm: this.validateForm()}); + } + + } /** @@ -230,9 +246,13 @@ export class SchedulingUnitCreate extends Component { [key]: true } }); - let schedulingUnit = this.state.schedulingUnit; + let schedulingUnit = _.cloneDeep(this.state.schedulingUnit); schedulingUnit[key] = value; - this.setState({schedulingUnit: schedulingUnit, validForm: this.validateForm(key), validEditor: this.validateEditor()}); + if ( !this.state.isDirty && !_.isEqual(this.state.schedulingUnit, schedulingUnit) ) { + this.setState({schedulingUnit: schedulingUnit, validForm: this.validateForm(key), validEditor: this.validateEditor(), isDirty: true}); + } else { + this.setState({schedulingUnit: schedulingUnit, validForm: this.validateForm(key), validEditor: this.validateEditor()}); + } this.validateEditor(); } @@ -361,12 +381,27 @@ export class SchedulingUnitCreate extends Component { if (!schedulingUnit.error) { // this.growl.show({severity: 'success', summary: 'Success', detail: 'Scheduling Unit and tasks created successfully!'}); const dialog = {header: 'Success', detail: 'Scheduling Unit and Tasks are created successfully. Do you want to create another Scheduling Unit?'}; - this.setState({schedulingUnit: schedulingUnit, dialogVisible: true, dialog: dialog}) + this.setState({schedulingUnit: schedulingUnit, dialogVisible: true, dialog: dialog, isDirty: false}); } else { this.growl.show({severity: 'error', summary: 'Error Occured', detail: schedulingUnit.message || 'Unable to save Scheduling Unit/Tasks'}); } } + /** + * warn before cancel the page if any changes detected + */ + checkIsDirty() { + if( this.state.isDirty ){ + this.setState({showDialog: true}); + } else { + this.cancelCreate(); + } + } + + close() { + this.setState({showDialog: false}); + } + /** * Cancel SU creation and redirect */ @@ -388,6 +423,7 @@ export class SchedulingUnitCreate extends Component { this.nameInput.element.focus(); this.setState({ dialogVisible: false, + isDirty: false, dialog: { header: '', detail: ''}, errors: [], schedulingSets: this.props.match.params.project?schedulingSets:[], @@ -415,25 +451,39 @@ export class SchedulingUnitCreate extends Component { } onUpdateStations = (state, selectedStations, missing_StationFieldsErrors, customSelectedStations) => { - this.setState({ - ...state, - selectedStations, - missing_StationFieldsErrors, - customSelectedStations - - }, () => { - this.setState({ - validForm: this.validateForm() + const selectedStation = this.state.selectedStations; + const customStation = this.state.customSelectedStations; + if ( !this.state.isDirty ) { + if (selectedStation && !_.isEqual(selectedStation, selectedStations)){ + this.setState({...state, selectedStations, missing_StationFieldsErrors, customSelectedStations }, () => { + this.setState({ validForm: this.validateForm(), isDirty: true }); + }); + } else if (customStation && !_.isEqual(customStation, customSelectedStations)){ + this.setState({...state, selectedStations, missing_StationFieldsErrors, customSelectedStations }, () => { + this.setState({ validForm: this.validateForm(), isDirty: true }); + }); + } else { + this.setState({...state, selectedStations, missing_StationFieldsErrors, customSelectedStations }, () => { + this.setState({ validForm: this.validateForm() }); + }); + } + } else { + this.setState({...state, selectedStations, missing_StationFieldsErrors, customSelectedStations }, () => { + this.setState({ validForm: this.validateForm() }); }); - - }); + } }; + async refreshSchedulingSet(){ + this.schedulingSets = await ScheduleService.getSchedulingSets(); + const filteredSchedluingSets = _.filter(this.schedulingSets, {'project_id': this.state.schedulingUnit.project}); + this.setState({saveDialogVisible: false, showAddSet: false, schedulingSets: filteredSchedluingSets}); + } + render() { if (this.state.redirect) { return <Redirect to={ {pathname: this.state.redirect} }></Redirect> } - const schema = this.state.paramsSchema; let jeditor = null; @@ -450,7 +500,8 @@ export class SchedulingUnitCreate extends Component { <React.Fragment> <Growl ref={(el) => this.growl = el} /> <PageHeader location={this.props.location} title={'Scheduling Unit - Add'} - actions={[{icon: 'fa-window-close',link: this.props.history.goBack,title:'Click to close Scheduling Unit creation', props : { pathname: `/schedulingunit`}}]}/> + actions={[{icon: 'fa-window-close', title:'Click to close Scheduling Unit creation', + type: 'button', actOn: 'click', props:{ callback: this.checkIsDirty }}]}/> { this.state.isLoading ? <AppLoader /> : <> <div> @@ -503,6 +554,13 @@ export class SchedulingUnitCreate extends Component { options={this.state.schedulingSets} onChange={(e) => {this.setSchedUnitParams('scheduling_set_id',e.value)}} placeholder="Select Scheduling Set" /> + + <Button label="" className="p-button-primary" icon="pi pi-plus" + onClick={() => {this.setState({showAddSet: true})}} + tooltip="Add new Scheduling Set" + style={{bottom: '2em', left: '25em'}} + disabled={this.state.schedulingUnit.project !== null ? false : true }/> + <label className={(this.state.errors.scheduling_set_id && this.state.touched.scheduling_set_id) ?"error":"info"}> {(this.state.errors.scheduling_set_id && this.state.touched.scheduling_set_id) ? this.state.errors.scheduling_set_id : "Scheduling Set of the Project"} </label> @@ -559,7 +617,7 @@ export class SchedulingUnitCreate extends Component { 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.cancelCreate} /> + <Button label="Cancel" className="p-button-danger" icon="pi pi-times" onClick={this.checkIsDirty} /> </div> </div> </div> @@ -585,6 +643,16 @@ export class SchedulingUnitCreate extends Component { </div> </div> </Dialog> + + <CustomDialog type="success" visible={this.state.showAddSet} width="40vw" + header={'Add Scheduling Set’'} message= {<SchedulingSet project={this.state.selectedProject[0]} onCancel={this.refreshSchedulingSet} />} showIcon={false} actions={this.actions} + content={''} onClose={this.refreshSchedulingSet} onCancel={this.refreshSchedulingSet} onSubmit={this.refreshSchedulingSet} + showAction={true}> + </CustomDialog> + <CustomDialog type="confirmation" visible={this.state.showDialog} width="40vw" + header={'Add Scheduling Unit'} message={'Do you want to leave this page? Your changes may not be saved.'} + content={''} onClose={this.close} onCancel={this.close} onSubmit={this.cancelCreate}> + </CustomDialog> </div> </React.Fragment> ); 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 0c92c2c764bdfad792e9beaefe93facfe3d93d89..31ebfe2212d988a6dff2dcf697f210659fb649de 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/edit.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/edit.js @@ -1,15 +1,15 @@ -import React, {Component} from 'react'; +import React, { Component } from 'react'; import { Redirect } from 'react-router-dom'; import moment from 'moment'; import _ from 'lodash'; import $RefParser from "@apidevtools/json-schema-ref-parser"; -import {InputText} from 'primereact/inputtext'; -import {InputTextarea} from 'primereact/inputtextarea'; -import {Dropdown} from 'primereact/dropdown'; +import { InputText } from 'primereact/inputtext'; +import { InputTextarea } from 'primereact/inputtextarea'; +import { Dropdown } from 'primereact/dropdown'; import { Button } from 'primereact/button'; -import {Growl} from 'primereact/components/growl/Growl'; - +import { Growl } from 'primereact/components/growl/Growl'; +import { CustomDialog } from '../../layout/components/CustomDialog'; import AppLoader from '../../layout/components/AppLoader'; import PageHeader from '../../layout/components/PageHeader'; import Jeditor from '../../components/JSONEditor/JEditor'; @@ -29,6 +29,8 @@ export class EditSchedulingUnit extends Component { constructor(props) { super(props); this.state = { + showDialog: false, + isDirty: false, isLoading: true, //Flag for loading spinner dialog: { header: '', detail: ''}, //Dialog properties redirect: null, //URL to redirect @@ -71,6 +73,8 @@ export class EditSchedulingUnit extends Component { this.saveSchedulingUnit = this.saveSchedulingUnit.bind(this); this.cancelCreate = this.cancelCreate.bind(this); this.setEditorOutputConstraint = this.setEditorOutputConstraint.bind(this); + this.checkIsDirty = this.checkIsDirty.bind(this); + this.close = this.close.bind(this); } /** @@ -189,8 +193,18 @@ export class EditSchedulingUnit extends Component { this.paramsOutput = jsonOutput; this.validEditor = errors.length === 0; this.setState({ paramsOutput: jsonOutput, - validEditor: errors.length === 0, - validForm: this.validateForm()}); + validEditor: errors.length === 0, + validForm: this.validateForm()}); + /*if ( !this.state.isDirty && this.state.paramsOutput && !_.isEqual(this.state.paramsOutput, jsonOutput) ) { + this.setState({ paramsOutput: jsonOutput, + validEditor: errors.length === 0, + validForm: this.validateForm()}); + } else { + this.setState({ paramsOutput: jsonOutput, + validEditor: errors.length === 0, + validForm: this.validateForm()}); + }*/ + } setEditorOutputConstraint(jsonOutput, errors) { @@ -200,9 +214,16 @@ export class EditSchedulingUnit extends Component { } this.constraintParamsOutput = jsonOutput || {}; this.constraintValidEditor = err.length === 0; - this.setState({ constraintParamsOutput: jsonOutput, - constraintValidEditor: err.length === 0, - validForm: this.validateForm()}); + if ( !this.state.isDirty && this.state.constraintParamsOutput && !_.isEqual(this.state.constraintParamsOutput, this.constraintParamsOutput) ) { + this.setState({ constraintParamsOutput: jsonOutput, + constraintValidEditor: err.length === 0, + validForm: this.validateForm(), isDirty: true}); + } else { + this.setState({ constraintParamsOutput: jsonOutput, + constraintValidEditor: err.length === 0, + validForm: this.validateForm()}); + } + } /** @@ -218,9 +239,14 @@ export class EditSchedulingUnit extends Component { * @param {object} value */ setSchedUnitParams(key, value) { - let schedulingUnit = this.state.schedulingUnit; + let schedulingUnit = _.cloneDeep(this.state.schedulingUnit); schedulingUnit[key] = value; - this.setState({schedulingUnit: schedulingUnit, validForm: this.validateForm(key), validEditor: this.validateEditor()}); + if ( !this.state.isDirty && !_.isEqual(this.state.schedulingUnit, schedulingUnit) ) { + this.setState({schedulingUnit: schedulingUnit, validForm: this.validateForm(key), validEditor: this.validateEditor(), isDirty: true}); + } else { + this.setState({schedulingUnit: schedulingUnit, validForm: this.validateForm(key), validEditor: this.validateEditor()}); + } + // this.setState({schedulingUnit: schedulingUnit, validForm: this.validateForm(key), validEditor: this.validateEditor()}); this.validateEditor(); } @@ -353,9 +379,24 @@ export class EditSchedulingUnit extends Component { } else { this.growl.show({severity: 'error', summary: 'Error Occured', detail: 'Template Missing.'}); } + this.setState({isDirty: false}); } + /** + * warn before cancel the page if any changes detected + */ + checkIsDirty() { + if( this.state.isDirty ){ + this.setState({showDialog: true}); + } else { + this.cancelCreate(); + } + } + close() { + this.setState({showDialog: false}); + } + /** * Cancel SU creation and redirect */ @@ -368,7 +409,28 @@ export class EditSchedulingUnit extends Component { } onUpdateStations = (state, selectedStations, missingStationFieldsErrors, customSelectedStations) => { - this.setState({ + const selectedStation = this.state.selectedStations; + const customStation = this.state.customSelectedStations; + if ( !this.state.isDirty ) { + if (selectedStation && !_.isEqual(selectedStation, selectedStations)){ + this.setState({...state, selectedStations, missingStationFieldsErrors, customSelectedStations }, () => { + this.setState({ validForm: this.validateForm(), isDirty: true }); + }); + } else if (customStation && !_.isEqual(customStation, customSelectedStations)){ + this.setState({...state, selectedStations, missingStationFieldsErrors, customSelectedStations }, () => { + this.setState({ validForm: this.validateForm(), isDirty: true }); + }); + } else { + this.setState({...state, selectedStations, missingStationFieldsErrors, customSelectedStations }, () => { + this.setState({ validForm: this.validateForm() }); + }); + } + } else { + this.setState({...state, selectedStations, missingStationFieldsErrors, customSelectedStations }, () => { + this.setState({ validForm: this.validateForm() }); + }); + } + /* this.setState({ ...state, selectedStations, missingStationFieldsErrors, @@ -377,7 +439,7 @@ export class EditSchedulingUnit extends Component { this.setState({ validForm: this.validateForm() }); - }); + });*/ }; render() { @@ -400,7 +462,8 @@ export class EditSchedulingUnit extends Component { <React.Fragment> <Growl ref={el => (this.growl = el)} /> <PageHeader location={this.props.location} title={'Scheduling Unit - Edit'} - actions={[{icon: 'fa-window-close',link: this.props.history.goBack,title:'Click to Close Scheduling Unit View', props : { pathname: `/schedulingunit/view/draft/${this.props.match.params.id}`}}]}/> + actions={[{icon: 'fa-window-close', title:'Click to Close Scheduling Unit View', + type: 'button', actOn: 'click', props:{ callback: this.checkIsDirty }}]}/> { this.state.isLoading ? <AppLoader /> : <> <div> @@ -480,14 +543,12 @@ export class EditSchedulingUnit extends Component { </div> </div> </div> - <Stations stationGroup={this.state.stationGroup} onUpdateStations={this.onUpdateStations.bind(this)} /> - {this.state.constraintSchema && <div className="p-fluid"> <div className="p-grid"> <div className="p-col-12"> @@ -509,9 +570,16 @@ export class EditSchedulingUnit extends Component { disabled={!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.cancelCreate} /> + <Button label="Cancel" className="p-button-danger" icon="pi pi-times" onClick={this.checkIsDirty} /> </div> </div> + + <div className="p-grid" data-testid="confirm_dialog"> + <CustomDialog type="confirmation" visible={this.state.showDialog} width="40vw" + header={'Edit Scheduling Unit'} message={'Do you want to leave this page? Your changes may not be saved.'} + content={''} onClose={this.close} onCancel={this.close} onSubmit={this.cancelCreate}> + </CustomDialog> + </div> </div> </> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/create.scheduleset.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/excelview.schedulingset.js similarity index 68% rename from SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/create.scheduleset.js rename to SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/excelview.schedulingset.js index 8182b7b409ccf8f6d54ebce091b782f5fe5d9682..f15290796a26f00a8bc3520f1241c31bb8f16337 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/create.scheduleset.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/excelview.schedulingset.js @@ -1,26 +1,22 @@ import React, { Component } from 'react'; import { Redirect } from 'react-router-dom'; - import { Dropdown } from 'primereact/dropdown'; import { Button } from 'primereact/button'; -import { Dialog } from 'primereact/components/dialog/Dialog'; import { Growl } from 'primereact/components/growl/Growl'; import { Checkbox } from 'primereact/checkbox'; - +import { Accordion, AccordionTab } from 'primereact/accordion'; import { AgGridReact } from 'ag-grid-react'; import { AllCommunityModules } from '@ag-grid-community/all-modules'; import $RefParser from "@apidevtools/json-schema-ref-parser"; - -import TimeInputmask from './../../components/Spreadsheet/TimeInputmask' -import DegreeInputmask from './../../components/Spreadsheet/DegreeInputmask' +import TimeInputmask from '../../components/Spreadsheet/TimeInputmask' +import DegreeInputmask from '../../components/Spreadsheet/DegreeInputmask' import NumericEditor from '../../components/Spreadsheet/numericEditor'; import BetweenEditor from '../../components/Spreadsheet/BetweenEditor'; import BetweenRenderer from '../../components/Spreadsheet/BetweenRenderer'; import MultiSelector from '../../components/Spreadsheet/MultiSelector'; import AppLoader from '../../layout/components/AppLoader'; - import PageHeader from '../../layout/components/PageHeader'; -import { CustomDialog } from '../../layout/components/CustomDialog'; + import ProjectService from '../../services/project.service'; import ScheduleService from '../../services/schedule.service'; import TaskService from '../../services/task.service'; @@ -31,16 +27,17 @@ import UnitConverter from '../../utils/unit.converter' import UIConstants from '../../utils/ui.constants'; import UnitConversion from '../../utils/unit.converter'; import StationEditor from '../../components/Spreadsheet/StationEditor'; - +import SchedulingSet from './schedulingset.create'; import moment from 'moment'; import _ from 'lodash'; import 'ag-grid-community/dist/styles/ag-grid.css'; import 'ag-grid-community/dist/styles/ag-theme-alpine.css'; import { CustomPageSpinner } from '../../components/CustomPageSpinner'; +import { CustomDialog } from '../../layout/components/CustomDialog'; const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; -const BG_COLOR= '#f878788f'; +const BG_COLOR = '#f878788f'; /** * Component to create / update Scheduling Unit Drafts using Spreadsheet @@ -48,13 +45,22 @@ const BG_COLOR= '#f878788f'; export class SchedulingSetCreate extends Component { constructor(props) { super(props); - this.gridApi = '' - this.gridColumnApi = '' + this.gridApi = ''; + this.gridColumnApi = ''; + this.topGridApi = ''; + this.topGridColumnApi = ''; this.rowData = []; this.tmpRowData = []; this.daily = []; + this.dailyOption = []; + this.isNewSet = false; + //this.dialogMsg = ''; + //this.dialogType = ''; + //this.callBackFunction = ''; this.state = { + selectedProject: {}, copyHeader: false, // Copy Table Header to clipboard + applyEmptyValue: false, dailyOption: [], projectDisabled: (props.match?(props.match.params.project? true:false):false), isLoading: true, @@ -120,17 +126,39 @@ export class SchedulingSetCreate extends Component { customSelectedStations: [], selectedStations: [], defaultStationGroups: [], - saveDialogVisible: false, + //saveDialogVisible: false, defaultCellValues: {}, showDefault: false, - } + confirmDialogVisible: false, + isDirty: false + }; + this.showIcon = true; + this.dialogType = "confirmation"; + this.dialogHeight = 'auto'; + this.dialogHeader = ""; + this.dialogMsg = ""; + this.dialogContent = ""; + this.applyToAllRow = false; + this.callBackFunction = ""; + this.onClose = this.close; + this.onCancel =this.close; + this.applyToEmptyRowOnly = false; // A SU Row not exists and the Name & Desc are empty + this.applyToAll = this.applyToAll.bind(this); + this.applyToSelected = this.applyToSelected.bind(this); + this.applyToEmptyRows = this.applyToEmptyRows.bind(this); + this.resetCommonData = this.resetCommonData.bind(this); + this.reload = this.reload.bind(this); + this.applyChanges = this.applyChanges.bind(this); + this.onTopGridReady = this.onTopGridReady.bind(this); this.onGridReady = this.onGridReady.bind(this); this.validateForm = this.validateForm.bind(this); this.validateEditor = this.validateEditor.bind(this); this.saveSchedulingUnit = this.saveSchedulingUnit.bind(this); this.cancelCreate = this.cancelCreate.bind(this); + this.checkIsDirty = this.checkIsDirty.bind(this); this.clipboardEvent = this.clipboardEvent.bind(this); + this.topAGGridEvent = this.topAGGridEvent.bind(this); this.reset = this.reset.bind(this); this.close = this.close.bind(this); this.saveSU = this.saveSU.bind(this); @@ -140,6 +168,13 @@ export class SchedulingSetCreate extends Component { this.setDefaultCellValue = this.setDefaultCellValue.bind(this); this.copyHeader = this.copyHeader.bind(this); this.copyOnlyHeader = this.copyOnlyHeader.bind(this); + this.cellValueChageEvent = this.cellValueChageEvent.bind(this); + this.onProjectChange = this.onProjectChange.bind(this); + this.showWarning = this.showWarning.bind(this); + this.onSchedulingSetChange = this.onSchedulingSetChange.bind(this); + this.onStrategyChange = this.onStrategyChange.bind(this); + this.refreshSchedulingSet = this.refreshSchedulingSet.bind(this); + this.showAddSchedulingSet = this.showAddSchedulingSet.bind(this); this.projects = []; // All projects to load project dropdown this.schedulingSets = []; // All scheduling sets to be filtered for project @@ -151,7 +186,6 @@ export class SchedulingSetCreate extends Component { project: {required: true, message: "Select project to get Scheduling Sets"}, scheduling_set_id: {required: true, message: "Select the Scheduling Set"}, }; - } componentDidMount() { @@ -166,13 +200,45 @@ export class SchedulingSetCreate extends Component { this.taskTemplates = responses[3]; if (this.state.schedulingUnit.project) { const projectSchedluingSets = _.filter(this.schedulingSets, {'project_id': this.state.schedulingUnit.project}); - this.setState({isLoading: false, schedulingSets: projectSchedluingSets}); + this.setState({isLoading: false, schedulingSets: projectSchedluingSets, allSchedulingSets: this.schedulingSets}); } else { this.setState({isLoading: false}); } }); } + /** + * Show warning messgae if any changes not saved when the AG grid reload or cancel the page + * @param {*} functionName + */ + showWarning (functionName) { + this.showIcon = true; + this.dialogType = "confirmation"; + this.dialogHeader = "Add Multiple Scheduling Unit(s)"; + this.dialogMsg = "Do you want to leave the changes? Your changes may not be saved."; + this.dialogContent = ""; + this.callBackFunction = functionName; + this.onClose = this.close; + this.onCancel = this.close; + this.setState({ + confirmDialogVisible: true, + }); + } + + /** + * Trigger when the project drop down get changed and check isDirty + * @param {*} projectName + */ + onProjectChange(projectName) { + if (this.state.isDirty) { + this.showWarning(() =>{ + this. changeProject(projectName); + }); + } else { + this.changeProject(projectName); + } + } + /** * Function to call on change of project and reload scheduling set dropdown * @param {string} projectName @@ -181,17 +247,37 @@ export class SchedulingSetCreate extends Component { const projectSchedluingSets = _.filter(this.schedulingSets, {'project_id': projectName}); let schedulingUnit = this.state.schedulingUnit; schedulingUnit.project = projectName; - this.setState({schedulingUnit: schedulingUnit, schedulingSets: projectSchedluingSets, validForm: this.validateForm('project'), rowData: [],observStrategy: {}, copyHeader: false}); + /* this.setState({confirmDialogVisible: false, isDirty: false, schedulingUnit: schedulingUnit, + schedulingSets: projectSchedluingSets, validForm: this.validateForm('project'), rowData: [], + observStrategy: {}, copyHeader: false, isDirty: false}); */ + + const selectedProject = _.filter(this.projects, {'name': projectName}); + this.setState({confirmDialogVisible: false, isDirty: false, selectedProject: selectedProject, schedulingUnit: schedulingUnit, + schedulingSets: projectSchedluingSets, validForm: this.validateForm('project'), rowData: [],observStrategy: {}, copyHeader: false}); } + /** + * Trigger when the Scheduling Set drop down get changed and check isDirty + * @param {*} key + * @param {*} value + */ + onSchedulingSetChange(key, value) { + if (this.state.isDirty) { + this.showWarning(() =>{ + this.setSchedulingSetParams(key, value); + }); + } else { + this. setSchedulingSetParams(key, value); + } + } + /** * Function to set form values to the SU object * @param {string} key * @param {object} value */ async setSchedulingSetParams(key, value) { - this.setState({isAGLoading: true, copyHeader: false}); - + this.setState({isAGLoading: true, copyHeader: false, confirmDialogVisible: false, isDirty: false}); let schedulingUnit = this.state.schedulingUnit; schedulingUnit[key] = value; @@ -205,6 +291,7 @@ export class SchedulingSetCreate extends Component { schedulingUnit: schedulingUnit, validForm: this.validateForm(key), validEditor: this.validateEditor(), schedulingUnitList: schedulingUnitList, schedulingSetId: value, selectedSchedulingSetId: value, observStrategy: observStrategy, }); + this.isNewSet = false; await this.prepareScheduleUnitListForGrid(); } else { /* Let user to select Observation Strategy */ @@ -232,7 +319,7 @@ export class SchedulingSetCreate extends Component { await $RefParser.resolve(task); // Identify the task specification template of every task in the strategy template const taskTemplate = _.find(this.taskTemplates, {'name': task['specifications_template']}); - if (taskTemplate.type_value==='observation' && task.specifications_doc.station_groups) { + if (taskTemplate.type_value === 'observation' && task.specifications_doc.station_groups) { station_group = task.specifications_doc.station_groups; } } @@ -240,20 +327,39 @@ export class SchedulingSetCreate extends Component { defaultStationGroups: station_group, }) } - - + + /** + * Trigger when the Strategy drop down get changed and check isDirty + * @param {*} strategyId + */ + onStrategyChange(strategyId) { + if (this.state.isDirty) { + this.showWarning(() =>{ + this.changeStrategy(strategyId); + }); + } else { + this. changeStrategy(strategyId); + } + } /** * Function called when observation strategy template is changed. * * @param {number} strategyId */ - async changeStrategy (strategyId) { - await this.setState({isAGLoading: true, copyHeader: false, rowData: []}); + async changeStrategy(strategyId) { + await this.setState({noOfSU: 10, isAGLoading: true, copyHeader: false, rowData: [], confirmDialogVisible: false, isDirty: false}); const observStrategy = _.find(this.observStrategies, {'id': strategyId}); let schedulingUnitList= await ScheduleService.getSchedulingBySet(this.state.selectedSchedulingSetId); schedulingUnitList = _.filter(schedulingUnitList,{'observation_strategy_template_id': strategyId}) ; this.setDefaultStationGroup(observStrategy); + if(schedulingUnitList.length === 0) { + schedulingUnitList = await this.getEmptySchedulingUnit(strategyId); + this.isNewSet = true; + } + else { + this.isNewSet = false; + } await this.setState({ schedulingUnitList: schedulingUnitList, observStrategy: observStrategy, @@ -266,9 +372,14 @@ export class SchedulingSetCreate extends Component { rowData: [] }); } - this.setState({isAGLoading: false}); + this.setState({isAGLoading: false,commonRowData: []}); } + async getEmptySchedulingUnit(strategyId){ + let suList = await ScheduleService.getSchedulingUnitDraft(); + return [_.find(suList.data.results, {'observation_strategy_template_id': strategyId})]; + } + /** * Resolve JSON Schema */ @@ -325,34 +436,10 @@ export class SchedulingSetCreate extends Component { } /** - * Function to generate AG-Grid column definition. - * @param {number} strategyId + * Create AG Grid column properties */ - async createGridColumns(scheduleUnit){ - let defaultCellValues = {}; - let schema = await this.getTaskSchema(scheduleUnit, false); - schema = await this.resolveSchema(schema); - let constraintSchema = await this.getConstraintSchema(scheduleUnit); - constraintSchema = await this.resolveSchema(constraintSchema); - - // AG Grid Cell Specific Properties - let dailyOption= []; - let dailyProps = Object.keys( constraintSchema.schema.properties.daily.properties); - this.daily = []; - dailyProps.forEach(prop => { - dailyOption.push({'Name':prop, 'Code':prop}); - this.daily.push(prop); - }) - - this.setState({ - dailyOption: this.dailyOption, - schedulingConstraintsDoc: scheduleUnit.scheduling_constraints_doc, - constraintUrl: scheduleUnit.scheduling_constraints_template, - constraintId: scheduleUnit.scheduling_constraints_template_id, - daily: this.daily, - }); - - let cellProps =[]; + createAGGridAngelColumnsProperty(schema) { + let cellProps = []; cellProps['angle1'] = {isgroup: true, type:'numberValueColumn', cellRenderer: 'timeInputMask',cellEditor: 'timeInputMask', valueSetter: 'valueSetter', cellStyle: function(params) { if (params.value && !Validator.validateTime(params.value)) { return { backgroundColor: BG_COLOR}; @@ -368,21 +455,20 @@ export class SchedulingSetCreate extends Component { } }, }; cellProps['angle3'] = {isgroup: true, cellStyle: function(params) { if (params.value){ - if (!Number(params.value)) { + if (!params.colDef.field.startsWith('gdef') && !Number(params.value)) { return { backgroundColor: BG_COLOR}; } else{ return { backgroundColor: ''}; } } else { - return { backgroundColor: BG_COLOR}; + return (!params.colDef.field.startsWith('gdef')) ?{ backgroundColor: BG_COLOR} : { backgroundColor: ''} }}}; cellProps['direction_type'] = {isgroup: true, cellEditor: 'agSelectCellEditor',default: schema.definitions.pointing.properties.direction_type.default, cellEditorParams: { values: schema.definitions.pointing.properties.direction_type.enum, }, }; - cellProps['duration'] = { type:'numberValueColumn', cellStyle: function(params) { if (params.value){ if ( !Number(params.value)){ @@ -396,13 +482,41 @@ export class SchedulingSetCreate extends Component { } }, }; + return cellProps; + } + + /** + * Function to generate AG-Grid column definition. + * @param {number} strategyId + */ + async createGridColumns(scheduleUnit){ + let defaultCellValues = {}; + let schema = await this.getTaskSchema(scheduleUnit, false); + schema = await this.resolveSchema(schema); + let constraintSchema = await this.getConstraintSchema(scheduleUnit); + constraintSchema = await this.resolveSchema(constraintSchema); + // AG Grid Cell Specific Properties + let dailyProps = Object.keys( constraintSchema.schema.properties.daily.properties); + this.daily = []; + this.dailyOption = []; + dailyProps.forEach(prop => { + this.dailyOption.push({'name':prop, 'value':prop}); + this.daily.push(prop); + }) + this.setState({ + dailyOption: this.dailyOption, + schedulingConstraintsDoc: scheduleUnit.scheduling_constraints_doc, + constraintUrl: scheduleUnit.scheduling_constraints_template, + constraintId: scheduleUnit.scheduling_constraints_template_id, + daily: this.daily, + }); + + let cellProps = this.createAGGridAngelColumnsProperty(schema); //Ag-grid Colums definition // Column order to use clipboard copy let colKeyOrder = []; - colKeyOrder.push("suname"); colKeyOrder.push("sudesc"); - let columnMap = []; let colProperty = {}; let columnDefs = [ @@ -572,7 +686,6 @@ export class SchedulingSetCreate extends Component { colKeyOrder.push('md_sun'); colKeyOrder.push('md_moon'); colKeyOrder.push('md_jupiter'); - defaultCellValues['scheduler'] = constraintSchema.schema.properties.scheduler.default; defaultCellValues['min_target_elevation'] = constraintSchema.schema.properties.sky.properties.min_target_elevation.default; defaultCellValues['min_calibrator_elevation'] = constraintSchema.schema.properties.sky.properties.min_calibrator_elevation.default; @@ -589,7 +702,7 @@ export class SchedulingSetCreate extends Component { }) defaultCellValues['stations'] = stationValue; } - colProperty ={'ID':'id', 'Name':'suname', 'Description':'sudesc'}; + colProperty = {'ID':'id', 'Name':'suname', 'Description':'sudesc'}; columnMap['Scheduling Unit'] = colProperty; let defaultSchema = await this.getTaskTemplateSchema(scheduleUnit, 'Target Observation'); @@ -648,12 +761,56 @@ export class SchedulingSetCreate extends Component { } columnDefs.push({headerName: 'Stations', field: 'stations', cellRenderer: 'betweenRenderer', cellEditor: 'station', valueSetter: 'newValueSetter'}); colKeyOrder.push('stations'); + let globalColmunDef =_.cloneDeep(columnDefs); + globalColmunDef = await this.createGlobalColumnDefs(globalColmunDef, schema, constraintSchema); + this.setState({ - columnDefs:columnDefs, - columnMap:columnMap, - colKeyOrder:colKeyOrder, + columnDefs: columnDefs, + globalColmunDef: globalColmunDef, + columnMap: columnMap, + colKeyOrder: colKeyOrder, defaultCellValues: defaultCellValues, - }) + }); + } + + /** + * Create AG Grid column definition + * @param {*} globalColmunDef + * @param {*} schema + * @param {*} constraintSchema + */ + createGlobalColumnDefs(globalColmunDef, schema, constraintSchema) { + let schedulerValues = [...' ', ...constraintSchema.schema.properties.scheduler.enum]; + let direction_type_Values = [...' ', ...schema.definitions.pointing.properties.direction_type.enum]; + globalColmunDef.forEach(colDef => { + if (colDef.children) { + colDef.children.forEach(childColDef => { + if (childColDef.field) { + if(childColDef.field.endsWith('direction_type')) { + childColDef.cellEditorParams.values = direction_type_Values; + } + childColDef.field = 'gdef_'+childColDef.field; + if (childColDef.default) { + childColDef.default = ''; + } + } + }); + } else { + if(colDef.headerName === '#') { + colDef['hide'] = true; + } + if(colDef.field) { + if ( colDef.field.endsWith('scheduler')) { + colDef.cellEditorParams.values = schedulerValues; + } + colDef.field = 'gdef_'+colDef.field; + if (colDef.default) { + colDef.default = ''; + } + } + } + }); + return globalColmunDef; } async getTaskTemplateSchema(scheduleUnit, taskName) { @@ -678,7 +835,6 @@ export class SchedulingSetCreate extends Component { let index = 0; for (const param of observStrategy.template.parameters) { if (param.refs[0].indexOf(`/tasks/${taskName}`) > 0) { - // tasksToUpdate[taskName] = taskName; // Resolve the identified template const $templateRefs = await $RefParser.resolve(taskTemplate); let property = { }; @@ -720,9 +876,9 @@ export class SchedulingSetCreate extends Component { let schema = { type: 'object', additionalProperties: false, properties: {}, definitions:{} }; - let taskDrafts= []; + let taskDrafts = []; await ScheduleService.getTasksDraftBySchedulingUnitId(scheduleUnit.id).then(response =>{ - taskDrafts= response.data.results; + taskDrafts = response.data.results; }); for (const taskName in tasks) { @@ -769,7 +925,7 @@ export class SchedulingSetCreate extends Component { } index++; } - if (taskTemplate.type_value==='observation' && task.specifications_doc.station_groups) { + if (taskTemplate.type_value === 'observation' && task.specifications_doc.station_groups) { tasksToUpdate[taskName] = taskName; } this.setState({ paramsOutput: paramsOutput, tasksToUpdate: tasksToUpdate}); @@ -777,44 +933,83 @@ export class SchedulingSetCreate extends Component { return schema; } - /** * CallBack Function : update time value in master grid */ async updateTime(rowIndex, field, value) { - let row = this.state.rowData[rowIndex]; - row[field] = value; - let tmpRowData =this.state.rowData; - tmpRowData[rowIndex]= row; - await this.setState({ - rowData: tmpRowData - }); - this.state.gridApi.setRowData(this.state.rowData); - this.state.gridApi.redrawRows(); - } - - /** - * Update the Daily/Station column value from external component - * @param {*} rowIndex - * @param {*} field - * @param {*} value - */ + let row = {}; + let tmpRowData = []; + if ( field.startsWith('gdef_')) { + row = this.state.commonRowData[0]; + row[field] = value; + tmpRowData =this.state.commonRowData; + tmpRowData[0] = row; + await this.setState({ + commonRowData: tmpRowData + }); + this.state.topGridApi.setRowData(this.state.commonRowData); + this.state.topGridApi.redrawRows(); + } + else { + row = this.state.rowData[rowIndex]; + row[field] = value; + tmpRowData = this.state.rowData; + tmpRowData[rowIndex] = row; + await this.setState({ + rowData: tmpRowData, + isDirty: true + }); + this.state.gridApi.setRowData(this.state.rowData); + this.state.gridApi.redrawRows(); + } + } + + /** + * Update the Daily/Station column value from external component + * @param {*} rowIndex + * @param {*} field + * @param {*} value + */ async updateCell(rowIndex, field, value) { - let row = this.state.rowData[rowIndex]; - row[field] = value; - let tmpRowData =this.state.rowData; - tmpRowData[rowIndex]= row; - await this.setState({ - rowData: tmpRowData - }); - if(field !== 'daily') { - this.state.gridApi.stopEditing(); - var focusedCell = this.state.gridColumnApi.getColumn(field) - this.state.gridApi.ensureColumnVisible(focusedCell); - this.state.gridApi.setFocusedCell(rowIndex, focusedCell); + let row = {}; + let tmpRowData = []; + if ( field.startsWith('gdef_')) { + row = this.state.commonRowData[0]; + row[field] = value; + tmpRowData = this.state.commonRowData; + tmpRowData[0] = row; + await this.setState({ + commonRowData: tmpRowData + }); + if(field !== 'gdef_daily') { + this.state.topGridApi.stopEditing(); + var focusedCell = this.state.topGridColumnApi.getColumn(field) + this.state.topGridApi.ensureColumnVisible(focusedCell); + this.state.topGridApi.setFocusedCell(rowIndex, focusedCell); + } + } + else { + row = this.state.rowData[rowIndex]; + row[field] = value; + tmpRowData = this.state.rowData; + tmpRowData[rowIndex] = row; + await this.setState({ + rowData: tmpRowData, + isDirty: true + }); + if(field !== 'daily') { + this.state.gridApi.stopEditing(); + var focusedCell = this.state.gridColumnApi.getColumn(field) + this.state.gridApi.ensureColumnVisible(focusedCell); + this.state.gridApi.setFocusedCell(rowIndex, focusedCell); + } } } + /** + * Get Station details + * @param {*} schedulingUnit + */ async getStationGrops(schedulingUnit){ let stationValue = ''; if (schedulingUnit && schedulingUnit.id>0) { @@ -907,11 +1102,13 @@ export class SchedulingSetCreate extends Component { * Function to prepare ag-grid row data. */ async prepareScheduleUnitListForGrid(){ - if (this.state.schedulingUnitList.length===0) { + if (this.state.schedulingUnitList.length === 0) { return; } this.tmpRowData = []; let totalSU = this.state.noOfSU; + let lastRow = {}; + let hasSameValue = true; //refresh column header await this.createGridColumns(this.state.schedulingUnitList[0]); let observationPropsList = []; @@ -985,6 +1182,7 @@ export class SchedulingSetCreate extends Component { } observationProps['daily'] = this.fetchDailyFieldValue(constraint.daily); + //console.log("SU id:", scheduleunit.id, "Connstraint:", constraint.sky); UnitConversion.radiansToDegree(constraint.sky); observationProps['min_target_elevation'] = constraint.sky.min_target_elevation; observationProps['min_calibrator_elevation'] = constraint.sky.min_calibrator_elevation; @@ -1000,41 +1198,65 @@ export class SchedulingSetCreate extends Component { } } observationPropsList.push(observationProps); + //Set values for global row if all rows has same value + if (_.isEmpty(lastRow)) { + lastRow = observationProps; + } + else if (!_.isEqual( + _.omit(lastRow, ['id']), + _.omit(observationProps, ['id']) + )) { + hasSameValue = false; + } + + } + let defaultCommonRowData = {}; + if (hasSameValue) { + defaultCommonRowData = observationPropsList[observationPropsList.length-1]; } - this.tmpRowData = observationPropsList; // find No. of rows filled in array let totalCount = this.tmpRowData.length; // Prepare No. Of SU for rows for UI - if (this.tmpRowData && this.tmpRowData.length>0){ + if (this.tmpRowData && this.tmpRowData.length > 0){ const paramsOutputKey = Object.keys( this.tmpRowData[0]); - const availableCount = this.tmpRowData.length; + let availableCount = this.tmpRowData.length; + if(this.isNewSet) { + availableCount = 0; + this.tmpRowData = []; + } if (availableCount >= totalSU){ - totalSU = availableCount+5; + totalSU = availableCount+1; } for(var i = availableCount; i<totalSU; i++){ let emptyRow = {}; paramsOutputKey.forEach(key =>{ if (key === 'id'){ - emptyRow[key]= 0; + emptyRow[key] = 0; } else { - emptyRow[key]= ''; + emptyRow[key] = ''; } }) this.tmpRowData.push(emptyRow); } } + if(this.isNewSet) { + defaultCommonRowData = this.tmpRowData[this.tmpRowData.length-1]; + } this.setState({ rowData: this.tmpRowData, totalCount: totalCount, - noOfSU: totalSU, + noOfSU: this.tmpRowData.length, emptyRow: this.tmpRowData[this.tmpRowData.length-1], - isAGLoading: false + isAGLoading: false, + commonRowData: [defaultCommonRowData], + defaultCommonRowData: defaultCommonRowData, + hasSameValue: hasSameValue }); - + this.setDefaultCellValue(); } - + /** * Get Daily column value * @param {*} daily @@ -1059,17 +1281,32 @@ export class SchedulingSetCreate extends Component { * @param {Stirng} cell -> contains Row ID, Column Name, Value, isDegree */ async updateAngle(rowIndex, field, value, isDegree, isValid){ - let row = this.state.rowData[rowIndex]; - row[field] = value; - row['isValid'] = isValid; - //Convertverted value for Angle 1 & 2, set in SU Row - row[field+'value'] = UnitConverter.getAngleOutput(value,isDegree); - let tmpRowData =this.state.rowData; - tmpRowData[rowIndex]= row; - await this.setState({ - rowData: tmpRowData - }); - } + let row = {}; + let tmpRowData = []; + if ( field.startsWith('gdef_')) { + row = this.state.commonRowData[0]; + row[field] = value; + row['isValid'] = isValid; + row[field+'value'] = UnitConverter.getAngleOutput(value,isDegree); + tmpRowData = this.state.commonRowData; + tmpRowData[0] = row; + await this.setState({ + commonRowData: tmpRowData + }); + } + else { + row = this.state.rowData[rowIndex]; + row[field] = value; + row['isValid'] = isValid; + row[field+'value'] = UnitConverter.getAngleOutput(value,isDegree); + tmpRowData = this.state.rowData; + tmpRowData[rowIndex] = row; + await this.setState({ + rowData: tmpRowData, + isDirty: true + }); + } + } /** * Read Data from clipboard @@ -1085,128 +1322,140 @@ export class SchedulingSetCreate extends Component { } } - /** - * Copy data to/from clipboard - * @param {*} e - */ - async clipboardEvent(e){ + async topAGGridEvent(e) { var key = e.which || e.keyCode; var ctrl = e.ctrlKey ? e.ctrlKey : ((key === 17) ? true : false); - if ( key === 67 && ctrl ) { - //Ctrl+C - var columnsName = this.state.gridColumnApi.getAllGridColumns(); - var selectedRows = this.state.gridApi.getSelectedRows(); - let clipboardData = ''; - if ( this.state.copyHeader ) { - var line = ''; - columnsName.map( column => { - if ( column.colId !== '0'){ - line += column.colDef.headerName + '\t'; - } - }) - line = _.trim(line); - clipboardData += line + '\r\n'; - // this.setState({copyHeader: false}); - } - for(const rowData of selectedRows){ - var line = ''; - for(const key of this.state.colKeyOrder){ - line += rowData[key] + '\t'; - } - line = _.trim(line); - clipboardData += line + '\r\n'; - } - clipboardData = _.trim(clipboardData); - - const queryOpts = { name: 'clipboard-write', allowWithoutGesture: true }; - await navigator.permissions.query(queryOpts); - await navigator.clipboard.writeText(clipboardData); - } - else if ( key === 86 && ctrl ) { - // Ctrl+V - try { - var selectedRows = this.state.gridApi.getSelectedNodes(); - this.tmpRowData = this.state.rowData; - let dataRowCount = this.state.totalCount; - //Read Clipboard Data - let clipboardData = await this.readClipBoard(); - let selectedRowIndex = 0; - if (selectedRows){ - await selectedRows.map(selectedRow =>{ - selectedRowIndex = selectedRow.rowIndex; - if (clipboardData){ - clipboardData = _.trim(clipboardData); - let suGridRowData= this.state.emptyRow; - clipboardData = _.trim(clipboardData); - let suRows = clipboardData.split("\n"); - suRows.forEach(line =>{ - suGridRowData ={}; - suGridRowData['id']= 0; - suGridRowData['isValid']= true; - - if ( this.tmpRowData.length <= selectedRowIndex ) { - this.tmpRowData.push(this.state.emptyRow); - } - - let colCount = 0; - let suRow = line.split("\t"); - for(const key of this.state.colKeyOrder){ - suGridRowData[key]= suRow[colCount]; - colCount++; - } - if (this.tmpRowData[selectedRowIndex].id > 0 ) { - suGridRowData['id'] = this.tmpRowData[selectedRowIndex].id; - } - this.tmpRowData[selectedRowIndex]= (suGridRowData); - selectedRowIndex++ - }) - } - }); - dataRowCount = selectedRowIndex; - let emptyRow = this.state.emptyRow; - let tmpNoOfSU= this.state.noOfSU; - if (dataRowCount >= tmpNoOfSU){ - tmpNoOfSU = dataRowCount; - //Create additional empty row at the end - for(let i= this.tmpRowData.length; i<=tmpNoOfSU; i++){ - this.tmpRowData.push(emptyRow); - } - } - await this.setState({ - rowData: this.tmpRowData, - noOfSU: this.tmpRowData.length, - totalCount: dataRowCount, - }) - this.state.gridApi.setRowData(this.state.rowData); - this.state.gridApi.redrawRows(); + if ( ctrl && (key === 67 || key === 86)) { + this.showIcon = true; + this.dialogType = "warning"; + this.dialogHeader = "Warning"; + this.dialogMsg = "Copy / Paste is restricted in this grid"; + this.dialogContent = ""; + this.callBackFunction = this.close; + this.onClose = this.close; + this.onCancel = this.close; + this.setState({ + confirmDialogVisible: true, + }); + } + } + + /** + * Function to copy the data to clipboard + */ + async copyToClipboard(){ + var columnsName = this.state.gridColumnApi.getAllGridColumns(); + var selectedRows = this.state.gridApi.getSelectedRows(); + let clipboardData = ''; + if ( this.state.copyHeader ) { + var line = ''; + columnsName.map( column => { + if ( column.colId !== '0'){ + line += column.colDef.headerName + '\t'; } + }) + line = _.trim(line); + clipboardData += line + '\r\n'; + } + for(const rowData of selectedRows){ + var line = ''; + for(const key of this.state.colKeyOrder){ + line += rowData[key] + '\t'; } - catch (err) { - console.error('Error: ', err); - } + line = _.trim(line); + clipboardData += line + '\r\n'; } + clipboardData = _.trim(clipboardData); + + const queryOpts = { name: 'clipboard-write', allowWithoutGesture: true }; + await navigator.permissions.query(queryOpts); + await navigator.clipboard.writeText(clipboardData); + const headerText = (this.state.copyHeader) ?'with Header' : ''; + this.growl.show({severity: 'success', summary: '', detail: selectedRows.length+' row(s) copied to clipboard '+headerText }); + } - //>>>>>> Resolved Conflicts by Ramesh. Remove or update this block as applicable. - /*else if ( key === 46){ - this.growl.show({severity: 'success', summary: '', detail: selectedRows.length+' row(s) copied to clipboard '}); - } - /* else if ( key === 46){ - // Delete selected rows - let tmpRowData = this.state.rowData; - + /** + * Function to copy the data from clipboard + */ + async copyFromClipboard(){ + try { var selectedRows = this.state.gridApi.getSelectedNodes(); + this.tmpRowData = this.state.rowData; + let dataRowCount = this.state.totalCount; + //Read Clipboard Data + let clipboardData = await this.readClipBoard(); + let selectedRowIndex = 0; if (selectedRows){ - await selectedRows.map(delRow =>{ - delete tmpRowData[delRow.rowIndex] + await selectedRows.map(selectedRow =>{ + selectedRowIndex = selectedRow.rowIndex; + if (clipboardData){ + clipboardData = _.trim(clipboardData); + let suGridRowData = this.state.emptyRow; + clipboardData = _.trim(clipboardData); + let suRows = clipboardData.split("\n"); + suRows.forEach(line => { + suGridRowData = {}; + suGridRowData['id'] = 0; + suGridRowData['isValid'] = true; + + if ( this.tmpRowData.length <= selectedRowIndex ) { + this.tmpRowData.push(this.state.emptyRow); + } + + let colCount = 0; + let suRow = line.split("\t"); + for(const key of this.state.colKeyOrder){ + suGridRowData[key] = suRow[colCount]; + colCount++; + } + if (this.tmpRowData[selectedRowIndex].id > 0 ) { + suGridRowData['id'] = this.tmpRowData[selectedRowIndex].id; + } + this.tmpRowData[selectedRowIndex] = (suGridRowData); + selectedRowIndex++ + }) + } }); + dataRowCount = selectedRowIndex; + let emptyRow = this.state.emptyRow; + let tmpNoOfSU = this.state.noOfSU; + if (dataRowCount >= tmpNoOfSU){ + tmpNoOfSU = dataRowCount; + //Create additional empty row at the end + for(let i= this.tmpRowData.length; i<= tmpNoOfSU; i++){ + this.tmpRowData.push(emptyRow); + } + } await this.setState({ - rowData: tmpRowData - }); - this.state.gridApi.setRowData(this.state.rowData); + rowData: this.tmpRowData, + noOfSU: this.tmpRowData.length, + totalCount: dataRowCount, + isDirty: true + }) + this.state.gridApi.setRowData(this.state.rowData); this.state.gridApi.redrawRows(); } } - } *///<<<<<< Resolved Conflicts + catch (err) { + console.error('Error: ', err); + } + } + + /** + * Copy data to/from clipboard + * @param {*} e + */ + async clipboardEvent(e){ + var key = e.which || e.keyCode; + var ctrl = e.ctrlKey ? e.ctrlKey : ((key === 17) ? true : false); + if ( key === 67 && ctrl ) { + //Ctrl+C + this.copyToClipboard(); + } + else if ( key === 86 && ctrl ) { + // Ctrl+V + this.copyFromClipboard(); + } } /** @@ -1234,6 +1483,7 @@ export class SchedulingSetCreate extends Component { this.growl.show({severity: 'success', summary: '', detail: 'Header copied to clipboard '}); } } + /** * Set state to copy the table header to clipboard * @param {*} value @@ -1259,14 +1509,17 @@ export class SchedulingSetCreate extends Component { tmpMandatoryKeys = []; const rowData = node.data; let isManualScheduler = false; + let hasData = true; if (rowData) { for(const key of mandatoryKeys) { if (rowData[key] === '') { if ( key === 'suname' ){ if( rowData['sudesc'] !== ''){ tmpMandatoryKeys.push(key); + } else { + hasData = false; } - } else if ( key === 'sudesc' ){ + } else if ( key === 'sudesc' ){ if( rowData['suname'] !== ''){ tmpMandatoryKeys.push(key); } @@ -1340,7 +1593,7 @@ export class SchedulingSetCreate extends Component { } else if(_.endsWith(column.colId, "stations")){ let sgCellValue = rowData[column.colId]; let stationGroups = _.split(sgCellValue, "|"); - stationGroups.map(stationGroup =>{ + stationGroups.map(stationGroup => { let sgValue = _.split(stationGroup, ":"); if (rowData['suname'] !== '' && rowData['sudesc'] !== '' && (sgValue[1] === 'undefined' || sgValue[1] === 'NaN' || Number(sgValue[1]) < 0 )){ isValidRow = false; @@ -1353,31 +1606,55 @@ export class SchedulingSetCreate extends Component { } } } - if (isValidRow) { - validCount++; - tmpRowData[node.rowIndex]['isValid'] = true; - } else { - inValidCount++; - tmpRowData[node.rowIndex]['isValid'] = false; - errorDisplay.push(errorMsg.slice(0, -2)); - } + if(hasData) { + if (isValidRow) { + validCount++; + tmpRowData[node.rowIndex]['isValid'] = true; + } else { + inValidCount++; + tmpRowData[node.rowIndex]['isValid'] = false; + errorDisplay.push(errorMsg.slice(0, -2)); + } + } }); - if (validCount > 0 && inValidCount === 0) { // save SU directly this.saveSU(); } else if (validCount === 0 && inValidCount === 0) { // leave with no change + this.showIcon = true; + this.dialogMsg = 'No valid Scheduling Unit found !'; + this.dialogType = 'warning'; + this.onClose = () => { + this.setState({confirmDialogVisible: false}); + }; + this.setState({ + confirmDialogVisible: true, + }); + } else { this.setState({ validCount: validCount, inValidCount: inValidCount, tmpRowData: tmpRowData, - saveDialogVisible: true, + //saveDialogVisible: true, errorDisplay: errorDisplay, + confirmDialogVisible: true, }); + this.callBackFunction = this.saveSU; this.state.gridApi.redrawRows(); + this.showIcon = true; + this.onCancel = () => { + this.setState({confirmDialogVisible: false}); + }; + this.onClose = () => { + this.setState({confirmDialogVisible: false}); + }; + this.dialogType = "confirmation"; + this.dialogHeader = "Save Scheduling Unit(s)"; + this.dialogMsg = "Some of the Scheduling Unit(s) has invalid data, Do you want to ignore and save valid Scheduling Unit(s) only?"; + this.dialogContent = this.showDialogContent; } } @@ -1397,7 +1674,8 @@ export class SchedulingSetCreate extends Component { let existingSUCount = 0; try{ this.setState({ - saveDialogVisible: false, + // saveDialogVisible: false, + confirmDialogVisible: false, showSpinner: true }) @@ -1416,7 +1694,7 @@ export class SchedulingSetCreate extends Component { let paramOutput = {}; let result = columnMap[parameter.name]; let resultKeys = Object.keys(result); - resultKeys.forEach(key =>{ + resultKeys.forEach(key => { if (key === 'angle1') { if (!Validator.validateTime(suRow[result[key]])) { validRow = false; @@ -1457,7 +1735,6 @@ export class SchedulingSetCreate extends Component { tmpStationGroup['max_nr_missing'] = Number(sgValue[1]); tmpStationGroups.push(tmpStationGroup); } - }) let observStrategy = _.cloneDeep(this.state.observStrategy); @@ -1496,23 +1773,23 @@ export class SchedulingSetCreate extends Component { //If No SU Constraint create default ( maintain default struc) constraint['scheduler'] = suRow.scheduler; if (suRow.scheduler === 'dynamic' || suRow.scheduler === 'online'){ - if (!this.isNotEmpty(suRow.timeat)) { - delete constraint.time.timeat; - } else { + if (this.isNotEmpty(suRow.timeat)) { + delete constraint.time.at; + } /*else { constraint.time.at = `${moment(suRow.timeat).format("YYYY-MM-DDTHH:mm:ss.SSSSS", { trim: false })}Z`; - } + }*/ if (!this.isNotEmpty(suRow.timeafter)) { delete constraint.time.after; - } else { + } /*else { constraint.time.after = `${moment(suRow.timeafter).format("YYYY-MM-DDTHH:mm:ss.SSSSS", { trim: false })}Z`; - } + }*/ if (!this.isNotEmpty(suRow.timebefore)) { delete constraint.time.before; - } else { + } /*else { constraint.time.before = `${moment(suRow.timebefore).format("YYYY-MM-DDTHH:mm:ss.SSSSS", { trim: false })}Z`; - } + }*/ } else { //mandatory @@ -1538,7 +1815,7 @@ export class SchedulingSetCreate extends Component { constraint.time.not_between = notbetween; } let dailyValueSelected = _.split(suRow.daily, ","); - this.state.daily.forEach(daily =>{ + this.state.daily.forEach(daily => { if (_.includes(dailyValueSelected, daily)){ constraint.daily[daily] = true; } else { @@ -1598,10 +1875,20 @@ export class SchedulingSetCreate extends Component { } } - if ((newSUCount+existingSUCount)>0){ - const dialog = {header: 'Success', detail: '['+newSUCount+'] Scheduling Units are created & ['+existingSUCount+'] Scheduling Units are updated successfully.'}; - this.setState({ showSpinner: false, dialogVisible: true, dialog: dialog, isAGLoading: true, copyHeader: false, rowData: []}); + if ((newSUCount+existingSUCount) > 0){ + //const dialog = {header: 'Success', detail: '['+newSUCount+'] Scheduling Units are created & ['+existingSUCount+'] Scheduling Units are updated successfully.'}; + // this.setState({ showSpinner: false, dialogVisible: true, dialog: dialog, isAGLoading: true, copyHeader: false, rowData: []}); + this.dialogType = "success"; + this.dialogHeader = "Success"; + this.showIcon = true; + this.dialogMsg = '['+newSUCount+'] Scheduling Units are created & ['+existingSUCount+'] Scheduling Units are updated successfully.'; + this.dialogContent = ""; + this.onCancel = this.close; + this.onClose = this.close; + this.callBackFunction = this.reset; + this.setState({isDirty : false, showSpinner: false, confirmDialogVisible: true, /*dialog: dialog,*/ isAGLoading: true, copyHeader: false, rowData: []}); } else { + this.setState({isDirty: false, showSpinner: false,}); this.growl.show({severity: 'error', summary: 'Warning', detail: 'No Scheduling Units create/update '}); } }catch(err){ @@ -1628,9 +1915,9 @@ export class SchedulingSetCreate extends Component { getBetweenStringValue(dates){ let returnDate = ''; if (dates){ - dates.forEach(utcDateArray =>{ - returnDate +=moment.utc(utcDateArray.from).format(DATE_TIME_FORMAT)+","; - returnDate +=moment.utc(utcDateArray.to).format(DATE_TIME_FORMAT)+"|"; + dates.forEach(utcDateArray => { + returnDate += moment.utc(utcDateArray.from).format(DATE_TIME_FORMAT)+","; + returnDate += moment.utc(utcDateArray.to).format(DATE_TIME_FORMAT)+"|"; }) } return returnDate; @@ -1656,17 +1943,18 @@ export class SchedulingSetCreate extends Component { return returnDate; } - /** * Refresh the grid with updated data */ async reset() { - let schedulingUnitList= await ScheduleService.getSchedulingBySet(this.state.selectedSchedulingSetId); + let schedulingUnitList = await ScheduleService.getSchedulingBySet(this.state.selectedSchedulingSetId); schedulingUnitList = _.filter(schedulingUnitList,{'observation_strategy_template_id': this.state.observStrategy.id}) ; this.setState({ schedulingUnitList: schedulingUnitList, - dialogVisible: false - }) + confirmDialogVisible: false, + isDirty: false + }); + this.isNewSet = false; await this.prepareScheduleUnitListForGrid(); this.state.gridApi.setRowData(this.state.rowData); this.state.gridApi.redrawRows(); @@ -1679,7 +1967,7 @@ export class SchedulingSetCreate extends Component { this.setState({redirect: '/schedulingunit'}); } - async onGridReady (params) { + async onGridReady (params) { await this.setState({ gridApi:params.api, gridColumnApi:params.columnApi, @@ -1687,9 +1975,17 @@ export class SchedulingSetCreate extends Component { this.state.gridApi.hideOverlay(); } + async onTopGridReady (params) { + await this.setState({ + topGridApi:params.api, + topGridColumnApi:params.columnApi, + }) + this.state.topGridApi.hideOverlay(); + } + async setNoOfSUint(value){ - this.setState({isAGLoading: true}); - if (value >= 0 && value < 501){ + this.setState({isDirty: true, isAGLoading: true}); + if (value >= 0 && value < 501){ await this.setState({ noOfSU: value }) @@ -1701,16 +1997,14 @@ export class SchedulingSetCreate extends Component { let noOfSU = this.state.noOfSU; this.tmpRowData = []; - let totalCount = this.state.totalCount; if (this.state.rowData && this.state.rowData.length >0 && this.state.emptyRow) { if (this.state.totalCount <= noOfSU) { - // set API data - for (var count = 0; count < totalCount; count++) { - this.tmpRowData.push(this.state.rowData[count]); - } - // add empty row - for(var i = this.state.totalCount; i < noOfSU; i++) { - this.tmpRowData.push(this.state.emptyRow); + for (var count = 0; count < noOfSU; count++) { + if(this.state.rowData.length > count ) { + this.tmpRowData.push(_.cloneDeep(this.state.rowData[count])); + } else { + this.tmpRowData.push(_.cloneDeep(this.state.emptyRow)); + } } this.setState({ rowData: this.tmpRowData, @@ -1770,10 +2064,6 @@ export class SchedulingSetCreate extends Component { return validForm; } - close(){ - this.setState({saveDialogVisible: false}) - } - /** * This function is mainly added for Unit Tests. If this function is removed Unit Tests will fail. */ @@ -1785,12 +2075,17 @@ export class SchedulingSetCreate extends Component { * Show the content in custom dialog */ showDialogContent(){ - return <> <br/>Invalid Rows:- Row # and Invalid columns <br/>{this.state.errorDisplay && this.state.errorDisplay.length>0 && - this.state.errorDisplay.map((msg, index) => ( - <React.Fragment key={index+10} > - <span key={'label1-'+ index}>{msg}</span> <br /> - </React.Fragment> - ))} </> + if (typeof this.state.errorDisplay === 'undefined' || this.state.errorDisplay.length === 0 ){ + return ""; + } + else { + return <> <br/>Invalid Rows:- Row # and Invalid columns <br/>{this.state.errorDisplay && this.state.errorDisplay.length>0 && + this.state.errorDisplay.map((msg, index) => ( + <React.Fragment key={index+10} > + <span key={'label1-'+ index}>{msg}</span> <br /> + </React.Fragment> + ))} </> + } } /** @@ -1801,8 +2096,8 @@ export class SchedulingSetCreate extends Component { if (!this.state.showDefault){ let tmpRowData = this.state.rowData; let defaultValueColumns = Object.keys(this.state.defaultCellValues); - await tmpRowData.forEach(rowData =>{ - defaultValueColumns.forEach(key =>{ + await tmpRowData.forEach(rowData => { + defaultValueColumns.forEach(key => { if(!this.isNotEmpty(rowData[key])){ rowData[key] = this.state.defaultCellValues[key]; } @@ -1820,6 +2115,220 @@ export class SchedulingSetCreate extends Component { } } + /** + * Reset the top table values + */ + resetCommonData(){ + let tmpData = [this.state.defaultCommonRowData]; //[...[this.state.emptyRow]]; + let gRowData = {}; + for (const key of _.keys(tmpData[0])) { + if (key === 'id') { + gRowData[key] = tmpData[0][key]; + } + else if(this.state.hasSameValue) { + gRowData['gdef_'+key] = tmpData[0][key]; + } else { + gRowData['gdef_'+key] = ''; + } + } + this.setState({commonRowData: [gRowData]}); + } + + /** + * Reload the data from API + */ + reload(){ + this.changeStrategy(this.state.observStrategy.id); + } + + /** + * Appliy the changes to all rows + */ + async applyToAll(){ + let isNotEmptyRow = true; + if (!this.state.applyEmptyValue) { + var row = this.state.commonRowData[0]; + Object.keys(row).forEach(key => { + if (key !== 'id' && row[key] !== '') { + isNotEmptyRow = false; + } + }); + } + if (!this.state.applyEmptyValue && isNotEmptyRow ) { + this.growl.show({severity: 'warn', summary: 'Warning', detail: 'Please enter value in the column(s) above to apply'}); + } else { + this.dialogType = "confirmation"; + this.dialogHeader = "Warning"; + this.showIcon = true; + this.dialogMsg = "Do you want to apply the above value(s) to all Scheduling Units?"; + this.dialogContent = ""; + this.callBackFunction = this.applyChanges; + this.applyToAllRow = true; + this.applyToEmptyRowOnly = false; + this.onClose = this.close; + this.onCancel =this.close; + this.setState({confirmDialogVisible: true}); + } + } + + /** + * Apply the changes to selected rows + */ + async applyToSelected(){ + let isNotEmptyRow = true; + let tmpRowData = this.state.gridApi.getSelectedRows(); + if (!this.state.applyEmptyValue) { + var row = this.state.commonRowData[0]; + Object.keys(row).forEach(key => { + if (key !== 'id' && row[key] !== '') { + isNotEmptyRow= false; + } + }); + } + if (!this.state.applyEmptyValue && isNotEmptyRow ) { + this.growl.show({severity: 'warn', summary: 'Warning', detail: 'Please enter value in the column(s) above to apply'}); + } else if(tmpRowData && tmpRowData.length === 0){ + this.growl.show({severity: 'warn', summary: 'Warning', detail: 'Please select at least one row to apply the changes'}); + } else { + this.showIcon = true; + this.dialogType = "confirmation"; + this.dialogHeader = "Warning"; + this.dialogMsg = "Do you want to apply the above value(s) to all selected Scheduling Unit(s) / row(s)?"; + this.dialogContent = ""; + this.applyToAllRow = false; + this.applyToEmptyRowOnly = false; + this.callBackFunction = this.applyChanges; + this.onClose = this.close; + this.onCancel = this.close; + this.setState({confirmDialogVisible: true}); + } + } + + /** + * Apply the changes to Empty rows + */ + async applyToEmptyRows(){ + let isNotEmptyRow = true; + if (!this.state.applyEmptyValue) { + var row = this.state.commonRowData[0]; + Object.keys(row).forEach(key => { + if (key !== 'id' && row[key] !== '') { + isNotEmptyRow= false; + } + }); + } + if (!this.state.applyEmptyValue && isNotEmptyRow ) { + this.growl.show({severity: 'warn', summary: 'Warning', detail: 'Please enter value in the column(s) above to apply'}); + } else { + this.showIcon = true; + this.dialogType = "confirmation"; + this.dialogHeader = "Warning"; + this.dialogMsg = "Do you want to apply the above value(s) to all empty rows?"; + this.dialogContent = ""; + this.applyToEmptyRowOnly = true; // Allows only empty to make changes + this.applyToAllRow = true; + this.callBackFunction = this.applyChanges; + this.onClose = this.close; + this.onCancel = this.close; + this.setState({confirmDialogVisible: true}); + } + } + + /** + * Make global changes in table data + */ + async applyChanges() { + await this.setState({ + confirmDialogVisible: false, + isDirty: true + }); + + let tmpRowData = []; + if (this.applyToAllRow) { + tmpRowData = this.state.rowData; + } + else { + tmpRowData = this.state.gridApi.getSelectedRows(); + } + var grow = this.state.commonRowData[0]; + if(tmpRowData.length >0) { + for( const row of tmpRowData) { + if (this.applyToEmptyRowOnly && (row['id'] > 0 || (row['suname'] !== '' && row['sudesc'] !== '') ) ){ + continue; + } + Object.keys(row).forEach(key => { + if (key !== 'id') { + let value = grow['gdef_'+key]; + if( this.state.applyEmptyValue) { + row[key] = value; + } + else { + row[key] = (_.isEmpty(value))? row[key] : value; + } + } + }); + } + this.state.gridApi.setRowData(this.state.rowData); + } + } + + /** + * Update isDirty when ever cell value updated in AG grid + * @param {*} params + */ + cellValueChageEvent(params) { + if( params.value && !_.isEqual(params.value, params.oldValue)) { + this.setState({isDirty: true}); + } + } + + /** + * warn before cancel the page if any changes detected + */ + checkIsDirty() { + if( this.state.isDirty ){ + this.showIcon = true; + this.dialogType = "confirmation"; + this.dialogHeader = "Add Multiple Scheduling Unit(s)"; + this.dialogMsg = "Do you want to leave this page? Your changes may not be saved."; + this.dialogContent = ""; + this.dialogHeight = '5em'; + this.callBackFunction = this.cancelCreate; + this.onClose = this.close; + this.onCancel = this.close; + this.setState({ + confirmDialogVisible: true, + }); + } else { + this.cancelCreate(); + } + } + + async refreshSchedulingSet(){ + this.schedulingSets = await ScheduleService.getSchedulingSets(); + const filteredSchedluingSets = _.filter(this.schedulingSets, {'project_id': this.state.schedulingUnit.project}); + this.setState({saveDialogVisible: false, confirmDialogVisible: false, schedulingSets: filteredSchedluingSets}); + } + + close(){ + this.setState({confirmDialogVisible: false}); + } + + showAddSchedulingSet() { + this.showIcon = false; + this.dialogType = "success"; + this.dialogHeader = "Add Scheduling Set’"; + this.dialogMsg = <SchedulingSet project={this.state.selectedProject[0]} onCancel={this.refreshSchedulingSet} />; + this.dialogContent = ""; + this.showIcon = false; + this.callBackFunction = this.refreshSchedulingSet; + this.onClose = this.refreshSchedulingSet; + this.onCancel = this.refreshSchedulingSet; + this.setState({ + confirmDialogVisible: true, + }); + } + render() { if (this.state.redirect) { return <Redirect to={ {pathname: this.state.redirect} }></Redirect> @@ -1827,8 +2336,8 @@ export class SchedulingSetCreate extends Component { return ( <React.Fragment> <Growl ref={(el) => this.growl = el} /> - <PageHeader location={this.props.location} title={'Scheduling Set - Add'} - actions={[{icon: 'fa-window-close',title:'Close', props:{pathname: '/schedulingunit' }}]} + <PageHeader location={this.props.location} title={'Scheduling Unit(s) Add Multiple'} + actions={[{icon: 'fa-window-close',title:'Close', type: 'button', actOn: 'click', props:{ callback: this.checkIsDirty }}]} /> { this.state.isLoading ? <AppLoader /> : <> @@ -1842,7 +2351,7 @@ export class SchedulingSetCreate extends Component { tooltip="Project" tooltipOptions={this.tooltipOptions} value={this.state.schedulingUnit.project} disabled={this.state.projectDisabled} options={this.projects} - onChange={(e) => {this.changeProject(e.value)}} + onChange={(e) => {this.onProjectChange(e.value)}} placeholder="Select Project" /> <label className={this.state.errors.project ?"error":"info"}> {this.state.errors.project ? this.state.errors.project : "Select Project to get Scheduling Sets"} @@ -1857,10 +2366,16 @@ export class SchedulingSetCreate extends Component { options={this.state.schedulingSets} onChange={(e) => {this.setSchedulingSetParams('scheduling_set_id',e.value)}} placeholder="Select Scheduling Set" /> + <Button label="" className="p-button-primary" icon="pi pi-plus" + onClick={this.showAddSchedulingSet} + tooltip="Add new Scheduling Set" + style={{bottom: '2em', left: '25em'}} + disabled={this.state.schedulingUnit.project !== null ? false : true }/> <label className={this.state.errors.scheduling_set_id ?"error":"info"}> {this.state.errors.scheduling_set_id ? this.state.errors.scheduling_set_id : "Scheduling Set of the Project"} </label> </div> + </div> <div className="p-field p-grid"> <label htmlFor="observStrategy" className="col-lg-2 col-md-2 col-sm-12">Observation Strategy <span style={{color:'red'}}>*</span></label> @@ -1869,7 +2384,7 @@ export class SchedulingSetCreate extends Component { tooltip="Observation Strategy Template to be used to create the Scheduling Unit" tooltipOptions={this.tooltipOptions} value={this.state.observStrategy.id} options={this.observStrategies} - onChange={(e) => {this.changeStrategy(e.value)}} + onChange={(e) => {this.onStrategyChange(e.value)}} placeholder="Select Strategy" /> <label className={this.state.errors.noOfSU ?"error":"info"}> {this.state.errors.noOfSU ? this.state.errors.noOfSU : "Select Observation Strategy"} @@ -1878,7 +2393,6 @@ export class SchedulingSetCreate extends Component { <div className="col-lg-1 col-md-1 col-sm-12"></div> <label htmlFor="schedSet" className="col-lg-2 col-md-2 col-sm-12">No of Scheduling Unit <span style={{color:'red'}}>*</span></label> <div className="col-lg-3 col-md-3 col-sm-12"> - <Dropdown editable options={this.state.noOfSUOptions} @@ -1906,37 +2420,90 @@ export class SchedulingSetCreate extends Component { /> </div> </div> - } + } + </div> <> - { this.state.isAGLoading ? <AppLoader /> : - <> - {this.state.observStrategy.id && - <div className="ag-theme-alpine" style={ {overflowX: 'inherit !importent', height: '500px', marginBottom: '10px' } } onKeyDown={this.clipboardEvent}> - <AgGridReact - suppressClipboardPaste={false} - columnDefs={this.state.columnDefs} - columnTypes={this.state.columnTypes} - defaultColDef={this.state.defaultColDef} - rowSelection={this.state.rowSelection} - onGridReady={this.onGridReady} - rowData={this.state.rowData} - frameworkComponents={this.state.frameworkComponents} - context={this.state.context} - components={this.state.components} - modules={this.state.modules} - enableRangeSelection={true} - rowSelection={this.state.rowSelection} - stopEditingWhenGridLosesFocus={true} - undoRedoCellEditing={true} - undoRedoCellEditingLimit={20} - exportDataAsCsv={true} - > - </AgGridReact> - </div> + { this.state.isAGLoading ? <AppLoader /> : + <> + {this.state.rowData && this.state.rowData.length > 0 && + <React.Fragment> + <Accordion onTabOpen={this.resetCommonData} style={{marginTop: '2em', marginBottom: '2em'}} > + <AccordionTab header={<React.Fragment><span style={{paddingLeft: '0.5em', paddingRight: '0.5em'}}>Input Values For Multiple Scheduling units</span> <i className="fas fa-clone"></i></React.Fragment>} > + <div className="ag-theme-alpine" style={ {overflowX: 'inherit !importent', height: '160px', marginBottom: '10px' } } onKeyDown={this.topAGGridEvent} > + <AgGridReact + suppressClipboardPaste={false} + columnDefs={this.state.globalColmunDef} + columnTypes={this.state.columnTypes} + defaultColDef={this.state.defaultColDef} + rowSelection={this.state.rowSelection} + onGridReady={this.onTopGridReady} + rowData={this.state.commonRowData} + frameworkComponents={this.state.frameworkComponents} + context={this.state.context} + components={this.state.components} + modules={this.state.modules} + enableRangeSelection={true} + > + </AgGridReact> + + </div> + <div className="p-grid p-justify-start" > + <label htmlFor="observStrategy" className="p-col-1" style={{width: '14em'}}>Include empty value(s)</label> + <Checkbox + tooltip="Copy the input value ( empty values also ) as it is while apply the changes in table" + tooltipOptions={this.tooltipOptions} + checked={this.state.applyEmptyValue} + onChange={e => this.setState({'applyEmptyValue': e.target.checked})} + style={{marginTop: '10px'}} > + </Checkbox> + + <div className="p-col-1" style={{width: 'auto' , marginLeft: '2em'}}> + <Button label="Apply to All Rows" tooltip="Apply changes to all rows in below table" className="p-button-primary" icon="fas fa-check-double" onClick={this.applyToAll}/> + </div> + <div className="p-col-1" style={{width: 'auto',marginLeft: '2em'}}> + <Button label="Apply to Selected Rows" tooltip="Apply changes to selected row in below table" className="p-button-primary" icon="fas fa-check-square" onClick={this.applyToSelected} /> + </div> + <div className="p-col-1" style={{width: 'auto',marginLeft: '2em'}}> + <Button label="Apply to Empty Rows" tooltip="Apply changes to empty row in below table" className="p-button-primary" icon="pi pi-check" onClick={this.applyToEmptyRows} /> + </div> + <div className="p-col-1" style={{width: 'auto',marginLeft: '2em'}}> + <Button label="Reset" tooltip="Reset input values" className="p-button-primary" icon="pi pi-refresh" onClick={this.resetCommonData} /> + </div> + {/*} <div className="p-col-1" style={{width: 'auto',marginLeft: '2em'}}> + <Button label="Refresh" tooltip="Refresh grid data" className="p-button-primary" icon="pi pi-refresh" onClick={this.reload} /> + </div> + */} + </div> + </AccordionTab> + </Accordion> + </React.Fragment> + } + + {this.state.observStrategy.id && + <div className="ag-theme-alpine" style={ {overflowX: 'inherit !importent', height: '500px', marginBottom: '3em', padding: '0.5em' } } onKeyDown={this.clipboardEvent}> + <label >Scheduling Unit(s) </label> + <AgGridReact + suppressClipboardPaste={false} + columnDefs={this.state.columnDefs} + columnTypes={this.state.columnTypes} + defaultColDef={this.state.defaultColDef} + rowSelection={this.state.rowSelection} + onGridReady={this.onGridReady} + rowData={this.state.rowData} + frameworkComponents={this.state.frameworkComponents} + context={this.state.context} + components={this.state.components} + modules={this.state.modules} + enableRangeSelection={true} + enableCellChangeFlash={true} + onCellValueChanged= {this.cellValueChageEvent} + > + </AgGridReact> + </div> + } + </> } - </> - } </> <div className="p-grid p-justify-start"> <div className="p-col-1"> @@ -1944,36 +2511,16 @@ export class SchedulingSetCreate extends Component { data-testid="save-btn" /> </div> <div className="p-col-1"> - <Button label="Cancel" className="p-button-danger" icon="pi pi-times" onClick={this.cancelCreate} /> + <Button label="Cancel" className="p-button-danger" icon="pi pi-times" onClick={this.checkIsDirty} /> </div> </div> - </div> </> } - - {/* Dialog component to show messages and get input */} - <div className="p-grid" data-testid="confirm_dialog"> - <Dialog header={this.state.dialog.header} visible={this.state.dialogVisible} style={{width: '25vw'}} inputId="confirm_dialog" - modal={true} onHide={() => {this.setState({dialogVisible: false})}} - footer={<div> - <Button key="back" onClick={this.reset} label="Close" /> - </div> - } > - <div className="p-grid"> - <div className="col-lg-2 col-md-2 col-sm-2" style={{margin: 'auto'}}> - <i className="pi pi-check-circle pi-large pi-success"></i> - </div> - <div className="col-lg-10 col-md-10 col-sm-10"> - {this.state.dialog.detail} - </div> - </div> - </Dialog> - </div> - - <CustomDialog type="confirmation" visible={this.state.saveDialogVisible} width="40vw" - header={'Save Scheduling Unit(s)'} message={' Some of the Scheduling Unit(s) has invalid data, Do you want to ignore and save valid Scheduling Unit(s) only?'} - content={this.showDialogContent} onClose={this.close} onCancel={this.close} onSubmit={this.saveSU}> + <CustomDialog type={this.dialogType} visible={this.state.confirmDialogVisible} width="40vw" height={this.dialogHeight} + header={this.dialogHeader} message={this.dialogMsg} + content={this.dialogContent} onClose={this.onClose} onCancel={this.onCancel} onSubmit={this.callBackFunction} + showIcon={this.showIcon} actions={this.actions}> </CustomDialog> <CustomPageSpinner visible={this.state.showSpinner} /> </React.Fragment> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/schedulingset.create.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/schedulingset.create.js new file mode 100644 index 0000000000000000000000000000000000000000..94f205629b8afec137bb323f4046d1c84a59db38 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/schedulingset.create.js @@ -0,0 +1,207 @@ +import React, { Component } from 'react'; +import { InputText } from 'primereact/inputtext'; +import { InputTextarea } from 'primereact/inputtextarea'; +import UIConstants from '../../utils/ui.constants'; +import { CustomDialog } from '../../layout/components/CustomDialog'; +import ScheduleService from '../../services/schedule.service'; +import { Growl } from 'primereact/components/growl/Growl'; + +export class SchedulingSet extends Component { + + constructor(props) { + super(props); + this.state= { + dialogVisible: true, + schedulingSet: { + project: (props.project) ? props.project.url : null, + name: null, + description: null, + }, + projectName: (props.project) ? props.project.name : null, + errors: [], + validFields: {}, + onCancel: (props.onCancel) ? props.onCancel: null, + actions: [ {id:"yes", title: 'Save', callback: this.saveSchedulingSet}, + {id:"no", title: 'Cancel', callback: this.props.onCancel} ] + }; + this.actions = [ {id:"yes", title: 'Save', callback: async ()=>{ + let schedulingSet = this.state.schedulingSet; + if (!this.isNotEmpty(schedulingSet.name) || !this.isNotEmpty(schedulingSet.description)){ + this.growl.show({severity: 'error', summary: 'Error Occured', detail: 'Name and Description are mandatory'}); + } else { + schedulingSet['generator_doc'] = {}; + schedulingSet['scheduling_unit_drafts'] = []; + const suSet = await ScheduleService.saveSchedulingSet(schedulingSet); + if (suSet.id !== null) { + this.growl.show({severity: 'success', summary: 'Success', detail: 'Scheduling Set is created successfully.'}); + this.setState({suSet: suSet, dialogVisible: true, }); + this.props.onCancel(); + } else { + this.growl.show({severity: 'error', summary: 'Error Occured', detail: schedulingSet.message || 'Unable to save Scheduling Set'}); + } + } + }}, + {id:"no", title: 'Cancel', callback: this.props.onCancel} ]; + + this.formRules = { // Form validation rules + name: {required: true, message: "Name can not be empty"}, + description: {required: true, message: "Description can not be empty"}, + project: {required: true, message: "Project can not be empty"}, + }; + + //this.validateForm = this.validateForm.bind(this); + this.saveSchedulingSet = this.saveSchedulingSet.bind(this); + this.close = this.close.bind(this); + this.isNotEmpty = this.isNotEmpty.bind(this); + } + + /** + * Validation function to validate the form or field based on the form rules. + * If no argument passed for fieldName, validates all fields in the form. + * @param {string} fieldName + */ + validateForm(fieldName) { + let validForm = false; + let errors = this.state.errors; + let validFields = this.state.validFields; + if (fieldName) { + delete errors[fieldName]; + delete validFields[fieldName]; + if (this.formRules[fieldName]) { + const rule = this.formRules[fieldName]; + const fieldValue = this.state.schedulingSet[fieldName]; + if (rule.required) { + if (!fieldValue) { + errors[fieldName] = rule.message?rule.message:`${fieldName} is required`; + } else { + validFields[fieldName] = true; + } + } + } + } /* else { + errors = {}; + validFields = {}; + for (const fieldName in this.formRules) { + const rule = this.formRules[fieldName]; + const fieldValue = this.state.schedulingSet[fieldName]; + if (rule.required) { + if (!fieldValue) { + errors[fieldName] = rule.message?rule.message:`${fieldName} is required`; + } else { + validFields[fieldName] = true; + } + } + } + }*/ + this.setState({errors: errors, validFields: validFields}); + if (Object.keys(validFields).length === Object.keys(this.formRules).length) { + validForm = true; + } + return validForm; + } + + /** + * Function to set form values to the SU Set object + * @param {string} key + * @param {object} value + */ + setSchedulingSetParams(key, value) { + this.tooltipOptions = UIConstants.tooltipOptions; + this.nameInput = React.createRef(); // Ref to Name field for auto focus + let schedulingSet = this.state.schedulingSet; + schedulingSet[key] = value; + let isValid = this.validateForm(key); + // isValid= this.validateForm('project'); + this.setState({schedulingSet: schedulingSet, validForm: isValid}); + } + + /** + * Create Scheduling Set + */ + async saveSchedulingSet(){ + let schedulingSet = this.state.schedulingSet; + schedulingSet['generator_doc'] = {}; + schedulingSet['scheduling_unit_drafts'] = []; + const suSet = await ScheduleService.saveSchedulingSet(schedulingSet); + if (suSet.id !== null) { + const dialog = {header: 'Success', detail: 'Scheduling Set is created successfully.'}; + this.setState({suSet: suSet, dialogVisible: false, dialog: dialog}); + } else { + this.growl.show({severity: 'error', summary: 'Error Occured', detail: schedulingSet.message || 'Unable to save Scheduling Set'}); + } + } + + close(){ + this.setState({dialogVisible: false}); + } + + /** + * Check is empty string + * @param {*} value + */ + isNotEmpty(value){ + if ( value === null || !value || value === 'undefined' || value.length === 0 ){ + return false; + } else { + return true; + } + } + render() { + return ( + <> + <Growl ref={(el) => this.growl = el} /> + <CustomDialog type="success" visible={this.state.dialogVisible} width="60vw" + header={'Add Scheduling Set'} + message= { + <React.Fragment> + <div className="p-fluid"> + <div className="p-field p-grid"> + <label htmlFor="project" className="col-lg-2 col-md-2 col-sm-12">Project</label> + <span className="col-lg-4 col-md-4 col-sm-12" style={{fontWeight: 'bold'}}>{this.state.projectName} </span> + <label className={(this.state.errors.project)?"error":"info"}> + {this.state.errors.project ? this.state.errors.project : ""} + </label> + </div> + </div> + <div className="col-lg-1 col-md-1 col-sm-12"></div> + <div className="p-fluid"> + <div className="p-field p-grid"> + <label htmlFor="project" className="col-lg-2 col-md-2 col-sm-12">Name <span style={{color:'red'}}>*</span></label> + <div className="col-lg-4 col-md-4 col-sm-12"> + <InputText className={(this.state.errors.name) ?'input-error':''} + id="suSetName" + tooltip="Enter name of the Scheduling Set" tooltipOptions={this.tooltipOptions} maxLength="128" + ref={input => {this.nameInput = input;}} + onChange={(e) => this.setSchedulingSetParams('name', e.target.value)} + onBlur={(e) => this.setSchedulingSetParams('name', e.target.value)} + value={this.state.schedulingSet.name} autoFocus + /> + <label className={(this.state.errors.name)?"error":"info"}> + {this.state.errors.name? this.state.errors.name : "Max 128 characters"} + </label> + </div> + + <label htmlFor="description" className="col-lg-2 col-md-2 col-sm-12">Description <span style={{color:'red'}}>*</span></label> + <div className="col-lg-4 col-md-4 col-sm-12"> + <InputTextarea className={(this.state.errors.description) ?'input-error':''} rows={3} cols={30} + tooltip="Longer description of the Scheduling Set" maxLength="128" + value={this.state.schedulingSet.description} + onChange={(e) => this.setSchedulingSetParams('description', e.target.value)} + onBlur={(e) => this.setSchedulingSetParams('description', e.target.value)} + /> + <label className={(this.state.errors.description) ?"error":"info"}> + {(this.state.errors.description) ? this.state.errors.description : "Max 255 characters"} + </label> + </div> + </div> + </div> + </React.Fragment>} + content={''} onClose={this.props.onCancel} onCancel={this.props.onCancel} onSubmit={this.saveSU} showAction={true} + actions={this.actions} + showIcon={false}> + </CustomDialog> + </> + ); + } +} +export default SchedulingSet; \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/edit.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/edit.js index 3cb5f2e8ac3b8c39edcfc6a4da138599190d6ca2..9384b5734904041783d4cae63c6c2f49466314aa 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/edit.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/edit.js @@ -1,13 +1,13 @@ -import React, {Component} from 'react'; +import React, { Component} from 'react'; import { Link, Redirect } from 'react-router-dom'; import _ from 'lodash'; -import {InputText} from 'primereact/inputtext'; -import {InputTextarea} from 'primereact/inputtextarea'; -import {Chips} from 'primereact/chips'; -import {Dropdown} from 'primereact/dropdown'; +import { InputText } from 'primereact/inputtext'; +import { InputTextarea } from 'primereact/inputtextarea'; +import { Chips } from 'primereact/chips'; +import { Dropdown } from 'primereact/dropdown'; import { Button } from 'primereact/button'; - +import { CustomDialog } from '../../layout/components/CustomDialog'; import Jeditor from '../../components/JSONEditor/JEditor'; import TaskService from '../../services/task.service'; @@ -21,6 +21,8 @@ export class TaskEdit extends Component { constructor(props) { super(props); this.state = { + showDialog: false, + isDirty: false, task: { name: "", created_at: null, @@ -47,6 +49,8 @@ export class TaskEdit extends Component { this.validateForm = this.validateForm.bind(this); this.saveTask = this.saveTask.bind(this); this.cancelEdit = this.cancelEdit.bind(this); + this.checkIsDirty = this.checkIsDirty.bind(this); + this.close = this.close.bind(this); } /** @@ -71,8 +75,14 @@ export class TaskEdit extends Component { */ setTaskParams(key, value) { let task = this.state.task; + const taskValue = this.state.task[key]; task[key] = value; - this.setState({task: task, validForm: this.validateForm()}); + if ( !this.state.isDirty && taskValue && !_.isEqual(taskValue, value) ) { + this.setState({task: task, validForm: this.validateForm(), isDirty: true}); + } else { + this.setState({task: task, validForm: this.validateForm()}); + } + } /** @@ -95,7 +105,7 @@ export class TaskEdit extends Component { task.specifications_template = template.url; this.setState({taskSchema: null}); - this.setState({task: task, taskSchema: template.schema}); + this.setState({task: task, taskSchema: template.schema, isDirty: true}); this.state.editorFunction(); } @@ -126,6 +136,7 @@ export class TaskEdit extends Component { * Function to call the servie and pass the values to save */ saveTask() { + this.setState({isDirty: false}); let task = this.state.task; task.specifications_doc = this.templateOutput[task.specifications_template_id]; // Remove read only properties from the object before sending to API @@ -138,6 +149,21 @@ export class TaskEdit extends Component { }); } + /** + * warn before cancel the page if any changes detected + */ + checkIsDirty() { + if( this.state.isDirty ){ + this.setState({showDialog: true}); + } else { + this.cancelEdit(); + } + } + + close() { + this.setState({showDialog: false}); + } + cancelEdit() { this.props.history.goBack(); } @@ -204,14 +230,16 @@ export class TaskEdit extends Component { </Link> </div> </div> */} - <PageHeader location={this.props.location} title={'Task - Edit'} actions={[{icon: 'fa-window-close',link: this.props.history.goBack,title:'Click to Close Task Edit Page' ,props : { pathname: `/task/view/draft/${this.state.task?this.state.task.id:''}`}}]}/> + <PageHeader location={this.props.location} title={'Task - Edit'} actions={[{icon: 'fa-window-close', + title:'Click to Close Task Edit Page', props : { pathname: `/task/view/draft/${this.state.task?this.state.task.id:''}`}}]}/> {isLoading ? <AppLoader/> : <div> <div className="p-fluid"> <div className="p-field p-grid"> <label htmlFor="taskName" className="col-lg-2 col-md-2 col-sm-12">Name <span style={{color:'red'}}>*</span></label> <div className="col-lg-4 col-md-4 col-sm-12"> - <InputText className={this.state.errors.name ?'input-error':''} id="taskName" type="text" value={this.state.task.name} onChange={(e) => this.setTaskParams('name', e.target.value)}/> + <InputText className={this.state.errors.name ?'input-error':''} id="taskName" type="text" value={this.state.task.name} + onChange={(e) => this.setTaskParams('name', e.target.value)}/> <label className="error"> {this.state.errors.name ? this.state.errors.name : ""} </label> @@ -276,8 +304,14 @@ export class TaskEdit extends Component { <Button label="Save" className="p-button-primary" icon="pi pi-check" onClick={this.saveTask} disabled={!this.state.validEditor || !this.state.validForm} /> </div> <div className="p-col-1"> - <Button label="Cancel" className="p-button-danger" icon="pi pi-times" onClick={this.cancelEdit} /> + <Button label="Cancel" className="p-button-danger" icon="pi pi-times" onClick={this.checkIsDirty} /> + </div> </div> + <div className="p-grid" data-testid="confirm_dialog"> + <CustomDialog type="confirmation" visible={this.state.showDialog} width="40vw" + header={'Edit Task'} message={'Do you want to leave this page? Your changes may not be saved.'} + content={''} onClose={this.close} onCancel={this.close} onSubmit={this.cancelEdit}> + </CustomDialog> </div> </React.Fragment> ); diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/reservation.create.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/reservation.create.js index e95f7a29fd922f3b7ee9ce6ca29a45eea45f4bd3..73856d5af49e1aecdb35d1bd1d94374298352022 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/reservation.create.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/reservation.create.js @@ -1,17 +1,18 @@ -import React, {Component} from 'react'; +import React, { Component } from 'react'; import { Redirect } from 'react-router-dom'; - -import {Growl} from 'primereact/components/growl/Growl'; +import _ from 'lodash'; +import { Growl } from 'primereact/components/growl/Growl'; import AppLoader from '../../layout/components/AppLoader'; import PageHeader from '../../layout/components/PageHeader'; import UIConstants from '../../utils/ui.constants'; -import {Calendar} from 'primereact/calendar'; +import { Calendar } from 'primereact/calendar'; import { InputMask } from 'primereact/inputmask'; -import {Dropdown} from 'primereact/dropdown'; -import {InputText} from 'primereact/inputtext'; -import {InputTextarea} from 'primereact/inputtextarea'; +import { Dropdown } from 'primereact/dropdown'; +import {InputText } from 'primereact/inputtext'; +import { InputTextarea } from 'primereact/inputtextarea'; import { Button } from 'primereact/button'; -import {Dialog} from 'primereact/components/dialog/Dialog'; +import { Dialog } from 'primereact/components/dialog/Dialog'; +import { CustomDialog } from '../../layout/components/CustomDialog'; import ProjectService from '../../services/project.service'; import ReservationService from '../../services/reservation.service'; import UnitService from '../../utils/unit.converter'; @@ -24,6 +25,8 @@ export class ReservationCreate extends Component { constructor(props) { super(props); this.state= { + showDialog: false, + isDirty: false, isLoading: true, redirect: null, paramsSchema: null, // JSON Schema to be generated from strategy template to pass to JSON editor @@ -59,6 +62,8 @@ export class ReservationCreate extends Component { this.saveReservation = this.saveReservation.bind(this); this.reset = this.reset.bind(this); this.cancelCreate = this.cancelCreate.bind(this); + this.checkIsDirty = this.checkIsDirty.bind(this); + this.close = this.close.bind(this); this.initReservation = this.initReservation.bind(this); } @@ -100,13 +105,21 @@ export class ReservationCreate extends Component { * @param {object} value */ setReservationParams(key, value) { - - let reservation = this.state.reservation; + let reservation = _.cloneDeep(this.state.reservation); reservation[key] = value; - this.setState({reservation: reservation, validForm: this.validateForm(key), validEditor: this.validateEditor(),touched: { - ...this.state.touched, - [key]: true - }}); + if ( !this.state.isDirty && !_.isEqual(this.state.reservation, reservation) ) { + this.setState({reservation: reservation, validForm: this.validateForm(key), validEditor: this.validateEditor(), touched: { + ...this.state.touched, + [key]: true + }, isDirty: true}); + } else { + this.setState({reservation: reservation, validForm: this.validateForm(key), validEditor: this.validateEditor(),touched: { + ...this.state.touched, + [key]: true + }}); + } + + } /** @@ -124,7 +137,8 @@ export class ReservationCreate extends Component { setParams(key, value, type) { if(key === 'duration' && !this.validateDuration( value)) { this.setState({ - durationError: true + durationError: true, + isDirty: true }) return; } @@ -139,7 +153,7 @@ export class ReservationCreate extends Component { break; } } - this.setState({reservation: reservation, validForm: this.validateForm(key), durationError: false}); + this.setState({reservation: reservation, validForm: this.validateForm(key), durationError: false, isDirty: true}); } /** @@ -205,9 +219,16 @@ export class ReservationCreate extends Component { setEditorOutput(jsonOutput, errors) { this.paramsOutput = jsonOutput; this.validEditor = errors.length === 0; - this.setState({ paramsOutput: jsonOutput, - validEditor: errors.length === 0, - validForm: this.validateForm()}); + if ( !this.state.isDirty && this.state.paramsOutput && !_.isEqual(this.state.paramsOutput, jsonOutput) ) { + this.setState({ paramsOutput: jsonOutput, + validEditor: errors.length === 0, + validForm: this.validateForm(), + isDirty: true}); + } else { + this.setState({ paramsOutput: jsonOutput, + validEditor: errors.length === 0, + validForm: this.validateForm()}); + } } saveReservation(){ @@ -220,9 +241,9 @@ export class ReservationCreate extends Component { reservation = ReservationService.saveReservation(reservation); if (reservation && reservation !== null){ const dialog = {header: 'Success', detail: 'Reservation is created successfully. Do you want to create another Reservation?'}; - this.setState({ dialogVisible: true, dialog: dialog, paramsOutput: {}}) + this.setState({ dialogVisible: true, dialog: dialog, paramsOutput: {}, showDialog: false, isDirty: false}) } else { - this.growl.show({severity: 'error', summary: 'Error Occured', detail: 'Unable to save Reservation'}); + this.growl.show({severity: 'error', summary: 'Error Occured', detail: 'Unable to save Reservation', showDialog: false, isDirty: false}); } } @@ -248,6 +269,8 @@ export class ReservationCreate extends Component { validFields: {}, touched:false, stationGroup: [], + showDialog: false, + isDirty: false }); this.initReservation(); } @@ -259,6 +282,21 @@ export class ReservationCreate extends Component { this.props.history.goBack(); } + /** + * warn before cancel the page if any changes detected + */ + checkIsDirty() { + if( this.state.isDirty ){ + this.setState({showDialog: true}); + } else { + this.cancelCreate(); + } + } + + close() { + this.setState({showDialog: false}); + } + render() { if (this.state.redirect) { return <Redirect to={ {pathname: this.state.redirect} }></Redirect> @@ -278,7 +316,8 @@ export class ReservationCreate extends Component { <React.Fragment> <Growl ref={(el) => this.growl = el} /> <PageHeader location={this.props.location} title={'Reservation - Add'} - actions={[{icon: 'fa-window-close' ,title:'Click to close Reservation creation', props : { pathname: `/su/timelineview/reservation/reservation/list`}}]}/> + actions={[{icon: 'fa-window-close' ,title:'Click to close Reservation creation', + type: 'button', actOn: 'click', props:{ callback: this.checkIsDirty }}]}/> { this.state.isLoading ? <AppLoader /> : <> <div> @@ -377,7 +416,7 @@ export class ReservationCreate extends Component { disabled={!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.cancelCreate} /> + <Button label="Cancel" className="p-button-danger" icon="pi pi-times" onClick={this.checkIsDirty} /> </div> </div> </div> @@ -402,6 +441,11 @@ export class ReservationCreate extends Component { </div> </div> </Dialog> + + <CustomDialog type="confirmation" visible={this.state.showDialog} width="40vw" + header={'Add Reservation'} message={'Do you want to leave this page? Your changes may not be saved.'} + content={''} onClose={this.close} onCancel={this.close} onSubmit={this.cancelCreate}> + </CustomDialog> </div> </React.Fragment> ); 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 f380d527ea32182515a720c017e4126177289f8d..3e4317e965ad5d93f6dd2e6ffa6489706a77ae6b 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/view.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/view.js @@ -2,6 +2,7 @@ import React, {Component} from 'react'; import { Redirect } from 'react-router-dom/cjs/react-router-dom.min'; import moment from 'moment'; import _ from 'lodash'; +import Websocket from 'react-websocket'; // import SplitPane, { Pane } from 'react-split-pane'; import {InputSwitch} from 'primereact/inputswitch'; @@ -17,6 +18,7 @@ import UtilService from '../../services/util.service'; import TaskService from '../../services/task.service'; import UnitConverter from '../../utils/unit.converter'; +import Validator from '../../utils/validator'; import SchedulingUnitSummary from '../Scheduling/summary'; import { Dropdown } from 'primereact/dropdown'; import { OverlayPanel } from 'primereact/overlaypanel'; @@ -86,6 +88,10 @@ export class TimelineView extends Component { this.resizeSUList = this.resizeSUList.bind(this); this.suListFilterCallback = this.suListFilterCallback.bind(this); this.addStationReservations = this.addStationReservations.bind(this); + this.handleData = this.handleData.bind(this); + this.addNewData = this.addNewData.bind(this); + this.updateExistingData = this.updateExistingData.bind(this); + this.updateSchedulingUnit = this.updateSchedulingUnit.bind(this); } async componentDidMount() { @@ -185,7 +191,8 @@ export class TimelineView extends Component { getTimelineItem(suBlueprint) { let antennaSet = ""; for (let task of suBlueprint.tasks) { - if (task.specifications_template.type_value.toLowerCase() === "observation") { + if (task.specifications_template.type_value.toLowerCase() === "observation" + && task.specifications_doc.antenna_set) { antennaSet = task.specifications_doc.antenna_set; } } @@ -441,7 +448,8 @@ export class TimelineView extends Component { currentStartTime: startTime, currentEndTime: endTime}); // On range change close the Details pane // this.closeSUDets(); - return {group: this.stationView?this.allStationsGroup:_.orderBy(group,["parent", "id"], ['asc', 'desc']), items: items}; + // console.log(_.orderBy(group, ["parent", "id"], ['asc', 'desc'])); + return {group: this.stationView?this.allStationsGroup:_.orderBy(_.uniqBy(group, 'id'),["parent", "start"], ['asc', 'asc']), items: items}; } /** @@ -662,6 +670,153 @@ export class TimelineView extends Component { } } } + + /** + * Function to call wnen websocket is connected + */ + onConnect() { + console.log("WS Opened") + } + + /** + * Function to call when websocket is disconnected + */ + onDisconnect() { + console.log("WS Closed") + } + + /** + * Handles the message received through websocket + * @param {String} data - String of JSON data + */ + handleData(data) { + if (data) { + const jsonData = JSON.parse(data); + if (jsonData.action === 'create') { + this.addNewData(jsonData.object_details.id, jsonData.object_type, jsonData.object_details); + } else if (jsonData.action === 'update') { + this.updateExistingData(jsonData.object_details.id, jsonData.object_type, jsonData.object_details); + } + } + } + + /** + * If any new object that is relevant to the timeline view, load the data to the existing state variable. + * @param {Number} id - id of the object created + * @param {String} type - model name of the object like scheduling_unit_draft, scheduling_unit_blueprint, task_blueprint, etc., + * @param {Object} object - model object with certain properties + */ + addNewData(id, type, object) { + switch(type) { + /* When a new scheduling_unit_draft is created, it should be added to the existing list of suDraft. */ + case 'scheduling_unit_draft': { + this.updateSUDraft(id); + // let suDrafts = this.state.suDrafts; + // let suSets = this.state.suSets; + // ScheduleService.getSchedulingUnitDraftById(id) + // .then(suDraft => { + // suDrafts.push(suDraft); + // _.remove(suSets, function(suSet) { return suSet.id === suDraft.scheduling_set_id}); + // suSets.push(suDraft.scheduling_set_object); + // this.setState({suSet: suSets, suDrafts: suDrafts}); + // }); + break; + } + case 'scheduling_unit_blueprint': { + this.updateSchedulingUnit(id); + break; + } + case 'task_blueprint': { + // this.updateSchedulingUnit(object.scheduling_unit_blueprint_id); + break; + } + default: { break; } + } + } + + /** + * If any if the given properties of the object is modified, update the schedulingUnit object in the list of the state. + * It is validated for both scheduling_unit_blueprint and task_blueprint objects + * @param {Number} id + * @param {String} type + * @param {Object} object + */ + updateExistingData(id, type, object) { + const objectProps = ['status', 'start_time', 'stop_time', 'duration']; + switch(type) { + case 'scheduling_unit_draft': { + this.updateSUDraft(id); + // let suDrafts = this.state.suDrafts; + // _.remove(suDrafts, function(suDraft) { return suDraft.id === id}); + // suDrafts.push(object); + // this.setState({suDrafts: suDrafts}); + break; + } + case 'scheduling_unit_blueprint': { + let suBlueprints = this.state.suBlueprints; + let existingSUB = _.find(suBlueprints, ['id', id]); + if (Validator.isObjectModified(existingSUB, object, objectProps)) { + this.updateSchedulingUnit(id); + } + break; + } + case 'task_blueprint': { + // let suBlueprints = this.state.suBlueprints; + // let existingSUB = _.find(suBlueprints, ['id', object.scheduling_unit_blueprint_id]); + // let existingTask = _.find(existingSUB.tasks, ['id', id]); + // if (Validator.isObjectModified(existingTask, object, objectProps)) { + // this.updateSchedulingUnit(object.scheduling_unit_blueprint_id); + // } + break; + } + default: { break;} + } + } + + /** + * Add or update the SUDraft object in the state suDraft list after fetching through API call + * @param {Number} id + */ + updateSUDraft(id) { + let suDrafts = this.state.suDrafts; + let suSets = this.state.suSets; + ScheduleService.getSchedulingUnitDraftById(id) + .then(suDraft => { + _.remove(suDrafts, function(suDraft) { return suDraft.id === id}); + suDrafts.push(suDraft); + _.remove(suSets, function(suSet) { return suSet.id === suDraft.scheduling_set_id}); + suSets.push(suDraft.scheduling_set_object); + this.setState({suSet: suSets, suDrafts: suDrafts}); + }); + } + + /** + * Fetch the latest SUB object from the backend and format as required for the timeline and pass them to the timeline component + * to update the timeline view with latest data. + * @param {Number} id + */ + updateSchedulingUnit(id) { + ScheduleService.getSchedulingUnitExtended('blueprint', id, true) + .then(suBlueprint => { + const suDraft = _.find(this.state.suDrafts, ['id', suBlueprint.draft_id]); + const suSet = this.state.suSets.find((suSet) => { return suDraft.scheduling_set_id===suSet.id}); + const project = this.state.projects.find((project) => { return suSet.project_id===project.name}); + let suBlueprints = this.state.suBlueprints; + suBlueprint['actionpath'] = `/schedulingunit/view/blueprint/${id}`; + suBlueprint.suDraft = suDraft; + suBlueprint.project = project.name; + suBlueprint.suSet = suSet; + suBlueprint.durationInSec = suBlueprint.duration; + suBlueprint.duration = UnitConverter.getSecsToHHmmss(suBlueprint.duration); + suBlueprint.tasks = suBlueprint.task_blueprints; + _.remove(suBlueprints, function(suB) { return suB.id === id}); + suBlueprints.push(suBlueprint); + // Set updated suBlueprints in the state and call the dateRangeCallback to create the timeline group and items + this.setState({suBlueprints: suBlueprints}); + this.dateRangeCallback(this.state.currentStartTime, this.state.currentEndTime); + }); + } + render() { if (this.state.redirect) { return <Redirect to={ {pathname: this.state.redirect} }></Redirect> @@ -821,6 +976,8 @@ export class TimelineView extends Component { </div> } </OverlayPanel> + {!this.state.isLoading && + <Websocket url={process.env.REACT_APP_WEBSOCKET_URL} onOpen={this.onConnect} onMessage={this.handleData} onClose={this.onDisconnect} /> } </React.Fragment> ); } 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 6e595e550a3a146868b9f05d1122937fc113efa7..75b8b2ca961f1a476c01a7cb3f391fe37c431f25 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 @@ -2,6 +2,7 @@ import React, {Component} from 'react'; import { Redirect } from 'react-router-dom/cjs/react-router-dom.min'; import moment from 'moment'; import _ from 'lodash'; +import Websocket from 'react-websocket'; // import SplitPane, { Pane } from 'react-split-pane'; // import { Dropdown } from 'primereact/dropdown'; @@ -17,6 +18,7 @@ import UtilService from '../../services/util.service'; import TaskService from '../../services/task.service'; import UnitConverter from '../../utils/unit.converter'; +import Validator from '../../utils/validator'; import SchedulingUnitSummary from '../Scheduling/summary'; import UIConstants from '../../utils/ui.constants'; import { OverlayPanel } from 'primereact/overlaypanel'; @@ -66,6 +68,10 @@ export class WeekTimelineView extends Component { this.dateRangeCallback = this.dateRangeCallback.bind(this); this.resizeSUList = this.resizeSUList.bind(this); this.suListFilterCallback = this.suListFilterCallback.bind(this); + this.handleData = this.handleData.bind(this); + this.addNewData = this.addNewData.bind(this); + this.updateExistingData = this.updateExistingData.bind(this); + this.updateSchedulingUnit = this.updateSchedulingUnit.bind(this); } async componentDidMount() { @@ -167,7 +173,8 @@ export class WeekTimelineView extends Component { async getTimelineItem(suBlueprint, displayDate) { let antennaSet = ""; for (let task of suBlueprint.tasks) { - if (task.specifications_template.type_value.toLowerCase() === "observation") { + if (task.specifications_template.type_value.toLowerCase() === "observation" + && task.specifications_doc.antenna_set) { antennaSet = task.specifications_doc.antenna_set; } } @@ -176,7 +183,7 @@ export class WeekTimelineView extends Component { group: moment.utc(suBlueprint.start_time).format("MMM DD ddd"), title: "", project: suBlueprint.project, - name: suBlueprint.suDraft.name, + name: suBlueprint.name, band: antennaSet?antennaSet.split("_")[0]:"", antennaSet: antennaSet, duration: suBlueprint.durationInSec?`${(suBlueprint.durationInSec/3600).toFixed(2)}Hrs`:"", @@ -216,7 +223,8 @@ export class WeekTimelineView extends Component { //Control Task ID const subTaskIds = (task.subTasks || []).filter(sTask => sTask.subTaskTemplate.name.indexOf('control') > 1); task.subTaskID = subTaskIds.length ? subTaskIds[0].id : ''; - if (task.template.type_value.toLowerCase() === "observation") { + if (task.template.type_value.toLowerCase() === "observation" + && task.specifications_doc.antenna_set) { task.antenna_set = task.specifications_doc.antenna_set; task.band = task.specifications_doc.filter; } @@ -270,6 +278,8 @@ export class WeekTimelineView extends Component { counts = counts.concat(itemStationGroups[stationgroup].length); item.stations.groups = groups; item.stations.counts = counts; + item.suStartTime = moment.utc(itemSU.start_time); + item.suStopTime = moment.utc(itemSU.stop_time); } this.popOver.toggle(evt); this.setState({mouseOverItem: item}); @@ -430,6 +440,144 @@ export class WeekTimelineView extends Component { } } + /** + * Function to call wnen websocket is connected + */ + onConnect() { + console.log("WS Opened") + } + + /** + * Function to call when websocket is disconnected + */ + onDisconnect() { + console.log("WS Closed") + } + + /** + * Handles the message received through websocket + * @param {String} data - String of JSON data + */ + handleData(data) { + if (data) { + const jsonData = JSON.parse(data); + if (jsonData.action === 'create') { + this.addNewData(jsonData.object_details.id, jsonData.object_type, jsonData.object_details); + } else if (jsonData.action === 'update') { + this.updateExistingData(jsonData.object_details.id, jsonData.object_type, jsonData.object_details); + } + } + } + + /** + * If any new object that is relevant to the timeline view, load the data to the existing state variable. + * @param {Number} id - id of the object created + * @param {String} type - model name of the object like scheduling_unit_draft, scheduling_unit_blueprint, task_blueprint, etc., + * @param {Object} object - model object with certain properties + */ + addNewData(id, type, object) { + switch(type) { + /* When a new scheduling_unit_draft is created, it should be added to the existing list of suDraft. */ + case 'scheduling_unit_draft': { + let suDrafts = this.state.suDrafts; + let suSets = this.state.suSets; + ScheduleService.getSchedulingUnitDraftById(id) + .then(suDraft => { + suDrafts.push(suDraft); + _.remove(suSets, function(suSet) { return suSet.id === suDraft.scheduling_set_id}); + suSets.push(suDraft.scheduling_set_object); + this.setState({suSet: suSets, suDrafts: suDrafts}); + }); + break; + } + case 'scheduling_unit_blueprint': { + this.updateSchedulingUnit(id); + break; + } + case 'task_blueprint': { + // this.updateSchedulingUnit(object.scheduling_unit_blueprint_id); + break; + } + default: { break; } + } + } + + /** + * If any if the given properties of the object is modified, update the schedulingUnit object in the list of the state. + * It is validated for both scheduling_unit_blueprint and task_blueprint objects + * @param {Number} id + * @param {String} type + * @param {Object} object + */ + updateExistingData(id, type, object) { + const objectProps = ['status', 'start_time', 'stop_time', 'duration']; + switch(type) { + case 'scheduling_unit_blueprint': { + let suBlueprints = this.state.suBlueprints; + let existingSUB = _.find(suBlueprints, ['id', id]); + if (Validator.isObjectModified(existingSUB, object, objectProps)) { + this.updateSchedulingUnit(id); + } + break; + } + case 'task_blueprint': { + // let suBlueprints = this.state.suBlueprints; + // let existingSUB = _.find(suBlueprints, ['id', object.scheduling_unit_blueprint_id]); + // let existingTask = _.find(existingSUB.tasks, ['id', id]); + // if (Validator.isObjectModified(existingTask, object, objectProps)) { + // this.updateSchedulingUnit(object.scheduling_unit_blueprint_id); + // } + break; + } + default: { break;} + } + } + + /** + * Fetch the latest SUB object from the backend and format as required for the timeline and pass them to the timeline component + * to update the timeline view with latest data. + * @param {Number} id + */ + updateSchedulingUnit(id) { + ScheduleService.getSchedulingUnitExtended('blueprint', id, true) + .then(async(suBlueprint) => { + const suDraft = _.find(this.state.suDrafts, ['id', suBlueprint.draft_id]); + const suSet = this.state.suSets.find((suSet) => { return suDraft.scheduling_set_id===suSet.id}); + const project = this.state.projects.find((project) => { return suSet.project_id===project.name}); + let suBlueprints = this.state.suBlueprints; + suBlueprint['actionpath'] = `/schedulingunit/view/blueprint/${id}`; + suBlueprint.suDraft = suDraft; + suBlueprint.project = project.name; + suBlueprint.suSet = suSet; + suBlueprint.durationInSec = suBlueprint.duration; + suBlueprint.duration = UnitConverter.getSecsToHHmmss(suBlueprint.duration); + suBlueprint.tasks = suBlueprint.task_blueprints; + // Add Subtask Id as control id for task if subtask type us control. Also add antenna_set & band prpoerties to the task object. + for (let task of suBlueprint.tasks) { + const subTaskIds = task.subtasks.filter(subtask => { + const template = _.find(this.subtaskTemplates, ['id', subtask.specifications_template_id]); + return (template && template.name.indexOf('control')) > 0; + }); + task.subTaskID = subTaskIds.length ? subTaskIds[0].id : ''; + if (task.specifications_template.type_value.toLowerCase() === "observation" + && task.specifications_doc.antenna_set) { + task.antenna_set = task.specifications_doc.antenna_set; + task.band = task.specifications_doc.filter; + } + } + // Get stations involved for this SUB + let stations = this.getSUStations(suBlueprint); + suBlueprint.stations = _.uniq(stations); + // Remove the old SUB object from the existing list and add the newly fetched SUB + _.remove(suBlueprints, function(suB) { return suB.id === id}); + suBlueprints.push(suBlueprint); + this.setState({suBlueprints: suBlueprints}); + // Create timeline group and items + let updatedItemGroupData = await this.dateRangeCallback(this.state.startTime, this.state.endTime, true); + this.timeline.updateTimeline(updatedItemGroupData); + }); + } + render() { if (this.state.redirect) { return <Redirect to={ {pathname: this.state.redirect} }></Redirect> @@ -539,9 +687,9 @@ export class WeekTimelineView extends Component { <label className={`col-5 su-${mouseOverItem.status}-icon`}>Friends:</label> <div className="col-7">{mouseOverItem.friends?mouseOverItem.friends:"-"}</div> <label className={`col-5 su-${mouseOverItem.status}-icon`}>Start Time:</label> - <div className="col-7">{mouseOverItem.start_time.format("YYYY-MM-DD HH:mm:ss")}</div> + <div className="col-7">{mouseOverItem.suStartTime.format("YYYY-MM-DD HH:mm:ss")}</div> <label className={`col-5 su-${mouseOverItem.status}-icon`}>End Time:</label> - <div className="col-7">{mouseOverItem.end_time.format("YYYY-MM-DD HH:mm:ss")}</div> + <div className="col-7">{mouseOverItem.suStopTime.format("YYYY-MM-DD HH:mm:ss")}</div> <label className={`col-5 su-${mouseOverItem.status}-icon`}>Antenna Set:</label> <div className="col-7">{mouseOverItem.antennaSet}</div> <label className={`col-5 su-${mouseOverItem.status}-icon`}>Stations:</label> @@ -553,6 +701,9 @@ export class WeekTimelineView extends Component { </div> } </OverlayPanel> + {/* Open Websocket after loading all initial data */} + {!this.state.isLoading && + <Websocket url={process.env.REACT_APP_WEBSOCKET_URL} onOpen={this.onConnect} onMessage={this.handleData} onClose={this.onDisconnect} /> } </React.Fragment> ); } diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/Scheduled.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/Scheduled.js index 5e1cad1b4c4994b239b627675ee6f85e5fc04891..6eda2278a31979af8b1c897cbb695317122d9332 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/Scheduled.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/Scheduled.js @@ -11,7 +11,7 @@ class Scheduled extends Component { } /** - * Method will trigger on click save buton + * Method will trigger on click Next buton * here onNext props coming from parent, where will handle redirection to other page */ Next() { @@ -49,10 +49,8 @@ class Scheduled extends Component { <div className="p-grid p-justify-start"> <div className="p-col-1"> - <Button label="Next" className="p-button-primary" icon="pi pi-check" onClick={ this.Next } /> - </div> - <div className="p-col-1"> - <Button label="Cancel" className="p-button-danger" icon="pi pi-times" style={{ width: '90px' }} /> + <Button label="Cancel" className="p-button-danger" icon="pi pi-times" style={{ width: '90px' }} + onClick={(e) => {this.props.onCancel()}} /> </div> </div> </div> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/decide.acceptance.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/decide.acceptance.js index 7087513f513adf2350dc8fd911b7d9c7953da671..c19c652a1a14fb5910cd130398816f800bf14d37 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/decide.acceptance.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/decide.acceptance.js @@ -3,54 +3,67 @@ import { Button } from 'primereact/button'; import SunEditor from 'suneditor-react'; import 'suneditor/dist/css/suneditor.min.css'; // Import Sun Editor's CSS File import { Checkbox } from 'primereact/checkbox'; +import WorkflowService from '../../services/workflow.service'; class DecideAcceptance extends Component { constructor(props) { super(props); this.state = { - content: props.report, - picomment: props.picomment, //PI Comment Field - showEditor: false, //Sun Editor - checked: false, //Checkbox - + content: '', + comment: '', + showEditor: false, + sos_accept_after_pi: false }; this.Next = this.Next.bind(this); this.handleChange = this.handleChange.bind(this); this.onChangePIComment = this.onChangePIComment.bind(this); } - - // Method will trigger on change of operator report sun-editor - handleChange(e) { + async componentDidMount() { + const qaSOSResponse = await WorkflowService.getQAReportingSOS(this.props.process.qa_reporting_sos); + const piVerificationResponse = await WorkflowService.getQAPIverification(this.props.process.pi_verification); this.setState({ - content: e + content: qaSOSResponse.sos_report, + comment: piVerificationResponse.pi_report }); - localStorage.setItem('report_qa', e); } - //PI Comment Editor - onChangePIComment(e) { - this.setState({ - picomment: e.target.value - }); - localStorage.setItem('pi_comment', e.target.value); + // Method will trigger on change of operator report sun-editor + handleChange(e) { + if (e === '<p><br></p>') { + this.setState({ content: '' }); + return; + } + this.setState({ content: e }); } - /** - * Method will trigger on click save buton - * here onNext props coming from parent, where will handle redirection to other page - */ - Next() { - this.props.onNext({ - report: this.state.content, - picomment: this.state.picomment - + async Next() { + const currentWorkflowTask = await this.props.getCurrentTaskDetails(); + const promise = []; + if (currentWorkflowTask && !currentWorkflowTask.fields.owner) { + promise.push(WorkflowService.updateAssignTo(currentWorkflowTask.pk, { owner: this.state.assignTo })); + } + promise.push(WorkflowService.updateQA_Perform(this.props.id, {"sos_accept_after_pi":this.state.sos_accept_after_pi})); + Promise.all(promise).then((responses) => { + if (responses.indexOf(null)<0) { + this.props.onNext({ report: this.state.content , pireport: this.state.comment}); + } else { + this.props.onError(); + } }); + } + - // Not using at present - cancelCreate() { - this.props.history.goBack(); + //PI Comment Editor + onChangePIComment(a) { + if (a === '<p><br></p>') { + localStorage.setItem('comment_pi', ''); + this.setState({ comment: '' }); + return; + } + this.setState({comment: a}); + localStorage.setItem('comment_pi', a); } render() { @@ -78,7 +91,7 @@ class DecideAcceptance extends Component { <div className="col-lg-12 col-md-12 col-sm-12"> {this.state.showEditor && <SunEditor setDefaultStyle="min-height: 250px; height: auto;" enableToolbar={true} onChange={this.onChangePIComment} - setContents={this.state.picomment} + setContents={this.state.comment} setOptions={{ buttonList: [ ['undo', 'redo', 'bold', 'underline', 'fontColor', 'table', 'link', 'image', 'video', 'italic', 'strike', 'subscript', @@ -86,24 +99,25 @@ class DecideAcceptance extends Component { ] }} />} - <div className="operator-report" dangerouslySetInnerHTML={{ __html: this.state.picomment }}></div> + <div className="pi-report" dangerouslySetInnerHTML={{ __html: this.state.comment }}></div> </div> </div> <div className="p-field p-grid"> <label htmlFor="piAccept" className="col-lg-2 col-md-2 col-sm-12">SDCO accepts after PI</label> <div className="col-lg-3 col-md-3 col-sm-6"> <div className="p-field-checkbox"> - <Checkbox inputId="binary" checked={this.state.checked} onChange={e => this.setState({ checked: e.checked })} /> + <Checkbox inputId="binary" checked={this.state.sos_accept_after_pi} onChange={e => this.setState({ sos_accept_after_pi: e.checked })} /> </div> </div> </div> </div> <div className="p-grid" style={{ marginTop: '20px' }}> <div className="p-col-1"> - <Button label="Next" className="p-button-primary" icon="pi pi-check" onClick = { this.Next } /> + <Button label="Next" className="p-button-primary" icon="pi pi-check" onClick = { this.Next } disabled={this.props.disableNextButton} /> </div> <div className="p-col-1"> - <Button label="Cancel" className="p-button-danger" icon="pi pi-times" style={{ width : '90px' }} /> + <Button label="Cancel" className="p-button-danger" icon="pi pi-times" style={{ width : '90px' }} + onClick={(e) => { this.props.onCancel()}} /> </div> </div> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/index.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/index.js index 8fb9718b9d314c0913820172f8f9720b987e2ecf..775c879181f3d8d7fb1b83270b99c32ee357eb76 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/index.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/index.js @@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react'; import PageHeader from '../../layout/components/PageHeader'; import {Growl} from 'primereact/components/growl/Growl'; import { Link } from 'react-router-dom'; +import _ from 'lodash'; import ScheduleService from '../../services/schedule.service'; import Scheduled from './Scheduled'; import ProcessingDone from './processing.done'; @@ -9,83 +10,152 @@ import QAreporting from './qa.reporting'; import QAsos from './qa.sos'; import PIverification from './pi.verification'; import DecideAcceptance from './decide.acceptance'; -import IngestDone from './ingest.done'; -import _ from 'lodash'; +import Ingesting from './ingesting'; import DataProduct from './unpin.data'; import UnitConverter from '../../utils/unit.converter'; +import AppLoader from '../../layout/components/AppLoader'; +import WorkflowService from '../../services/workflow.service'; +import DataProductService from '../../services/data.product.service'; +const RedirectionMap = { + 'wait scheduled': 1, + 'wait processed': 2, + 'qa reporting to': 3, + 'qa reporting sos':4, + 'pi verification':5, + 'decide acceptance':6, + 'ingest done':7, + 'unpin data':8 + }; //Workflow Page Title -const pageTitle = ['Scheduled','Processing Done','QA Reporting (TO)', 'QA Reporting (SDCO)', 'PI Verification', 'Decide Acceptance','Ingest Done','Unpin Data']; +const pageTitle = ['Waiting To Be Scheduled','Scheduled','QA Reporting (TO)', 'QA Reporting (SDCO)', 'PI Verification', 'Decide Acceptance','Ingest','Unpin Data']; export default (props) => { let growl; + // const [disableNextButton, setDisableNextButton] = useState(false); + const [loader, setLoader] = useState(false); const [state, setState] = useState({}); const [tasks, setTasks] = useState([]); - const [currentStep, setCurrentStep] = useState(1); + const [QASUProcess, setQASUProcess] = useState(); + const [currentStep, setCurrentStep] = useState(); const [schedulingUnit, setSchedulingUnit] = useState(); - const [ingestTask, setInjestTask] = useState({}); + // const [ingestTask, setInjestTask] = useState({}); + // const [QASchedulingTask, setQASchdulingTask] = useState([]); + useEffect(() => { - // Clearing Localstorage on start of the page to load fresh - clearLocalStorage(); - ScheduleService.getSchedulingUnitBlueprintById(props.match.params.id) - .then(schedulingUnit => { - setSchedulingUnit(schedulingUnit); - }) - const promises = [ScheduleService.getSchedulingUnitBlueprintById(props.match.params.id), ScheduleService.getTaskType()] + setLoader(true); + const promises = [ + ScheduleService.getSchedulingUnitExtended('blueprint', props.match.params.id), + ScheduleService.getTaskType() + ] Promise.all(promises).then(responses => { + const SUB = responses[0]; setSchedulingUnit(responses[0]); - ScheduleService.getTaskBlueprintsBySchedulingUnit(responses[0], true, false, false, true).then(response => { - response.map(task => { - task.actionpath = `/task/view/blueprint/${task.id}/dataproducts`; - (task.dataProducts || []).map(product => { - if (product.size) { - if (!task.totalDataSize) { - task.totalDataSize = 0; - } - task.totalDataSize += product.size; + setTasks(SUB.task_blueprints); + getStatusUpdate(SUB.task_blueprints); + }); + }, []); - // For deleted since - if (!product.deleted_since && product.size) { - if (!task.dataSizeNotDeleted) { - task.dataSizeNotDeleted = 0; - } - task.dataSizeNotDeleted += product.size; - } - } - }); - if (task.totalDataSize) { - task.totalDataSize = UnitConverter.getUIResourceUnit('bytes', (task.totalDataSize)); - } - if (task.dataSizeNotDeleted) { - task.dataSizeNotDeleted = UnitConverter.getUIResourceUnit('bytes', (task.dataSizeNotDeleted)); - } - }); - setTasks(response); - setInjestTask(response.find(task => task.template.type_value==='observation')); - }); + + /** + * Method to fetch data product for each sub task except ingest. + * @param {*} taskItems List of tasks + */ + const getDataProductDetails = async (taskItems) => { + // setLoader(true); + taskItems = taskItems?taskItems:tasks; + const taskList = [...taskItems]; + for (const task of taskList) { + if (task.specifications_template.type_value === 'observation' || task.specifications_template.type_value === 'pipeline') { + const promises = []; + task.subtasks_ids.map(id => promises.push(DataProductService.getSubtaskOutputDataproduct(id))); + const dataProducts = await Promise.all(promises); + task['dataProducts'] = dataProducts.filter(product => product.data.length).map(product => product.data).flat(); + task.actionpath = `/task/view/blueprint/${task.id}/dataproducts`; + task.totalDataSize = _.sumBy(task['dataProducts'], 'size'); + task.dataSizeNotDeleted = _.sumBy(task['dataProducts'], function(product) { return product.deletedSince?0:product.size}); + if (task.totalDataSize) { + task.totalDataSize = UnitConverter.getUIResourceUnit('bytes', (task.totalDataSize)); + } + if (task.dataSizeNotDeleted) { + task.dataSizeNotDeleted = UnitConverter.getUIResourceUnit('bytes', (task.dataSizeNotDeleted)); + } + } + } + // setInjestTask(taskList.find(task => task.specifications_template.type_value==='ingest')); + // setTasks(taskList); + // setLoader(false); + }; + + /** + * Method to fetch current step workflow details + * @param {*} taskList List of tasks + */ + const getStatusUpdate = (taskList) => { + setLoader(true); + const promises = [ + WorkflowService.getWorkflowProcesses(), + WorkflowService.getWorkflowTasks() + ] + Promise.all(promises).then(async responses => { + const suQAProcess = responses[0].find(process => process.su === parseInt(props.match.params.id)); + setQASUProcess(suQAProcess); + const suQAProcessTasks = responses[1].filter(item => item.process === suQAProcess.id); + // setQASchdulingTask(suQAProcessTasks); + // const workflowLastTask = responses[1].find(task => task.process === suQAProcess.id); + const workflowLastTask = (_.orderBy(suQAProcessTasks, ['id'], ['desc']))[0]; + setCurrentStep(RedirectionMap[workflowLastTask.flow_task.toLowerCase()]); + // Need to cross check below if condition if it fails in next click + if (workflowLastTask.status === 'NEW') { + setCurrentStep(RedirectionMap[workflowLastTask.flow_task.toLowerCase()]); + } //else { + // setCurrentStep(3); + // } + else if (workflowLastTask.status.toLowerCase() === 'done' || workflowLastTask.status.toLowerCase() === 'finished') { + await getDataProductDetails(taskList); + // setDisableNextButton(true); + setCurrentStep(8); + } + setLoader(false); }); -}, []); + } - const clearLocalStorage = () => { - localStorage.removeItem('pi_comment'); - localStorage.removeItem('report_qa'); + const getIngestTask = () => { + return tasks.find(task => task.specifications_template.type_value==='ingest') } - - //Pages changes step by step + + const getCurrentTaskDetails = async () => { + // const response = await WorkflowService.getCurrentTask(props.match.params.id); + const response = await WorkflowService.getCurrentTask(QASUProcess.id); + return response; + }; + + //Pages changes step by step const onNext = (content) => { setState({...state, ...content}); - setCurrentStep(currentStep + 1); + getStatusUpdate(tasks); }; + const onCancel = () => { + props.history.goBack(); + } + + //TODO: Need to customize this function to have different messages. + const showMessage = () => { + growl.show({severity: 'error', summary: 'Unable to proceed', detail: 'Please clear your browser cookies and try again'}); + } + + const title = pageTitle[currentStep - 1]; return ( <> <Growl ref={(el) => growl = el} /> - <PageHeader location={props.location} title={`${pageTitle[currentStep - 1]}`} actions={[{ icon: 'fa-window-close', link: props.history.goBack, title: 'Click to Close Workflow', props: { pathname: '/schedulingunit/1/workflow' } }]} /> - {schedulingUnit && + {currentStep && <PageHeader location={props.location} title={`${title}`} actions={[{ icon: 'fa-window-close', link: props.history.goBack, title: 'Click to Close Workflow', props: { pathname: '/schedulingunit/1/workflow' } }]} />} + {loader && <AppLoader />} + {!loader && schedulingUnit && <> <div className="p-fluid"> - <div className="p-field p-grid"> + {currentStep && <div className="p-field p-grid"> <label htmlFor="suName" className="col-lg-2 col-md-2 col-sm-12">Scheduling Unit</label> <div className="col-lg-3 col-md-3 col-sm-12"> <Link to={{ pathname: `/schedulingunit/view/blueprint/${schedulingUnit.id}` }}>{schedulingUnit.name}</Link> @@ -98,24 +168,35 @@ export default (props) => { <label htmlFor="viewPlots" className="col-lg-2 col-md-2 col-sm-12">View Plots</label> <div className="col-lg-3 col-md-3 col-sm-12" style={{ paddingLeft: '2px' }}> <label className="col-sm-10 " > - <a href="https://proxy.lofar.eu/inspect/HTML/" target="_blank">Inspection plots</a> + <a rel="noopener noreferrer" href="https://proxy.lofar.eu/inspect/HTML/" target="_blank">Inspection plots</a> </label> <label className="col-sm-10 "> - <a href="https://proxy.lofar.eu/qa" target="_blank">Adder plots</a> + <a rel="noopener noreferrer" href="https://proxy.lofar.eu/qa" target="_blank">Adder plots</a> </label> <label className="col-sm-10 "> <a href=" https://proxy.lofar.eu/lofmonitor/" target="_blank">Station Monitor</a> </label> </div> - </div> - {currentStep === 1 && <Scheduled onNext={onNext} {...state} schedulingUnit={schedulingUnit} />} - {currentStep === 2 && <ProcessingDone onNext={onNext} {...state}/>} - {currentStep === 3 && <QAreporting onNext={onNext}/>} - {currentStep === 4 && <QAsos onNext={onNext} {...state} />} - {currentStep === 5 && <PIverification onNext={onNext} {...state} />} - {currentStep === 6 && <DecideAcceptance onNext={onNext} {...state} />} - {currentStep === 7 && <IngestDone onNext={onNext}{...state} task={ingestTask} />} - {currentStep === 8 && <DataProduct onNext={onNext} tasks={tasks} schedulingUnit={schedulingUnit} />} + </div>} + {currentStep === 1 && <Scheduled onNext={onNext} onCancel={onCancel} + schedulingUnit={schedulingUnit} /*disableNextButton={disableNextButton}*/ />} + {currentStep === 2 && <ProcessingDone onNext={onNext} onCancel={onCancel} + schedulingUnit={schedulingUnit} />} + {currentStep === 3 && <QAreporting onNext={onNext} onCancel={onCancel} id={QASUProcess.id} + getCurrentTaskDetails={getCurrentTaskDetails} onError={showMessage} />} + {currentStep === 4 && <QAsos onNext={onNext} onCancel={onCancel} id={QASUProcess.id} + process={QASUProcess} getCurrentTaskDetails={getCurrentTaskDetails} + onError={showMessage} />} + {currentStep === 5 && <PIverification onNext={onNext} onCancel={onCancel} id={QASUProcess.id} + process={QASUProcess} getCurrentTaskDetails={getCurrentTaskDetails} + onError={showMessage} />} + {currentStep === 6 && <DecideAcceptance onNext={onNext} onCancel={onCancel} id={QASUProcess.id} + process={QASUProcess} getCurrentTaskDetails={getCurrentTaskDetails} + onError={showMessage} />} + {currentStep === 7 && <Ingesting onNext={onNext} onCancel={onCancel} id={QASUProcess.id} + onError={showMessage} task={getIngestTask()} />} + {currentStep === 8 && <DataProduct onNext={onNext} onCancel={onCancel} onError={showMessage} + tasks={tasks} schedulingUnit={schedulingUnit} />} </div> </> } diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/ingest.done.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/ingesting.js similarity index 66% rename from SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/ingest.done.js rename to SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/ingesting.js index 6c830385fa71f8c0eb2bb5da5854de921f68fbe8..77c795899b9b1ea4c4288eafe2bc0d1145abe823 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/ingest.done.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/ingesting.js @@ -1,8 +1,7 @@ import React, { Component } from 'react'; import { Button } from 'primereact/button'; -import { Link } from 'react-router-dom'; -class IngestDone extends Component { +class Ingesting extends Component { constructor(props) { super(props); this.state = { }; @@ -10,10 +9,7 @@ class IngestDone extends Component { } onSave(){ - this.props.onNext({ - report: this.props.report, - picomment: this.props.picomment - }); + this.props.onNext({}); } render(){ @@ -28,23 +24,20 @@ class IngestDone extends Component { <div className="col-lg-1 col-md-1 col-sm-12"></div> <label htmlFor="ingestTask" className="col-lg-2 col-md-2 col-sm-12">Ingest Task</label> <div className="col-lg-3 col-md-3 col-sm-12"> - <a href={`${window.location.origin}/task/view/blueprint/${this.props.task.id}`}>{this.props.task.name}</a> + <a rel="noopener noreferrer" href={`${window.location.origin}/task/view/blueprint/${this.props.task.id}`}>{this.props.task.name}</a> </div> <label htmlFor="ingestMonitoring" className="col-lg-2 col-md-2 col-sm-12">Ingest Monitoring</label> <label className="col-sm-10 " > - <a href="http://lexar003.control.lofar:9632/" target="_blank">View Ingest Monitoring <span class="fas fa-desktop"></span></a> + <a rel="noopener noreferrer" href="http://lexar003.control.lofar:9632/" target="_blank">View Ingest Monitoring <span class="fas fa-desktop"></span></a> </label> - - {/* <div className="col-lg-3 col-md-3 col-sm-12"> - <Link to={{ pathname: `http://lexar003.control.lofar:9632/` }}> View Ingest Monitoring <span class="fas fa-desktop"></span></Link> - </div> */} </div> <div className="p-grid p-justify-start"> <div className="p-col-1"> <Button label="Next" className="p-button-primary" icon="pi pi-check" onClick={ this.onSave }/> </div> <div className="p-col-1"> - <Button label="Cancel" className="p-button-danger" icon="pi pi-times" style={{ width : '90px' }} /> + <Button label="Cancel" className="p-button-danger" icon="pi pi-times" style={{ width : '90px' }} + onClick={(e) => { this.props.onCancel()}} /> </div> </div> </div> @@ -53,4 +46,4 @@ class IngestDone extends Component { }; } -export default IngestDone; \ No newline at end of file +export default Ingesting \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/pi.verification.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/pi.verification.js index f63a6fe0591e6dd6acd9fd5bc72c1988c33fc358..dd4492e9314ee077e2effd7620ee433fc66efb46 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/pi.verification.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/pi.verification.js @@ -3,38 +3,58 @@ import { Button } from 'primereact/button'; import SunEditor from 'suneditor-react'; import 'suneditor/dist/css/suneditor.min.css'; // Import Sun Editor's CSS File import { Checkbox } from 'primereact/checkbox'; +import WorkflowService from '../../services/workflow.service'; //import {InputTextarea} from 'primereact/inputtextarea'; class PIverification extends Component { constructor(props) { super(props); this.state = { - content: props.report, + content: '', + comment: '', showEditor: false, + pi_accept: false }; this.Next = this.Next.bind(this); this.handleChange = this.handleChange.bind(this); this.onChangePIComment = this.onChangePIComment.bind(this); } + + async componentDidMount() { + const response = await WorkflowService.getQAReportingSOS(this.props.process.qa_reporting_sos); + this.setState({ + content: response.sos_report + }); + } /** - * Method wiill trigger on change of operator report sun-editor + * Method will trigger on change of operator report sun-editor */ handleChange(e) { - this.setState({ - comment: e - }); - localStorage.setItem('report_pi', e); + if (e === '<p><br></p>') { + this.setState({ content: '' }); + return; + } + this.setState({ content: e }); } - /** - * Method will trigger on click save buton + /** + * Method will trigger on click save button * here onNext props coming from parent, where will handle redirection to other page */ - Next(){ - this.props.onNext({ - report: this.state.content, - picomment: this.state.comment + async Next() { + const currentWorkflowTask = await this.props.getCurrentTaskDetails(); + const promise = []; + if (currentWorkflowTask && !currentWorkflowTask.fields.owner) { + promise.push(WorkflowService.updateAssignTo(currentWorkflowTask.pk),{ owner: this.state.assignTo }); + } + promise.push(WorkflowService.updateQA_Perform(this.props.id,{"pi_report": this.state.comment, "pi_accept": this.state.pi_accept})); + Promise.all(promise).then((responses) => { + if (responses.indexOf(null)<0) { + this.props.onNext({ report:this.state.content, pireport: this.state.comment}); + } else { + this.props.onError(); + } }); } @@ -42,15 +62,11 @@ class PIverification extends Component { * Method wiill triigger on change of pi report sun-editor */ onChangePIComment(a) { - this.setState({ - comment: a - }); - localStorage.setItem('comment_pi', a); - } - - // Not using at present - cancelCreate() { - this.props.history.goBack(); + if (a === '<p><br></p>') { + this.setState({ comment: '' }); + return; + } + this.setState({comment: a }); } render() { @@ -74,7 +90,7 @@ class PIverification extends Component { <div className="operator-report" dangerouslySetInnerHTML={{ __html: this.state.content }}></div> </div> <div className="p-grid" style={{ padding: '10px' }}> - <label htmlFor="piReport" >PI Report</label> + <label htmlFor="piReport" >PI Report<span style={{color:'red'}}>*</span></label> <div className="col-lg-12 col-md-12 col-sm-12"></div> <SunEditor setDefaultStyle="min-height: 150px; height: auto;" enableToolbar={true} setContents={this.state.comment} @@ -85,25 +101,20 @@ class PIverification extends Component { 'superscript', 'outdent', 'indent', 'fullScreen', 'showBlocks', 'codeView', 'preview', 'print', 'removeFormat'] ] }} /> - {/* <InputTextarea rows={3} cols={30} - tooltip="PIReport" tooltipOptions={this.tooltipOptions} maxLength="128" - data-testid="PIReport" - value={this.state.piComment} - onChange={this.onChangePIComment} - /> */} - </div> + </div> <div className="p-field p-grid"> <label htmlFor="piAccept" className="col-lg-2 col-md-2 col-sm-12">PI Accept</label> <div className="p-field-checkbox"> - <Checkbox inputId="binary" checked={this.state.checked} onChange={e => this.setState({ checked: e.checked })} /> + <Checkbox inputId="binary" checked={this.state.pi_accept} onChange={e => this.setState({ pi_accept: e.checked })} /> </div> </div> <div className="p-grid" style={{ marginTop: '20px' }}> <div className="p-col-1"> - <Button label="Next" className="p-button-primary" icon="pi pi-check" onClick={ this.Next } /> + <Button disabled= {!this.state.comment} label="Next" className="p-button-primary" icon="pi pi-check" onClick={ this.Next } /> </div> <div className="p-col-1"> - <Button label="Cancel" className="p-button-danger" icon="pi pi-times" style={{ width : '90px' }} /> + <Button label="Cancel" className="p-button-danger" icon="pi pi-times" style={{ width : '90px' }} + onClick={(e) => { this.props.onCancel()}} /> </div> </div> </div> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/processing.done.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/processing.done.js index 07c7ca9cdca1ae96d2d8ce166d836303036ccbc0..42bf78d229f16924fc44226fda9b99c1a73ffcf5 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/processing.done.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/processing.done.js @@ -1,14 +1,15 @@ import React, { Component } from 'react'; import { Button } from 'primereact/button'; +import { Link } from 'react-router-dom'; +import moment from 'moment'; class ProcessingDone extends Component { - constructor(props) { super(props); this.Next = this.Next.bind(this); } - - /** + + /** * Method will trigger on click save buton * here onNext props coming from parent, where will handle redirection to other page */ @@ -19,12 +20,29 @@ class ProcessingDone extends Component { render(){ return( <> + <div className="p-fluid"> + <div className="p-field p-grid"> + <label htmlFor="startTime" className="col-lg-2 col-md-2 col-sm-12">Start Time</label> + <div className="col-lg-3 col-md-3 col-sm-12"> + <span>{this.props.schedulingUnit.start_time && moment(this.props.schedulingUnit.start_time).format("YYYY-MMM-DD HH:mm:SS")}</span> + </div> + <div className="col-lg-1 col-md-1 col-sm-12"></div> + <label htmlFor="endTime" className="col-lg-2 col-md-2 col-sm-12">End Time</label> + <div className="col-lg-3 col-md-3 col-sm-12"> + <span>{this.props.schedulingUnit.stop_time && moment(this.props.schedulingUnit.stop_time).format("YYYY-MMM-DD HH:mm:SS")}</span> + </div> + <div className="col-lg-1 col-md-1 col-sm-12"></div> + <label htmlFor="timeLine" className="col-lg-2 col-md-2 col-sm-12">Timeline</label> + <div className="col-lg-3 col-md-3 col-sm-12 block-list"> + <Link to={{ pathname: '/su/timelineview' }}>TimeLine View <span class="fas fa-clock"></span></Link> + <Link to={{ pathname: '/su/timelineview/week' }}>Week Overview <span class="fas fa-calendar-alt"></span></Link> + </div> + </div> + </div> <div className="p-grid p-justify-start"> <div className="p-col-1"> - <Button label="Next" className="p-button-primary" icon="pi pi-check" onClick={ this.Next }/> - </div> - <div className="p-col-1"> - <Button label="Cancel" className="p-button-danger" icon="pi pi-times" style={{ width : '90px' }} /> + <Button label="Cancel" className="p-button-danger" icon="pi pi-times" style={{ width : '90px' }} + onClick={(e) => {this.props.onCancel()}} /> </div> </div> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/qa.reporting.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/qa.reporting.js index 1c4684e2c5b5b13e34cd1a2f87a27118c27f33f8..ef425cf3f9c5fd53495ce418c7c9b38cbd8e04a6 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/qa.reporting.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/qa.reporting.js @@ -3,6 +3,8 @@ import { Button } from 'primereact/button'; import SunEditor from 'suneditor-react'; import 'suneditor/dist/css/suneditor.min.css'; // Import Sun Editor's CSS File import { Dropdown } from 'primereact/dropdown'; +import WorkflowService from '../../services/workflow.service'; +import { Checkbox } from 'primereact/checkbox'; //import katex from 'katex' // for mathematical operations on sun editor this component should be added //import 'katex/dist/katex.min.css' @@ -11,47 +13,59 @@ class QAreporting extends Component{ constructor(props) { super(props); this.state={ - content: props.report + content: '', + assignTo: '', + operator_accept: false, }; this.Next = this.Next.bind(this); this.handleChange = this.handleChange.bind(this); } /** - * Method will trigger on click save buton + * Method will trigger on click next buton * here onNext props coming from parent, where will handle redirection to other page */ - Next() { - this.props.onNext({ report: this.state.content }); - } + async Next() { + const currentWorkflowTask = await this.props.getCurrentTaskDetails(); + const promise = []; + if (currentWorkflowTask && !currentWorkflowTask.fields.owner) { + promise.push(WorkflowService.updateAssignTo(currentWorkflowTask.pk, { owner: this.state.assignTo })); + } + promise.push(WorkflowService.updateQA_Perform(this.props.id, {"operator_report": this.state.content, "operator_accept": this.state.operator_accept})); + Promise.all(promise).then((responses) => { + if (responses.indexOf(null)<0) { + this.props.onNext({ report: this.state.content }); + } else { + this.props.onError(); + } + }); + } /** * Method will trigger on change of operator report sun-editor */ handleChange(e) { - localStorage.setItem('report_qa', e); + if (e === '<p><br></p>') { + this.setState({ content: '' }); + return; + } this.setState({ content: e }); } - //Not using at present - cancelCreate() { - this.props.history.goBack(); - } - render() { return ( <> <div className="p-fluid"> <div className="p-field p-grid"> - <label htmlFor="assignTo" className="col-lg-2 col-md-2 col-sm-12">Assign To </label> + <label htmlFor="assignTo" className="col-lg-2 col-md-2 col-sm-12">Assign To</label> <div className="col-lg-3 col-md-3 col-sm-12" data-testid="assignTo" > - <Dropdown inputId="assignToValue" optionLabel="value" optionValue="value" - options={[{ value: 'User 1' }, { value: 'User 2' }, { value: 'User 3' }]} + <Dropdown inputId="assignToValue" value={this.state.assignTo} optionLabel="value" optionValue="id" onChange={(e) => this.setState({assignTo: e.value})} + options={[{ value: 'User 1', id: 1 }, { value: 'User 2', id: 2 }, { value: 'User 3', id: 3 }]} placeholder="Assign To" /> </div> </div> <div className="p-grid" style={{ padding: '10px' }}> - <label htmlFor="comments" >Comments</label> + <label htmlFor="comments" >Comments<span style={{color:'red'}}>*</span></label> <div className="col-lg-12 col-md-12 col-sm-12"></div> <SunEditor enableToolbar={true} setDefaultStyle="min-height: 250px; height: auto;" @@ -64,12 +78,19 @@ class QAreporting extends Component{ }} /> </div> </div> + <div className="p-grid"> + <div className="p-col-12"> + <Checkbox inputId="operator_accept" onChange={e => this.setState({operator_accept: e.checked})} checked={this.state.operator_accept}></Checkbox> + <label htmlFor="operator_accept " className="p-checkbox-label">Operator Accept</label> + </div> + </div> <div className="p-grid p-justify-start"> <div className="p-col-1"> - <Button label="Next" className="p-button-primary" icon="pi pi-check" onClick={ this.Next } /> + <Button disabled= {!this.state.content} label="Next" className="p-button-primary" icon="pi pi-check" onClick={ this.Next } /> </div> <div className="p-col-1"> - <Button label="Cancel" className="p-button-danger" icon="pi pi-times" style={{ width : '88px' }}/> + <Button label="Cancel" className="p-button-danger" icon="pi pi-times" style={{ width : '88px' }} + onClick={(e) => { this.props.onCancel()}} /> </div> </div> </> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/qa.sos.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/qa.sos.js index 59ef61e29ac4d7f2fe3eb7510fc290e65febff47..b32d13319c490a36c033b4d83a0b5d42d613e08a 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/qa.sos.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/qa.sos.js @@ -3,42 +3,59 @@ import { Button } from 'primereact/button'; import SunEditor from 'suneditor-react'; import 'suneditor/dist/css/suneditor.min.css'; // Import Sun Editor's CSS File import { Checkbox } from 'primereact/checkbox'; +import WorkflowService from '../../services/workflow.service'; class QAreportingSDCO extends Component { constructor(props) { super(props); this.state = { - content: props.report, + content: '', showEditor: false, - checked: false, - pichecked: false - + quality_within_policy: false, + sos_accept_show_pi: false }; this.Next = this.Next.bind(this); this.handleChange = this.handleChange.bind(this); } + async componentDidMount() { + const response = await WorkflowService.getQAReportingTo(this.props.process.qa_reporting_to); + this.setState({ + content: response.operator_report + }); + } + /** * Method will trigger on change of sun-editor */ handleChange(e) { - this.setState({ - content: e - }); - localStorage.setItem('report_qa', e); + if (e === '<p><br></p>') { + this.setState({ content: '' }); + return; + } + this.setState({ content: e }); } - /** - * Method will trigger on click save buton + /** + * Method will trigger on click Next buton * here onNext props coming from parent, where will handle redirection to other page */ - Next() { - this.props.onNext({ - report: this.state.content, - piChecked: this.state.pichecked - }) + async Next() { + const currentWorkflowTask = await this.props.getCurrentTaskDetails(); + const promise = []; + if (currentWorkflowTask && !currentWorkflowTask.fields.owner) { + promise.push(WorkflowService.updateAssignTo(currentWorkflowTask.pk, { owner: this.state.assignTo })); + } + promise.push(WorkflowService.updateQA_Perform(this.props.id, {"sos_report": this.state.content, "sos_accept_show_pi": this.state.sos_accept_show_pi, "quality_within_policy": this.state.quality_within_policy})); + Promise.all(promise).then((responses) => { + if (responses.indexOf(null)<0) { + this.props.onNext({ report: this.state.content }); + } else { + this.props.onError(); + } + }); } - + //Not using at present cancelCreate() { this.props.history.goBack(); @@ -53,19 +70,22 @@ class QAreportingSDCO extends Component { <label htmlFor="qualityPolicy" className="col-lg-2 col-md-2 col-sm-12">Quality Policy</label> <div className="col-lg-3 col-md-3 col-sm-12"> <div className="p-field-checkbox"> - <Checkbox inputId="binary" checked={this.state.checked} onChange={e => this.setState({ checked: e.checked })} /> + <Checkbox inputId="quality_within_policy" checked={this.state.quality_within_policy} onChange={e => this.setState({quality_within_policy: e.checked})} /> </div> </div> <div className="col-lg-1 col-md-1 col-sm-12"></div> <label htmlFor="sdcoAccept" className="col-lg-2 col-md-2 col-sm-12">SDCO Accept</label> <div className="col-lg-3 col-md-3 col-sm-12"> <div className="p-field-checkbox"> - <Checkbox inputId="secondary" pichecked={this.state.pichecked} onChange={e => this.setState({ pichecked: e.pichecked })} /> + <Checkbox inputId="sos_accept_show_pi" checked={this.state.sos_accept_show_pi} onChange={e => this.setState({ sos_accept_show_pi: e.checked })} /> </div> </div> </div> <div className="p-grid" style={{ padding: '10px' }}> - <label htmlFor="operatorReport" >Operator Report {!this.state.showEditor && <span className="con-edit">(Click content to edit)</span>}</label> + <label htmlFor="operatorReport" > + Operator Report {!this.state.showEditor && <span className="con-edit">(Click content to edit)</span>} + <span style={{color:'red'}}>*</span> + </label> <div className="col-lg-12 col-md-12 col-sm-12"></div> {this.state.showEditor && <SunEditor setDefaultStyle="min-height: 250px; height: auto" enableToolbar={true} onChange={this.handleChange} @@ -82,16 +102,16 @@ class QAreportingSDCO extends Component { </div> <div className="p-grid" style={{ marginTop: '20px' }}> <div className="p-col-1"> - <Button label="Next" className="p-button-primary" icon="pi pi-check" onClick={ this.Next } /> + <Button label="Next" disabled= {!this.state.content} className="p-button-primary" icon="pi pi-check" onClick={ this.Next } /> </div> <div className="p-col-1"> - <Button label="Cancel" className="p-button-danger" icon="pi pi-times" style={{ width : '90px' }} /> + <Button label="Cancel" className="p-button-danger" icon="pi pi-times" style={{ width : '90px' }} + onClick={(e) => { this.props.onCancel()}} /> </div> </div> </div> </> ) }; - } export default QAreportingSDCO; \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/unpin.data.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/unpin.data.js index f4893b6f5a35de0f32b79b447da14c99d7382750..ee4de9a3ed910123b1e386eef96589d1af5c82d7 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/unpin.data.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/unpin.data.js @@ -3,12 +3,12 @@ import { Button } from 'primereact/button'; import { Dialog } from 'primereact/dialog'; import ViewTable from './../../components/ViewTable'; -export default ({ tasks, schedulingUnit }) => { +export default ({ tasks, schedulingUnit, onCancel }) => { const [showConfirmDialog, setShowConfirmDialog] = useState(false); const defaultcolumns = [ { name: "Name", totalDataSize:"Total Data Size(TB)", - dataSizeNotDeleted :"Data Size Not Deleted(TB)" + dataSizeNotDeleted :"Data Size on Disk(TB)" }]; const optionalcolumns = [{ actionpath:"actionpath", @@ -25,7 +25,7 @@ export default ({ tasks, schedulingUnit }) => { <div className="p-fluid mt-2"> <label><h6>Details of data products of Tasks</h6></label> <ViewTable - data={tasks.filter(task => task.template.name !== 'ingest' && (task.totalDataSize || task.dataSizeNotDeleted))} + data={tasks.filter(task => (task.totalDataSize || task.dataSizeNotDeleted))} optionalcolumns={optionalcolumns} defaultcolumns={defaultcolumns} defaultSortColumn={defaultSortColumn} @@ -43,7 +43,8 @@ export default ({ tasks, schedulingUnit }) => { <Button label="Delete" className="p-button-primary" icon="pi pi-trash" onClick={toggleDialog} /> </div> <div className="p-col-1"> - <Button label="Cancel" className="p-button-danger" icon="pi pi-times" style={{ width: '90px' }} /> + <Button label="Cancel" className="p-button-danger" icon="pi pi-times" style={{ width: '90px' }} + onClick={(e) => { onCancel()}} /> </div> </div> <div className="p-grid" data-testid="confirm_dialog"> @@ -66,4 +67,4 @@ export default ({ tasks, schedulingUnit }) => { </div> </div> ) -} \ No newline at end of file +} diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/index.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/index.js index ffaeeac79c33f3fbad5a109796b76171bebc723b..afe449b4d5daa05d04f2207a4647a8dd5ba397a0 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/index.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/index.js @@ -15,7 +15,7 @@ import SchedulingUnitCreate from './Scheduling/create'; import EditSchedulingUnit from './Scheduling/edit'; import { CycleList, CycleCreate, CycleView, CycleEdit } from './Cycle'; import { TimelineView, WeekTimelineView, ReservationCreate, ReservationList } from './Timeline'; -import SchedulingSetCreate from './Scheduling/create.scheduleset'; +import SchedulingSetCreate from './Scheduling/excelview.schedulingset'; import Workflow from './Workflow'; export const routes = [ 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 506f246c4423d9083548e43ebf5bb806d842a1ba..072ff6ca0dcbf92f0914152c772b9ab3352e2969 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/services/schedule.service.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/services/schedule.service.js @@ -45,17 +45,17 @@ const ScheduleService = { } return blueprints; }, - getSchedulingUnitExtended: async function (type, id){ + getSchedulingUnitExtended: async function (type, id, ignoreRef){ let schedulingUnit = null; try { const response = await axios.get(`/api/scheduling_unit_${type}_extended/${id}/`); schedulingUnit = response.data; - if (schedulingUnit) { + if (schedulingUnit && !ignoreRef) { if (type === "blueprint") { const schedulingUnitDraft = (await axios.get(`/api/scheduling_unit_draft/${schedulingUnit.draft_id}/`)).data; schedulingUnit.draft_object = schedulingUnitDraft; schedulingUnit.scheduling_set_id = schedulingUnitDraft.scheduling_set_id; - schedulingUnit.scheduling_constraints_doc = schedulingUnitDraft.scheduling_constraints_doc; + schedulingUnit.scheduling_constraints_doc = schedulingUnitDraft.scheduling_constraints_doc?schedulingUnitDraft.scheduling_constraints_doc:{}; } else { // Fetch all blueprints data associated with draft to display the name schedulingUnit.blueprintList = (await this.getBlueprintsByschedulingUnitId(schedulingUnit.id)).data.results; @@ -137,6 +137,15 @@ const ScheduleService = { return null; } }, + getSubtaskOutputDataproduct: async function(id){ + try { + const url = `/api/subtask/${id}/output_dataproducts/`; + const response = await axios.get(url); + return response.data; + } catch (error) { + console.error('[data.product.getSubtaskOutputDataproduct]',error); + } + }, getTaskBlueprintById: async function(id, loadTemplate, loadSubtasks, loadSubtaskTemplate){ let result; try { @@ -302,11 +311,11 @@ const ScheduleService = { if (loadTemplate) { const ingest = scheduletasklist.find(task => task.template.type_value === 'ingest' && task.tasktype.toLowerCase() === 'draft'); const promises = []; - ingest.produced_by_ids.map(id => promises.push(this.getTaskRelation(id))); + ingest.produced_by_ids.forEach(id => promises.push(this.getTaskRelation(id))); const response = await Promise.all(promises); - response.map(producer => { + response.forEach(producer => { const tasks = scheduletasklist.filter(task => producer.producer_id === task.id); - tasks.map(task => { + tasks.forEach(task => { task.canIngest = true; }); }); @@ -397,6 +406,15 @@ const ScheduleService = { }); return res; }, + getSchedulingUnitTemplate: async function(id){ + try { + const response = await axios.get(`/api/scheduling_unit_template/${id}`); + return response.data; + } catch(error) { + console.error(error); + return null; + }; + }, getSchedulingSets: async function() { try { const response = await axios.get('/api/scheduling_set/'); @@ -624,12 +642,21 @@ const ScheduleService = { } return stationGroups; }, - getProjectList: async function() { + getProjectList: async function() { + try { + const response = await axios.get('/api/project/'); + return response.data.results; + } catch (error) { + console.error('[project.services.getProjectList]',error); + } + }, + saveSchedulingSet: async function(suSet) { try { - const response = await axios.get('/api/project/'); - return response.data.results; + const response = await axios.post(('/api/scheduling_set/'), suSet); + return response.data; } catch (error) { - console.error('[project.services.getProjectList]',error); + console.log(error.response.data); + return error.response.data; } } } diff --git a/SAS/TMSS/frontend/tmss_webapp/src/services/workflow.service.js b/SAS/TMSS/frontend/tmss_webapp/src/services/workflow.service.js index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..4b8e9d4741f51f5f3ecf4f0b6b814d79b6496057 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/services/workflow.service.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/services/workflow.service.js @@ -0,0 +1,98 @@ +const axios = require('axios'); + +const WorkflowService = { + getWorkflowProcesses: async function (){ + let data = []; + try { + let initResponse = await axios.get('/workflow_api/scheduling_unit_flow/qa_scheduling_unit_process/?ordering=id'); + data = initResponse.data.results; + const totalCount = initResponse.data.count; + const initialCount = initResponse.data.results.length; + if (initialCount < totalCount) { + let nextResponse = await axios.get(`/workflow_api/scheduling_unit_flow/qa_scheduling_unit_process/?ordering=id&limit=${totalCount-initialCount}&offset=${initialCount}`); + data = data.concat(nextResponse.data.results); + } + } catch(error) { + console.log(error); + } + return data; + }, + getWorkflowTasks: async function (){ + let data = []; + try { + let initResponse = await axios.get('/workflow_api/scheduling_unit_flow/qa_scheduling_unit_task/?ordering=id'); + data = initResponse.data.results; + const totalCount = initResponse.data.count; + const initialCount = initResponse.data.results.length; + if (initialCount < totalCount) { + let nextResponse = await axios.get(`/workflow_api/scheduling_unit_flow/qa_scheduling_unit_task/?ordering=id&limit=${totalCount-initialCount}&offset=${initialCount}`); + data = data.concat(nextResponse.data.results); + } + } catch(error) { + console.log(error); + } + return data; + }, + updateAssignTo: async (id, data) => { + try { + const response = await axios.post(`/workflow_api/scheduling_unit_flow/qa_scheduling_unit_task/${id}/assign/`, data); + return response.data; + } catch(error) { + console.error('[workflow.services.updateAssignTo]',error); + return null; + } + }, + updateQA_Perform: async (id, data) => { + try { + const response = await axios.post(`/workflow_api/scheduling_unit_flow/qa_scheduling_unit_process/${id}/perform/`, data); + return response.data; + } catch(error) { + console.error('[workflow.services.updateQA_Perform]',error); + return null; + } + }, + getSchedulingUnitTask: async () => { + try { + const response = await axios.get(`/workflow_api/scheduling_unit_flow/qa_scheduling_unit_task/`); + return response.data.results; + } catch(error) { + console.error('[workflow.services.getSchedulingUnitTask]',error); + } + }, + getCurrentTask: async (id) => { + let currentTask = null; + try { + const response = await axios.post(`/workflow_api/scheduling_unit_flow/qa_scheduling_unit_process/${id}/current_task/`); + currentTask = response.data[0]; + } catch(error) { + console.error('[workflow.services.current_task]',error); + } + return currentTask; + }, + getQAReportingTo: async (id) => { + try { + const response = await axios.get(`/workflow_api/scheduling_unit_flow/qa_reporting_to/${id}`); + return response.data; + } catch(error) { + console.error('[workflow.services.qa_reporting_to]',error); + } + }, + getQAReportingSOS: async (id) => { + try { + const response = await axios.get(`/workflow_api/scheduling_unit_flow/qa_reporting_sos/${id}`); + return response.data; + } catch(error) { + console.error('[workflow.services.qa_reporting_sos]',error); + } + }, + getQAPIverification: async (id) => { + try { + const response = await axios.get(`/workflow_api/scheduling_unit_flow/qa_pi_verification/${id}`); + return response.data; + } catch(error) { + console.error('[workflow.services.qa_pi_verification]',error); + } + } +} + +export default WorkflowService; diff --git a/SAS/TMSS/frontend/tmss_webapp/src/utils/validator.js b/SAS/TMSS/frontend/tmss_webapp/src/utils/validator.js index b40341265ddb5d38f69424c954e867df9e020813..79107d64b50cb0ccdac9c9c91501dcff1b6c818d 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/utils/validator.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/utils/validator.js @@ -1,34 +1,53 @@ const Validator = { validateTime(value) { + const splitOutput = value.split(':'); + if (splitOutput.length < 3) { + return false; + } else { + if (parseInt(splitOutput[0]) > 23 || parseInt(splitOutput[1])>59 || parseInt(splitOutput[2])>59) { + return false; + } + const timeValue = parseInt(splitOutput[0]*60*60) + parseInt(splitOutput[1]*60) + parseInt(splitOutput[2]); + if (timeValue >= 86400) { + return false; + } + } + return true; + }, + validateAngle(value) { const splitOutput = value.split(':'); if (splitOutput.length < 3) { return false; } else { - if (parseInt(splitOutput[0]) > 23 || parseInt(splitOutput[1])>59 || parseInt(splitOutput[2])>59) { - return false; - } - const timeValue = parseInt(splitOutput[0]*60*60) + parseInt(splitOutput[1]*60) + parseInt(splitOutput[2]); - if (timeValue >= 86400) { - return false; - } - } - return true; - }, - validateAngle(value) { - const splitOutput = value.split(':'); - if (splitOutput.length < 3) { + if (parseInt(splitOutput[0]) > 90 || parseInt(splitOutput[1])>59 || parseInt(splitOutput[2])>59) { + return false; + } + const timeValue = parseInt(splitOutput[0]*60*60) + parseInt(splitOutput[1]*60) + parseInt(splitOutput[2]); + if (timeValue > 324000) { return false; - } else { - if (parseInt(splitOutput[0]) > 90 || parseInt(splitOutput[1])>59 || parseInt(splitOutput[2])>59) { - return false; - } - const timeValue = parseInt(splitOutput[0]*60*60) + parseInt(splitOutput[1]*60) + parseInt(splitOutput[2]); - if (timeValue > 324000) { - return false; - } - } - return true; } + } + return true; + }, + /** + * Validates whether any of the given property values is modified comparing the old and new object. + * @param {Object} oldObject - old object that is already existing in the state list + * @param {Object} newObject - new object received from the websocket message + * @param {Array} properties - array of string (name of the properties) to veriy + */ + isObjectModified(oldObject, newObject, properties) { + let isModified = false; + // If oldObject is not found, the object should be got from server + if(!oldObject && newObject) { + return true; + } + for (const property of properties) { + if (oldObject[property] !== newObject[property]) { + isModified = true; + } + } + return isModified; + } }; export default Validator;