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/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/src/components/ViewTable.js b/SAS/TMSS/frontend/tmss_webapp/src/components/ViewTable.js index 32ed6b454cd03bca82d59316ffc0f67c18b3b00c..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" > - <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 1bfd6304ed46e79ec41ea0dd0fa78f6c706f96be..05696126b8f657f27ec557ae46e42b53ea0fae6b 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/layout/_overrides.scss +++ b/SAS/TMSS/frontend/tmss_webapp/src/layout/_overrides.scss @@ -200,6 +200,10 @@ .p-growl { z-index: 3000 !important; } +.p-hidden-accessible { + position: relative; +} + .data-product { label { display: block; 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..6ed9f5a31da40116f749923b0f9cb238e484d66a 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,71 @@ 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'; 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 +77,270 @@ 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{ + // 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(), + ScheduleService.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) { + 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; + }); + } + return su; + }); + this.setState({ + scheduleunit: schedulingUnits, isLoading: false, optionalColumns: [optionalColumns], + columnclassname: [columnclassname] + }); + } + componentDidMount(){ this.getSchedulingUnitList(); - } /** @@ -188,6 +383,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" @@ -196,6 +392,7 @@ class SchedulingUnitList extends Component{ tablename="scheduleunit_list" allowRowSelection={this.props.allowRowSelection} onRowSelection = {this.onRowSelection} + columnOrders={this.state.columnOrders} /> :<div>No scheduling unit found </div> } 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..ccee75dd85c151988cb2880c645fbde37aa600db 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: [{ @@ -81,7 +119,12 @@ class ViewSchedulingUnit extends Component{ "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, @@ -419,6 +477,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/Workflow/index.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/index.js index 8fb9718b9d314c0913820172f8f9720b987e2ecf..a1a8cf8a56dfa51ed7c687c9cec1574fc5cc5cb8 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/index.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/index.js @@ -10,7 +10,6 @@ import QAsos from './qa.sos'; import PIverification from './pi.verification'; import DecideAcceptance from './decide.acceptance'; import IngestDone from './ingest.done'; -import _ from 'lodash'; import DataProduct from './unpin.data'; import UnitConverter from '../../utils/unit.converter'; @@ -36,9 +35,9 @@ export default (props) => { Promise.all(promises).then(responses => { setSchedulingUnit(responses[0]); ScheduleService.getTaskBlueprintsBySchedulingUnit(responses[0], true, false, false, true).then(response => { - response.map(task => { + response.forEach(task => { task.actionpath = `/task/view/blueprint/${task.id}/dataproducts`; - (task.dataProducts || []).map(product => { + (task.dataProducts || []).forEach(product => { if (product.size) { if (!task.totalDataSize) { task.totalDataSize = 0; @@ -98,10 +97,10 @@ 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 href="https://proxy.lofar.eu/inspect/HTML/" target="_blank" rel="noopener noreferrer">Inspection plots</a> </label> <label className="col-sm-10 "> - <a href="https://proxy.lofar.eu/qa" target="_blank">Adder plots</a> + <a href="https://proxy.lofar.eu/qa" target="_blank" rel="noopener noreferrer">Adder plots</a> </label> <label className="col-sm-10 "> <a href=" https://proxy.lofar.eu/lofmonitor/" target="_blank">Station Monitor</a> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/ingest.done.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/ingest.done.js index 6c830385fa71f8c0eb2bb5da5854de921f68fbe8..fcb21eeffcda3b9e1f00b46a3b4de3be5cb87de1 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/ingest.done.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/ingest.done.js @@ -1,6 +1,5 @@ import React, { Component } from 'react'; import { Button } from 'primereact/button'; -import { Link } from 'react-router-dom'; class IngestDone extends Component { constructor(props) { @@ -32,7 +31,7 @@ class IngestDone extends Component { </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 href="http://lexar003.control.lofar:9632/" target="_blank" rel="noopener noreferrer">View Ingest Monitoring <span class="fas fa-desktop"></span></a> </label> {/* <div className="col-lg-3 col-md-3 col-sm-12"> 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 1d2fdb8d607cb1651c8fb3d0a233e6052985a5bd..3dd94d62328507f1ba6a1035d84235d329ff9749 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/services/schedule.service.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/services/schedule.service.js @@ -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/'); @@ -633,15 +651,26 @@ const ScheduleService = { } }, saveSchedulingSet: async function(suSet) { - try { - const response = await axios.post(('/api/scheduling_set/'), suSet); - return response.data; - } catch (error) { - console.log(error.response.data); - return error.response.data; - } + try { + const response = await axios.post(('/api/scheduling_set/'), suSet); + return response.data; + } catch (error) { + console.log(error.response.data); + return error.response.data; + } }, - + //TODO: This function should be removed and + //all its implementations should use the function from WorkflowService after merging workflow ticket branches + getWorkflowProcesses: async function() { + let processes = []; + try { + const response = await axios.get('/workflow_api/scheduling_unit_flow/qa_scheduling_unit_process/'); + processes = response.data.results; + } catch (error) { + console.error(error); + } + return processes; + } } export default ScheduleService; \ No newline at end of file