diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index cbdac6666da10c7e0092d604e6eec625fccbd928..02eb22467f56319e16296528badbd15a5a893a4b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -107,9 +107,9 @@ build_MCU_MAC: - cd build/gnucxx11_opt - cmake -DBUILD_PACKAGES=$PACKAGE -DWINCC_ROOT_DIR=/opt/WinCC_OA/3.16/ -DBLITZ_ROOT_DIR=/opt/blitz/ -DCASACORE_ROOT_DIR=/opt/casacore/ -DCMAKE_INSTALL_PREFIX=/opt/lofar ../.. - make -j 12 - - make DESTDIR=${CI_BUILDS_DIR}/install install - - cd ${CI_BUILDS_DIR}/install/opt/lofar - - tar --ignore-failed-read --exclude=include -czf MCU_MAC_${CI_COMMIT_REF_NAME}-${CI_COMMIT_SHORT_SHA}.ztar * + - make DESTDIR=${CI_BUILDS_DIR}/${CI_COMMIT_SHORT_SHA}/install install + - cd ${CI_BUILDS_DIR}/${CI_COMMIT_SHORT_SHA}/install/opt/lofar + - tar --ignore-failed-read --exclude=include --exclude="*.ztar" -czf MCU_MAC_${CI_COMMIT_REF_NAME}-${CI_COMMIT_SHORT_SHA}.ztar * - curl --insecure --upload-file MCU_MAC_${CI_COMMIT_REF_NAME}-${CI_COMMIT_SHORT_SHA}.ztar -u upload:upload https://support.astron.nl/nexus/content/repositories/branches/nl/astron/lofar/${CI_COMMIT_REF_NAME}-${CI_COMMIT_SHORT_SHA}/MCU_MAC_${CI_COMMIT_REF_NAME}-${CI_COMMIT_SHORT_SHA}.x86_64.ztar dependencies: diff --git a/Docker/lofar-ci/Dockerfile_ci_sas b/Docker/lofar-ci/Dockerfile_ci_sas index 3fb7193ab74ffd72c5ef3441d8915957b1c155ec..1076fbe2f569d87021cc2b6291c3235e4fc20389 100644 --- a/Docker/lofar-ci/Dockerfile_ci_sas +++ b/Docker/lofar-ci/Dockerfile_ci_sas @@ -18,7 +18,6 @@ ENV PATH /usr/pgsql-9.6/bin:$PATH RUN pip3 install cython kombu lxml requests pygcn xmljson mysql-connector-python python-dateutil Django==3.0.9 djangorestframework djangorestframework-xml ldap==1.0.2 flask fabric coverage python-qpid-proton PyGreSQL numpy h5py psycopg2 testing.postgresql Flask-Testing scipy Markdown django-filter python-ldap python-ldap-test ldap3 djangorestframework django-jsonforms django-json-widget django-jsoneditor drf-yasg flex swagger-spec-validator django-auth-ldap mozilla-django-oidc jsonschema comet - # Note: nodejs now comes with npm, do not install the npm package separately, since that will be taken from the epel repo and is conflicting. RUN echo "Installing Nodejs packages..." && \ curl -sL https://rpm.nodesource.com/setup_14.x | bash - && \ diff --git a/SAS/TMSS/CMakeLists.txt b/SAS/TMSS/CMakeLists.txt index 668eefd608fd3c612bb660ba71ad1e5dee363d21..23e6616f579313fb8e5eab6c5e172e306c927c33 100644 --- a/SAS/TMSS/CMakeLists.txt +++ b/SAS/TMSS/CMakeLists.txt @@ -1,6 +1,6 @@ -lofar_package(TMSS 0.1 DEPENDS PyCommon pyparameterset PyMessaging ResourceAssigner) +lofar_package(TMSS 0.1 DEPENDS PyCommon pyparameterset PyMessaging ResourceAssigner TaskPrescheduler) lofar_add_package(TMSSClient client) add_subdirectory(src) diff --git a/SAS/TMSS/frontend/CMakeLists.txt b/SAS/TMSS/frontend/CMakeLists.txt index b4a09541f6dd24d668847a68334265259315bcef..cb74bd697d19246e5b3467bc7fa6f874498791c8 100644 --- a/SAS/TMSS/frontend/CMakeLists.txt +++ b/SAS/TMSS/frontend/CMakeLists.txt @@ -1 +1,2 @@ -add_subdirectory(frontend_poc) \ No newline at end of file +# add_subdirectory(frontend_poc) +add_subdirectory(tmss_webapp) diff --git a/SAS/TMSS/frontend/tmss_webapp/.env b/SAS/TMSS/frontend/tmss_webapp/.env new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/SAS/TMSS/frontend/tmss_webapp/CMakeLists.txt b/SAS/TMSS/frontend/tmss_webapp/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..b20842bcefd3a4313c19ad5e7001a2f9b9175aac --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/CMakeLists.txt @@ -0,0 +1,2 @@ +include(NPMInstall) +npm_install(package.json PUBLIC public SOURCE src DESTINATION ${PYTHON_INSTALL_DIR}/lofar/sas/frontend/tmss_webapp/build) diff --git a/SAS/TMSS/frontend/tmss_webapp/package.json b/SAS/TMSS/frontend/tmss_webapp/package.json index b5328e779ebfa77c91b24d16cf90c040bc06e3a4..41a95ba47c18c1fed93dadfcf8949dfc9b4e39d9 100644 --- a/SAS/TMSS/frontend/tmss_webapp/package.json +++ b/SAS/TMSS/frontend/tmss_webapp/package.json @@ -3,17 +3,25 @@ "version": "0.1.0", "private": true, "dependencies": { + "@fortawesome/fontawesome-free": "^5.13.1", "@json-editor/json-editor": "^2.3.0", "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^7.1.2", "axios": "^0.19.2", + "bootstrap": "^4.5.0", + "cleave.js": "^1.6.0", + "flatpickr": "^4.6.3", "font-awesome": "^4.7.0", "history": "^5.0.0", + "lodash": "^4.17.19", + "match-sorter": "^4.1.0", + "moment": "^2.27.0", "node-sass": "^4.12.0", "primeflex": "^1.3.0", "primeicons": "^4.0.0", "primereact": "^4.2.2", + "prop-types": "^15.7.2", "react": "^16.13.1", "react-app-polyfill": "^1.0.6", "react-bootstrap": "^1.0.1", @@ -21,9 +29,13 @@ "react-dom": "^16.13.1", "react-frame-component": "^4.1.2", "react-json-view": "^1.19.1", + "react-loader-spinner": "^3.1.14", "react-router-dom": "^5.2.0", "react-scripts": "3.4.1", - "react-transition-group": "^1.2.1", + "react-table": "^7.2.1", + "react-transition-group": "^2.5.1", + "reactstrap": "^8.5.1", + "styled-components": "^5.1.1", "typescript": "^3.9.5", "yup": "^0.29.1" }, @@ -33,6 +45,7 @@ "test": "react-scripts test", "eject": "react-scripts eject" }, + "proxy": "http://127.0.0.1:8008/", "eslintConfig": { "extends": "react-app" }, @@ -47,5 +60,9 @@ "last 1 firefox version", "last 1 safari version" ] + }, + "devDependencies": { + "customize-cra": "^0.9.1", + "react-app-rewired": "^1.6.2" } } diff --git a/SAS/TMSS/frontend/tmss_webapp/public/manifest.json b/SAS/TMSS/frontend/tmss_webapp/public/manifest.json index 080d6c77ac21bb2ef88a6992b2b73ad93daaca92..1f2f141fafdeb1d31d85b008ec5132840c5e6362 100644 --- a/SAS/TMSS/frontend/tmss_webapp/public/manifest.json +++ b/SAS/TMSS/frontend/tmss_webapp/public/manifest.json @@ -6,16 +6,6 @@ "src": "favicon.ico", "sizes": "64x64 32x32 24x24 16x16", "type": "image/x-icon" - }, - { - "src": "logo192.png", - "type": "image/png", - "sizes": "192x192" - }, - { - "src": "logo512.png", - "type": "image/png", - "sizes": "512x512" } ], "start_url": ".", diff --git a/SAS/TMSS/frontend/tmss_webapp/src/App.css b/SAS/TMSS/frontend/tmss_webapp/src/App.css index 18ccb9475ca09cfadf5cd73e300c156090c9812f..766fff47baad6747a35c03a164125f8d181f5956 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/App.css +++ b/SAS/TMSS/frontend/tmss_webapp/src/App.css @@ -29,6 +29,177 @@ color: #61dafb; } +label { + font-size: 14px; + font-weight: 600; + align-items: flex-start !important; + color: #004B93; + margin-bottom: 10px; +} + +h2, .h2 { + font-size: 1.5rem; +} + +h3, .h3 { + font-size: 1.25rem; +} + +h5, .h5 { + font-size: 15px !important; +} + +a{ + margin-bottom: 10px; +} + +.main-content span,a{ + font-size: 14px; +} + +#editor_holder { + padding: 0px 10px; +} + +.chips-readonly > ul { + border: none; +} + +p { + font-size: 14px; +} + +.card { + border: none; +} + +.card-title { + margin-bottom: 0.5rem; +} + +.card-body { + padding: 0.25rem; + margin-bottom: 5px !important; +} + +.form-group { + margin-bottom: 5px !important; +} + +.btn-group { + margin-bottom: 5px; +} + +.p-field { + margin-bottom: 0.5rem; +} + +.p-inputtext { + padding-top: 0.25em !important; + padding-bottom: 0.25em !important; + padding-left: 0.25em !important; +} + +.act-btn-grp { + margin-top: 20px; +} + +.task-list { + padding-inline-start: 0px; +} + +.task-list > li { + list-style: none; +} + +.col-filter-btn { + margin-left: 15px; + padding-top: 5px; + cursor: pointer; +} + +#editor_holder label { + text-transform: capitalize; + color: #28289b; +} + +.p-multiselect-label { + margin-bottom: 0px; +} + +.resource-input-grid div { + margin-bottom: 1rem; +} + +.layout-main .fa { + color: #005b9f; + font-size: 20px; +} + +thead { + background-color: #ebeaea; +} + +.error { + color: #dc3545; + font-size: 80%; + font-weight: 400; +} + +.info { + color: #b4b2b2; + font-size: 80%; + font-weight: 400; +} + +.input-error { + border-color: #dc3545 !important; +} + +.p-tooltip-custom .p-tooltip-text { + /* background-color: #49b0ff !important; */ +} + +.p-link:focus { + box-shadow: none !important; +} + +.pi-primary { + color: #007ad9; +} + +.pi-warning { + color: #ffba01; +} + +.pi-success { + color: #34A835; +} + +.pi-info { + color: #008fba; +} + +.pi-error { + color: #e91224; +} + +.pi-small { + font-size: rem !important; +} + +.pi-medium { + font-size: 1.5rem !important; +} + +.pi-large { + font-size: 2rem !important; +} + +.pi-x-large { + font-size: 3rem !important; +} + @keyframes App-logo-spin { from { transform: rotate(0deg); diff --git a/SAS/TMSS/frontend/tmss_webapp/src/App.js b/SAS/TMSS/frontend/tmss_webapp/src/App.js index 0d607d738c330273f30c7f9dc5a6421c4a0069c7..4445c3bf0ce5c6d9662006321657cfadf6df59fe 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/App.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/App.js @@ -1,60 +1,120 @@ import React, {Component} from 'react'; import { BrowserRouter as Router } from 'react-router-dom'; - +import classNames from 'classnames'; import {AppTopbar} from './layout/components/AppTopbar'; import {AppMenu} from './layout/components/AppMenu'; -import { AppFooter } from './layout/components/AppFooter'; +import {AppFooter } from './layout/components/AppFooter'; import {RoutedContent} from './routes'; +import {AppBreadcrumb } from "./layout/components/AppBreadcrumb"; +import {withRouter } from 'react-router'; -// import {Dashboard} from './routes/dashboard/dashboard'; - +import 'primeicons/primeicons.css'; +import 'primereact/resources/themes/nova-light/theme.css'; +import 'primereact/resources/primereact.css'; import './layout/layout.scss'; -// import './App.css'; - +import 'primeflex/primeflex.css'; +import './App.scss'; +import './App.css'; class App extends Component { - - constructor() { + constructor() { super(); this.state = { + layoutMode: 'static', currentMenu: '', - currentPath: '/' - } - this.onMenuItemClick = this.onMenuItemClick.bind(this) - this.menu = [ + currentPath: '/', + staticMenuInactive: false, + overlayMenuActive: false, + mobileMenuActive: false, + }; + this.onWrapperClick = this.onWrapperClick.bind(this); + this.onToggleMenu = this.onToggleMenu.bind(this); + this.onSidebarClick = this.onSidebarClick.bind(this); + this.onMenuItemClick = this.onMenuItemClick.bind(this); + + this.menu = [ {label: 'Dashboard', icon: 'pi pi-fw pi-home', to:'/dashboard'}, - {label: 'Scheduling Units', icon: 'pi pi-fw pi-calendar', to:'/scheduling'} + {label: 'Scheduling Units', icon: 'pi pi-fw pi-calendar', to:'/schedulingunit'}, + {label: 'Tasks', icon: 'pi pi-fw pi-check-square', to:'/task'}, + {label: 'Project', icon: 'fa fa-fw fa-binoculars', to:'/project'} ]; // this.menuComponent = {'Dashboard': Dashboard} } + + onWrapperClick(event) { + if (!this.menuClick) { + this.setState({ + overlayMenuActive: false, + mobileMenuActive: false + }); + } - onMenuItemClick(event) { - console.log(event); - this.setState({currentMenu:event.item.label, currentPath: event.item.path}); - } + this.menuClick = false; + } + onToggleMenu(event) { + this.menuClick = true; + if (this.isDesktop()) { + if (this.state.layoutMode === 'overlay') { + this.setState({ + overlayMenuActive: !this.state.overlayMenuActive + }); + } + else if (this.state.layoutMode === 'static') { + this.setState({ + staticMenuInactive: !this.state.staticMenuInactive + }); + } + } + else { + const mobileMenuActive = this.state.mobileMenuActive; + this.setState({ + mobileMenuActive: !mobileMenuActive + }); + } + event.preventDefault(); + } + + onSidebarClick(event) { + this.menuClick = true; + } + + onMenuItemClick(event) { + this.setState({currentMenu:event.item.label, currentPath: event.item.path}); + } + + isDesktop() { + return window.innerWidth > 1024; + } + render() { - return ( + const wrapperClass = classNames('layout-wrapper', { + 'layout-overlay': this.state.layoutMode === 'overlay', + 'layout-static': this.state.layoutMode === 'static', + 'layout-static-sidebar-inactive': this.state.staticMenuInactive && this.state.layoutMode === 'static', + 'layout-overlay-sidebar-active': this.state.overlayMenuActive && this.state.layoutMode === 'overlay', + 'layout-mobile-sidebar-active': this.state.mobileMenuActive + }); + const AppBreadCrumbWithRouter = withRouter(AppBreadcrumb); + + return ( <React.Fragment> - <div className="App"> - - <AppTopbar></AppTopbar> - <Router basename={ this.state.currentPath }> - <AppMenu model={this.menu} onMenuItemClick={this.onMenuItemClick} /> - <div className="layout-wrapper layout-static layout-static-sidebar-active"> + <div className="App"> + {/* <div className={wrapperClass} onClick={this.onWrapperClick}> */} + <div className={wrapperClass}> + <AppTopbar onToggleMenu={this.onToggleMenu}></AppTopbar> + <Router basename={ this.state.currentPath }> + <AppMenu model={this.menu} onMenuItemClick={this.onMenuItemClick} /> <div className="layout-main"> - - <RoutedContent /> - + <AppBreadCrumbWithRouter/> + <RoutedContent /> </div> + </Router> + <AppFooter></AppFooter> + </div> </div> - </Router> - <AppFooter></AppFooter> - - - </div> - </React.Fragment> + </React.Fragment> ); } } diff --git a/SAS/TMSS/frontend/tmss_webapp/src/App.scss b/SAS/TMSS/frontend/tmss_webapp/src/App.scss new file mode 100644 index 0000000000000000000000000000000000000000..93c4b1c7a72602ac2fa3e7d2a16fd445736e30e9 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/App.scss @@ -0,0 +1 @@ +@import "~bootstrap/scss/bootstrap"; \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/__mocks__/project.service.data.js b/SAS/TMSS/frontend/tmss_webapp/src/__mocks__/project.service.data.js new file mode 100644 index 0000000000000000000000000000000000000000..066fd339758c56e64cc51a2ae81142eb978a583d --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/__mocks__/project.service.data.js @@ -0,0 +1,315 @@ + +const ProjectServiceMock= { + project_categories: [{url: "Regular", value: 'Regular'}, {url: "User Shared Support", value: 'User Shared Support'}], + period_categories: [{url: "Single Cycle", value: 'Single Cycle'}, {url: "Long Term", value: 'Long Term'}], + resources: [{ + "name": "LOFAR Observing Time", + "url": "http://localhost:3000/api/resource_type/LOFAR%20Observing%20Time/", + "created_at": "2020-07-29T07:31:21.708296", + "description": "LOFAR Observing Time", + "resource_unit": "http://localhost:3000/api/resource_unit/second/", + "resource_unit_id": "second", + "tags": [ + ], + "updated_at": "2020-07-29T07:31:21.708316", + "resourceUnit": { + "name": "second", + "url": "http://localhost:3000/api/resource_unit/second/", + "created_at": "2020-07-29T07:31:21.070088", + "description": "Unit of time or duration", + "tags": [ + ], + "updated_at": "2020-07-29T07:31:21.070114" + } + }, + { + "name": "LOFAR Observing Time prio A", + "url": "http://localhost:3000/api/resource_type/LOFAR%20Observing%20Time%20prio%20A/", + "created_at": "2020-07-29T07:31:21.827537", + "description": "LOFAR Observing Time prio A", + "resource_unit": "http://localhost:3000/api/resource_unit/second/", + "resource_unit_id": "second", + "tags": [ + ], + "updated_at": "2020-07-29T07:31:21.827675", + "resourceUnit": { + "name": "second", + "url": "http://localhost:3000/api/resource_unit/second/", + "created_at": "2020-07-29T07:31:21.070088", + "description": "Unit of time or duration", + "tags": [ + ], + "updated_at": "2020-07-29T07:31:21.070114" + } + }, + { + "name": "LOFAR Observing Time prio B", + "url": "http://localhost:3000/api/resource_type/LOFAR%20Observing%20Time%20prio%20B/", + "created_at": "2020-07-29T07:31:21.950948", + "description": "LOFAR Observing Time prio B", + "resource_unit": "http://localhost:3000/api/resource_unit/second/", + "resource_unit_id": "second", + "tags": [ + ], + "updated_at": "2020-07-29T07:31:21.950968", + "resourceUnit": { + "name": "second", + "url": "http://localhost:3000/api/resource_unit/second/", + "created_at": "2020-07-29T07:31:21.070088", + "description": "Unit of time or duration", + "tags": [ + ], + "updated_at": "2020-07-29T07:31:21.070114" + } + }, + { + "name": "CEP Processing Time", + "url": "http://localhost:3000/api/resource_type/CEP%20Processing%20Time/", + "created_at": "2020-07-29T07:31:22.097916", + "description": "CEP Processing Time", + "resource_unit": "http://localhost:3000/api/resource_unit/second/", + "resource_unit_id": "second", + "tags": [ + ], + "updated_at": "2020-07-29T07:31:22.097941", + "resourceUnit": { + "name": "second", + "url": "http://localhost:3000/api/resource_unit/second/", + "created_at": "2020-07-29T07:31:21.070088", + "description": "Unit of time or duration", + "tags": [ + ], + "updated_at": "2020-07-29T07:31:21.070114" + } + }, + { + "name": "LTA Storage", + "url": "http://localhost:3000/api/resource_type/LTA%20Storage/", + "created_at": "2020-07-29T07:31:22.210071", + "description": "LTA Storage", + "resource_unit": "http://localhost:3000/api/resource_unit/byte/", + "resource_unit_id": "byte", + "tags": [ + ], + "updated_at": "2020-07-29T07:31:22.210091", + "resourceUnit": { + "name": "byte", + "url": "http://localhost:3000/api/resource_unit/byte/", + "created_at": "2020-07-29T07:31:21.500997", + "description": "Unit of data storage", + "tags": [ + ], + "updated_at": "2020-07-29T07:31:21.501028" + } + }, + { + "name": "Number of triggers", + "url": "http://localhost:3000/api/resource_type/Number%20of%20triggers/", + "created_at": "2020-07-29T07:31:22.317313", + "description": "Number of triggers", + "resource_unit": "http://localhost:3000/api/resource_unit/number/", + "resource_unit_id": "number", + "tags": [ + ], + "updated_at": "2020-07-29T07:31:22.317341", + "resourceUnit": { + "name": "number", + "url": "http://localhost:3000/api/resource_unit/number/", + "created_at": "2020-07-29T07:31:21.596364", + "description": "Unit of count", + "tags": [ + ], + "updated_at": "2020-07-29T07:31:21.596385" + } + }, + { + "name": "LOFAR Support Time", + "url": "http://localhost:3000/api/resource_type/LOFAR%20Support%20Time/", + "created_at": "2020-07-29T07:31:22.437945", + "description": "LOFAR Support Time", + "resource_unit": "http://localhost:3000/api/resource_unit/second/", + "resource_unit_id": "second", + "tags": [ + ], + "updated_at": "2020-07-29T07:31:22.437964", + "resourceUnit": { + "name": "second", + "url": "http://localhost:3000/api/resource_unit/second/", + "created_at": "2020-07-29T07:31:21.070088", + "description": "Unit of time or duration", + "tags": [ + ], + "updated_at": "2020-07-29T07:31:21.070114" + } + }, + { + "name": "LOFAR Support hours", + "url": "http://localhost:3000/api/resource_type/LOFAR%20Support%20hours/", + "created_at": "2020-07-29T07:31:22.571850", + "description": "LOFAR Support hours", + "resource_unit": "http://localhost:3000/api/resource_unit/second/", + "resource_unit_id": "second", + "tags": [ + ], + "updated_at": "2020-07-29T07:31:22.571869", + "resourceUnit": { + "name": "second", + "url": "http://localhost:3000/api/resource_unit/second/", + "created_at": "2020-07-29T07:31:21.070088", + "description": "Unit of time or duration", + "tags": [ + ], + "updated_at": "2020-07-29T07:31:21.070114" + } + }, + { + "name": "Support hours", + "url": "http://localhost:3000/api/resource_type/Support%20hours/", + "created_at": "2020-07-29T07:31:22.694438", + "description": "Support hours", + "resource_unit": "http://localhost:3000/api/resource_unit/second/", + "resource_unit_id": "second", + "tags": [ + ], + "updated_at": "2020-07-29T07:31:22.694514", + "resourceUnit": { + "name": "second", + "url": "http://localhost:3000/api/resource_unit/second/", + "created_at": "2020-07-29T07:31:21.070088", + "description": "Unit of time or duration", + "tags": [ + ], + "updated_at": "2020-07-29T07:31:21.070114" + } + } + ], + projectResourceDefaults: { + 'LOFAR Observing Time': 3600, + 'LOFAR Observing Time prio A': 3600, + 'LOFAR Observing Time prio B': 3600, + 'CEP Processing Time': 3600, + 'LTA Storage': 1024*1024*1024*1024, + 'Number of triggers': 1, + 'LOFAR Support Time': 3600 + }, + project: [{ + "name": "OSR-11", + "url": "http://192.168.99.100:8008/api/project/OSR-11/", + "can_trigger": true, + "created_at": "2020-07-29T18:20:06.187276", + "cycles": [ + "http://192.168.99.100:8008/api/cycle/Cycle%200/" + ], + "cycles_ids": [ + "Cycle 0" + ], + "description": "OSR-11", + "expert": false, + "filler": false, + "period_category": "Single Cycle", + "period_category_value": "Single Cycle", + "priority_rank": 5, + "private_data": true, + "project_category": "Regular", + "project_category_value": "Regular", + "project_quota": [ + "http://192.168.99.100:8008/api/project_quota/70/", + "http://192.168.99.100:8008/api/project_quota/71/", + "http://192.168.99.100:8008/api/project_quota/72/", + "http://192.168.99.100:8008/api/project_quota/73/", + "http://192.168.99.100:8008/api/project_quota/74/", + "http://192.168.99.100:8008/api/project_quota/75/", + "http://192.168.99.100:8008/api/project_quota/76/", + "http://192.168.99.100:8008/api/project_quota/77/" + ], + "project_quota_ids": [ + 70, + 71, + 72, + 73, + 74, + 75, + 76, + 77 + ], + "tags": [], + "trigger_priority": 990, + "updated_at": "2020-07-29T18:20:06.187342" + }], + projectQuota: [ + { + "id": 70, + "url": "http://192.168.99.100:8008/api/project_quota/70/", + "project": "http://192.168.99.100:8008/api/project/OSR-11/", + "project_id": "OSR-11", + "resource_type": "http://192.168.99.100:8008/api/resource_type/CEP%20Processing%20Time/", + "resource_type_id": "CEP Processing Time", + "value": 36000 + }, + { + "id": 71, + "url": "http://192.168.99.100:8008/api/project_quota/71/", + "project": "http://192.168.99.100:8008/api/project/OSR-11/", + "project_id": "OSR-11", + "resource_type": "http://192.168.99.100:8008/api/resource_type/LOFAR%20Observing%20Time/", + "resource_type_id": "LOFAR Observing Time", + "value": 72000 + }, + { + "id": 72, + "url": "http://192.168.99.100:8008/api/project_quota/72/", + "project": "http://192.168.99.100:8008/api/project/OSR-11/", + "project_id": "OSR-11", + "resource_type": "http://192.168.99.100:8008/api/resource_type/LOFAR%20Observing%20Time%20prio%20A/", + "resource_type_id": "LOFAR Observing Time prio A", + "value": 108000 + }, + { + "id": 73, + "url": "http://192.168.99.100:8008/api/project_quota/73/", + "project": "http://192.168.99.100:8008/api/project/OSR-11/", + "project_id": "OSR-11", + "resource_type": "http://192.168.99.100:8008/api/resource_type/LOFAR%20Observing%20Time%20prio%20B/", + "resource_type_id": "LOFAR Observing Time prio B", + "value": 144000 + }, + { + "id": 74, + "url": "http://192.168.99.100:8008/api/project_quota/74/", + "project": "http://192.168.99.100:8008/api/project/OSR-11/", + "project_id": "OSR-11", + "resource_type": "http://192.168.99.100:8008/api/resource_type/LOFAR%20Support%20Time/", + "resource_type_id": "LOFAR Support Time", + "value": 180000 + }, + { + "id": 75, + "url": "http://192.168.99.100:8008/api/project_quota/75/", + "project": "http://192.168.99.100:8008/api/project/OSR-11/", + "project_id": "OSR-11", + "resource_type": "http://192.168.99.100:8008/api/resource_type/LTA%20Storage/", + "resource_type_id": "LTA Storage", + "value": 6597069766656 + }, + { + "id": 76, + "url": "http://192.168.99.100:8008/api/project_quota/76/", + "project": "http://192.168.99.100:8008/api/project/OSR-11/", + "project_id": "OSR-11", + "resource_type": "http://192.168.99.100:8008/api/resource_type/Number%20of%20triggers/", + "resource_type_id": "Number of triggers", + "value": 7 + }, + { + "id": 77, + "url": "http://192.168.99.100:8008/api/project_quota/77/", + "project": "http://192.168.99.100:8008/api/project/OSR-11/", + "project_id": "OSR-11", + "resource_type": "http://192.168.99.100:8008/api/resource_type/Support%20hours/", + "resource_type_id": "Support hours", + "value": 8 + } + ] +} + +export default ProjectServiceMock; \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/components/ActionComponents.js b/SAS/TMSS/frontend/tmss_webapp/src/components/ActionComponents.js new file mode 100644 index 0000000000000000000000000000000000000000..777784715bc34c41fd5cf29f0dfddafe3a365334 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/components/ActionComponents.js @@ -0,0 +1,73 @@ +export { + Alert, + Badge, + Breadcrumb, + BreadcrumbItem, + Button, + ButtonDropdown, + ButtonGroup, + ButtonToolbar, + CardBody, + CardColumns, + CardDeck, + CardFooter, + CardGroup, + CardImg, + CardImgOverlay, + CardLink, + CardSubtitle, + CardText, + CardTitle, + Carousel, + CarouselCaption, + CarouselControl, + CarouselIndicators, + CarouselItem, + Col, + Collapse, + Container, + Dropdown, + DropdownItem, + DropdownMenu, + DropdownToggle, + Fade, + Form, + FormFeedback, + FormGroup, + FormText, + Input, + InputGroup, + InputGroupButtonDropdown, + InputGroupText, + Jumbotron, + Label, + ListGroup, + ListGroupItem, + ListGroupItemHeading, + ListGroupItemText, + Media, + Modal, + ModalBody, + ModalFooter, + ModalHeader, + NavbarBrand, + NavbarToggler, + NavItem, + NavLink, + Pagination, + PaginationItem, + PaginationLink, + Popover, + PopoverBody, + PopoverHeader, + Row, + TabContent, + Table, + TabPane, + Tooltip, + UncontrolledAlert, + UncontrolledButtonDropdown, + UncontrolledDropdown, + UncontrolledCollapse, + UncontrolledTooltip +} from 'reactstrap'; diff --git a/SAS/TMSS/frontend/tmss_webapp/src/components/JSONEditor/JEditor.js b/SAS/TMSS/frontend/tmss_webapp/src/components/JSONEditor/JEditor.js new file mode 100644 index 0000000000000000000000000000000000000000..db176a3dec57157b173ab4491ecc03722c2a2200 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/components/JSONEditor/JEditor.js @@ -0,0 +1,530 @@ +/** + * This is the custom component to use "@json-editor/json-editor" + * to create form using JSON Schema and get JSON output + */ +import React, {useEffect, useRef} from 'react'; +import _ from 'lodash'; +import flatpickr from 'flatpickr'; + +import "@fortawesome/fontawesome-free/css/all.css"; +import "flatpickr/dist/flatpickr.css"; +const JSONEditor = require("@json-editor/json-editor").JSONEditor; + +function Jeditor(props) { + // console.log("In JEditor"); + const editorRef = useRef(null); + let editor = null; + useEffect(() => { + const element = document.getElementById('editor_holder'); + let schema = {}; + Object.assign(schema, props.schema?props.schema:{}); + + // Customize the pointing property to capture angle1 and angle2 to specified format + for (const definitionKey in schema.definitions) { + if (definitionKey === 'pointing') { + const defintion = schema.definitions[definitionKey]; + let defProperties = defintion.properties; + if (defProperties) { + for (const propName in defProperties) { + if (propName === 'angle1' || propName === 'angle2') { + let defProperty = getAngleProperty(defProperties[propName], propName === 'angle2'); + defProperties[propName] = defProperty; + } + if (defProperties[propName].options) { + defProperties[propName].options.grid_columns = 4; + } else { + defProperties[propName].options = {grid_columns: 4}; + } + } + } + } + } + + // Customize datatype of certain properties like subbands, duration, etc., + getCustomProperties(schema.properties); + + schema.title = props.title; + const subbandValidator = validateSubbandOutput; + const timeValidator = validateTime; + const angleValidator = validateAngle; + JSONEditor.defaults.custom_validators.push((schema, value, path) => { + const errors = []; + if (schema.validationType === "subband_list") { + if (!subbandValidator(value)) { + errors.push({ + path: path, + property: 'validationType', + message: 'Not a valid input for Subband List' + }); + } + } else if (schema.validationType === "time") { + if (!timeValidator(value)) { + errors.push({ + path: path, + property: 'validationType', + message: 'Not a valid input. Mimimum: 00:00:00, Maximum:23:59:59' + }); + } + } else if (schema.validationType === "angle") { + if (!angleValidator(value)) { + errors.push({ + path: path, + property: 'validationType', + message: 'Not a valid input. Mimimum: 00:00:00, Maximum:90:00:00' + }); + } + } + return errors; + }); + schema.format = "grid" + const editorOptions = { + form_name_root: "specification", + schema: schema, + theme: 'bootstrap4', + iconlib: 'fontawesome5', + // theme: 'tailwind', + // iconlib: 'spectre', + display_required_only: false, + remove_button_labels: true, + disable_edit_json: true, + disable_properties: true, + disable_collapse: true, + compact: true + }; + // Set Initial value to the editor + if (props.initValue) { + editorOptions.startval = updateInput(_.cloneDeep(props.initValue)); + } + editor = new JSONEditor(element, editorOptions); + // editor.getEditor('root').disable(); + if (props.disabled) { + editor.disable(); + } + if (props.parentFunction) { + props.parentFunction(editorFunction); + } + editorRef.current = editor; + editor.on('change', () => {setEditorOutput()}); + }, [props.schema]); + + /** + * Function to call on button click and send the output back to parent through callback + * + */ + function setEditorOutput(){ + const editorOutput = editorRef.current.getValue(); + const formattedOutput = updateOutput(_.cloneDeep(editorOutput)); + const editorValidationErrors = editorRef.current.validate(); + if (props.callback) { + props.callback(formattedOutput, editorValidationErrors); + } + } + + /** + * Function called by the parent component to perform certain action ib JEditor + */ + function editorFunction() { + editorRef.current.destroy(); + } + + /** + * Function to convert the angle fields in HH:mm:ss or DD:mm:ss format based on isDegree value. + * @param {Object} defProperty + * @param {Boolean} isDegree + */ + function getAngleProperty(defProperty, isDegree) { + /*let newProperty = { + "type": "object", + "additionalProperties": false, + "format": "grid", + // "title": defProperty.title, + // "description": defProperty.description}; + "title": "Duration", + "description": "Duration of the observation"}; + let subProperties = {}; + if (isDegree) { + subProperties["dd"] = { "type": "number", + "title": "DD", + "description": "Degrees", + "default": 0, + "minimum": 0, + "maximum": 90 }; + } else { + subProperties["hh"] = { "type": "number", + "title": "HH", + "description": "Hours", + "default": 0, + "minimum": 0, + "maximum": 23 }; + + } + subProperties["mm"] = { "type": "number", + "title": "MM", + "description": "Minutes", + "default": 0, + "minimum": 0, + "maximum": 59 }; + subProperties["ss"] = { "type": "number", + "title": "SS", + "description": "Seconds", + "default": 0, + "minimum": 0, + "maximum": 59 }; + + newProperty.properties = subProperties; + newProperty.required = isDegree?["dd", "mm", "ss"]:["hh", "mm", "ss"];*/ + let newProperty = { + type: "string", + title: defProperty.title, + description: (defProperty.description + (isDegree?'(Degrees:Minutes:Seconds)':'(Hours:Minutes:Seconds)')), + default: "00:00:00", + validationType: isDegree?'angle':'time', + options: { + "grid_columns": 4, + "inputAttributes": { + "placeholder": isDegree?"DD:mm:ss":"HH:mm:ss" + }, + "cleave": { + date: true, + datePattern: ['HH','mm','ss'], + delimiter: ':' + } + } + } + return newProperty; + } + + /** + * Function to get the schema change for specified properties like subbands, duration, column width, etc + * @param {Object} properties + */ + function getCustomProperties(properties) { + for (const propertyKey in properties) { + const propertyValue = properties[propertyKey]; + if (propertyKey === 'subbands') { + let newProperty = {}; + newProperty.additionalItems = false; + newProperty.title = propertyValue.title; + newProperty.type = 'string'; + newProperty.default = ''; + newProperty.description = "For Range enter Start and End seperated by 2 dots. Mulitple ranges can be separated by comma. Minimum should be 0 and maximum should be 511. For exmaple 11..20, 30..50"; + newProperty.validationType = 'subband_list'; + // newProperty.options = { + // grid_columns: 4 + // }; + properties[propertyKey] = newProperty; + } else if (propertyKey.toLowerCase() === 'duration') { + /*propertyValue.title = "Duration (minutes)"; + propertyValue.default = "1"; + propertyValue.description = "Duration of this observation. Enter in decimal for seconds. For example 0.5 for 30 seconds"; + propertyValue.minimum = 0.25; + propertyValue.options = { + grid_columns: 6 + };*/ + /*propertyValue.title = "Duration"; + propertyValue.default = "1H20M30S"; + propertyValue.type = "string"; + propertyValue.description = "Duration of the observation (H-hours,M-minutes,S-seconds & should be in the order of H, M and S respectively)"; + /*let newProperty = { + type: "string", + title: "Duration", + description: `${propertyValue.description} (Hours:Minutes:Seconds)`, + default: "00:00:00", + "options": { + "grid_columns": 5, + "inputAttributes": { + "placeholder": "HH:mm:ss" + }, + "cleave": { + date: true, + datePattern: ['HH','mm','ss'], + delimiter: ':' + } + } + }*/ + let newProperty = { + "type": "string", + "format": "time", + "title": "Duration", + "description": `${propertyValue.description} (Hours:Minutes:Seconds)`, + "options": { + "grid_columns": 3, + "inputAttributes": { + "placeholder": "Enter time" + }, + "flatpickr": { + "wrap": true, + "showClearButton": false, + "inlineHideInput": true, + "defaultHour": 0, + "defaultMinute": 1, + "enableSeconds": true, + "defaultSecond": 0, + "hourIncrement": 1, + "minuteIncrement": 1, + "secondIncrement": 5, + "time_24hr": true, + "allowInput": true + } + } + }; + + properties[propertyKey] = newProperty; + } else if (propertyValue instanceof Object) { + if (propertyKey !== 'properties' && propertyKey !== 'default') { + propertyValue.format = "grid"; + } + if (propertyKey === 'average' || propertyKey === 'calibrator' || propertyKey === 'stations') { + propertyValue.propertyOrder = 1; + } else if (propertyKey === 'demix') { + propertyValue.propertyOrder = 2; + } else if (propertyKey === 'QA' || propertyKey === 'beams') { + propertyValue.propertyOrder = 10000; + } + if (propertyKey === 'storage_cluster' || propertyKey === 'integration_time' || propertyKey === 'storage_manager') { + let options = propertyValue.options?propertyValue.options:{}; + options.grid_columns = 3; + propertyValue.options = options; + } else if (propertyKey === 'flag') { + let options = propertyValue.options?propertyValue.options:{}; + options.grid_columns = 9; + propertyValue.options = options; + } + getCustomProperties(propertyValue); + } + } + } + + /** + * Function to format the input for custom fields when the editor receive the inital values from the parent component + * @param {*} editorInput + */ + function updateInput(editorInput) { + for (const inputKey in editorInput) { + const inputValue = editorInput[inputKey]; + if (inputValue instanceof Object) { + if (inputKey.endsWith('pointing')) { + inputValue.angle1 = getAngleInput(inputValue.angle1); + inputValue.angle2 = getAngleInput(inputValue.angle2, true); + } else if (inputKey === 'subbands') { + editorInput[inputKey] = getSubbandInput(inputValue); + } else { + updateInput(inputValue); + } + } else if (inputKey.toLowerCase() === 'duration') { + // editorInput[inputKey] = inputValue/60; + editorInput[inputKey] = getTimeInput(inputValue); + } + } + return editorInput; + } + + /** + * Function to format the output of the customized fields + * @param {*} editorOutput + */ + function updateOutput(editorOutput) { + for (const outputKey in editorOutput) { + let outputValue = editorOutput[outputKey]; + if (outputValue instanceof Object) { + if (outputKey.endsWith('pointing')) { + outputValue.angle1 = getAngleOutput(outputValue.angle1, false); + outputValue.angle2 = getAngleOutput(outputValue.angle2, true); + } else { + updateOutput(outputValue); + } + } else if (outputKey === 'subbands') { + editorOutput[outputKey] = getSubbandOutput(outputValue); + } else if (outputKey.toLowerCase() === 'duration') { + // editorOutput[outputKey] = outputValue * 60; + const splitOutput = outputValue.split(':'); + editorOutput[outputKey] = (splitOutput[0] * 3600 + splitOutput[1] * 60 + splitOutput[2]*1); + } + } + return editorOutput; + } + + /** + * Function to format angle values in the input of inital values + * @param {*} prpInput + * @param {Boolean} isDegree + */ + function getAngleInput(prpInput, isDegree) { + const degrees = prpInput * 180 / Math.PI; + if (isDegree) { + const dd = Math.floor(prpInput * 180 / Math.PI); + const mm = Math.floor((degrees-dd) * 60); + const ss = +((degrees-dd-(mm/60)) * 3600).toFixed(0); + /*return { + dd: dd, + mm: mm, + ss: ss + }*/ + return (dd<10?`0${dd}`:`${dd}`) + ':' + (mm<10?`0${mm}`:`${mm}`) + ':' + (ss<10?`0${ss}`:`${ss}`); + } else { + const hh = Math.floor(degrees/15); + const mm = Math.floor((degrees - (hh*15))/15 * 60 ); + const ss = +((degrees -(hh*15)-(mm*15/60))/15 * 3600).toFixed(0); + /*return { + hh: hh, + mm: mm, + ss: ss + }*/ + return (hh<10?`0${hh}`:`${hh}`) + ':' + (mm<10?`0${mm}`:`${mm}`) + ':' + (ss<10?`0${ss}`:`${ss}`); + } + } + + /** + * Function to format subband list inout arrived as Array to String + * @param {Array} prpInput + */ + function getSubbandInput(prpInput) { + let subbandString = ""; + for (let index=0; index < prpInput.length; index++) { + if (subbandString.length > 0) { + subbandString += ","; + } + let firstVal = prpInput[index] + let nextVal = prpInput[index]; + if (prpInput[index+1] - nextVal === 1) { + subbandString += firstVal + ".."; + while( prpInput[index+1] - nextVal === 1) { + index++; + nextVal = prpInput[index]; + } + subbandString += nextVal; + } else { + subbandString += firstVal; + } + } + return subbandString; + } + + /** + * Convert time value in seconds to string format of HH:mm:ss + * @param {Number} seconds + */ + function getTimeInput(seconds) { + const hh = Math.floor(seconds/3600); + const mm = Math.floor((seconds - hh*3600) / 60 ); + const ss = +((seconds -(hh*3600)-(mm*60)) / 1); + return (hh<10?`0${hh}`:`${hh}`) + ':' + (mm<10?`0${mm}`:`${mm}`) + ':' + (ss<10?`0${ss}`:`${ss}`); + } + + /** + * Converts the angle input to radians + * @param {String} prpOutput + * @param {Boolean} isDegree + */ + function getAngleOutput(prpOutput, isDegree) { + /*if ('dd' in prpOutput) { + return ((prpOutput.dd + prpOutput.mm/60 + prpOutput.ss/3600)*Math.PI/180); + } else { + return ((prpOutput.hh*15 + prpOutput.mm/4 + prpOutput.ss/240)*Math.PI/180); + }*/ + const splitOutput = prpOutput.split(':'); + if (isDegree) { + return ((splitOutput[0]*1 + splitOutput[1]/60 + splitOutput[2]/3600)*Math.PI/180); + } else { + return ((splitOutput[0]*15 + splitOutput[1]/4 + splitOutput[2]/240)*Math.PI/180); + } + } + + /** + * Validate time entered as string in HH:mm:ss format + * @param {String} prpOutput + */ + function validateTime(prpOutput) { + const splitOutput = prpOutput.split(':'); + if (splitOutput.length < 3) { + return false; + } else { + const timeValue = parseInt(splitOutput[0]*60*60) + parseInt(splitOutput[1]*60) + parseInt(splitOutput[2]); + if (timeValue >= 86400) { + return false; + } + } + return true; + } + + /** + * Validate angle input to not exceed 90 degrees + * @param {String} prpOutput + */ + function validateAngle(prpOutput) { + const splitOutput = prpOutput.split(':'); + if (splitOutput.length < 3) { + return false; + } else { + const timeValue = parseInt(splitOutput[0]*60*60) + parseInt(splitOutput[1]*60) + parseInt(splitOutput[2]); + if (timeValue > 324000) { + return false; + } + } + return true; + } + + /** + * Validates if the subband list custom field + * @param {String} prpOutput + */ + function validateSubbandOutput(prpOutput){ + try { + if (prpOutput) { + const subbandArray = prpOutput.split(","); + for (const subband of subbandArray ) { + const subbandRange = subband.split('..'); + if (subbandRange.length > 1) { + const firstVal = parseInt(subbandRange[0]); + const nextVal = parseInt(subbandRange[1]) + 1; + if (isNaN(firstVal * nextVal) || firstVal < 0 || firstVal > 510 + || nextVal < 0 || nextVal > 511 + || firstVal >nextVal) { + return false; + } + } else { + if (isNaN(parseInt(subbandRange[0]))) { + return false; + } + if (parseInt(subbandRange[0]) < 0 || parseInt(subbandRange[0]) > 511) { + return false; + } + } + } + } else { + return false + } + } catch(exception) { + return false; + } + return true; + } + + /** + * Convert the string input for subband list to Array + * @param {String} prpOutput + */ + function getSubbandOutput(prpOutput) { + const subbandArray = prpOutput.split(","); + let subbandList = []; + for (const subband of subbandArray ) { + const subbandRange = subband.split('..'); + if (subbandRange.length > 1) { + subbandList = subbandList.concat( _.range(subbandRange[0], (parseInt(subbandRange[1])+1))); + } else { + subbandList = subbandList.concat(parseInt(subbandRange[0])); + } + } + prpOutput = subbandList; + return prpOutput; + } + + return ( + <React.Fragment> + <div id='editor_holder'></div> + {/* <div><input type="button" onClick={setEditorOutput} value="Show Output" /></div> */} + </React.Fragment> + ); +}; + +export default Jeditor; \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/components/JSONEditor/JViewer.js b/SAS/TMSS/frontend/tmss_webapp/src/components/JSONEditor/JViewer.js new file mode 100644 index 0000000000000000000000000000000000000000..220a6cfa73cf42d7dc933eb253cb382ebba4acfb --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/components/JSONEditor/JViewer.js @@ -0,0 +1,31 @@ +/** + * Component to view the JSON data using 'react-json-view' package + */ +import React, {Component} from 'react'; +import ReactJson from 'react-json-view'; + +export default class JViewer extends Component { + constructor(props) { + super(props); + this.state = { + outputJSON: props.outputJSON + } + this.updateOutput = this.updateOutput.bind(this); + } + + /** + * Function to be called by the parent to update the JSON content of the viewer + * @param {JSON} outputJSON + */ + updateOutput(outputJSON) { + this.state.outputJSON = outputJSON; + } + + render() { + return ( + <React.Fragment> + <ReactJson src={this.state.outputJSON} /> + </React.Fragment> + ); + } +} \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/components/JSONEditor/index.js b/SAS/TMSS/frontend/tmss_webapp/src/components/JSONEditor/index.js new file mode 100644 index 0000000000000000000000000000000000000000..60bb36fd1b4c5645a2dead54172db7fc3c9f0e1f --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/components/JSONEditor/index.js @@ -0,0 +1,3 @@ +import { JSONEditor } from './jsonEditor'; + +export default JSONEditor; \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/components/UncontrolledModal/UncontrolledModal.js b/SAS/TMSS/frontend/tmss_webapp/src/components/UncontrolledModal/UncontrolledModal.js new file mode 100644 index 0000000000000000000000000000000000000000..1b812de2765a0338083b3264f17709e21b98eeb3 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/components/UncontrolledModal/UncontrolledModal.js @@ -0,0 +1,63 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import _ from 'lodash'; +import { Modal } from 'reactstrap'; + +import { Provider } from './context'; + +class UncontrolledModal extends React.Component { + static propTypes = { + target: PropTypes.string.isRequired + } + + constructor(props) { + super(props); + + this.state = { + isOpen: false + }; + + this.boundClickEventHandler = this.clickEventHandler.bind(this); + } + + componentDidMount() { + if (typeof document !== 'undefined') { + this.triggerElement = document.querySelector(`#${this.props.target}`); + + if (!this.triggerElement) { + // eslint-disable-next-line no-console + console.error('UncontrolledModal: \'target\' element has not been found in the DOM via querySelector'); + return; + } + + this.triggerElement.addEventListener('click', this.boundClickEventHandler); + } + } + + componentWillUnmount() { + if (this.triggerElement) { + this.triggerElement.removeEventListener('click', this.boundClickEventHandler); + } + } + + clickEventHandler() { + this.setState({ isOpen: true }); + } + + render() { + const modalProps = _.omit(this.props, ['target']); + const toggleModal = () => { this.setState({ isOpen: !this.state.isOpen }) }; + + return ( + <Provider value={{ toggleModal }}> + <Modal + { ...modalProps } + isOpen={ this.state.isOpen } + toggle={ toggleModal } + /> + </Provider> + ); + } +} + +export { UncontrolledModal }; diff --git a/SAS/TMSS/frontend/tmss_webapp/src/components/UncontrolledModal/UncontrolledModalClose.js b/SAS/TMSS/frontend/tmss_webapp/src/components/UncontrolledModal/UncontrolledModalClose.js new file mode 100644 index 0000000000000000000000000000000000000000..5f8ad29fa4ca192549c55a7c0693d10be84e359c --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/components/UncontrolledModal/UncontrolledModalClose.js @@ -0,0 +1,34 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Button } from 'reactstrap'; + +import { Consumer } from './context'; + +const UncontrolledModalClose = (props) => { + const { tag, ...otherProps } = props; + const Tag = tag; + + return ( + <Consumer> + { + (value) => ( + <Tag + { ...otherProps } + onClick={ () => value.toggleModal() } + /> + ) + } + </Consumer> + ) +}; +UncontrolledModalClose.propTypes = { + tag: PropTypes.oneOfType([ + PropTypes.func, + PropTypes.string + ]) +}; +UncontrolledModalClose.defaultProps = { + tag: Button +}; + +export { UncontrolledModalClose }; diff --git a/SAS/TMSS/frontend/tmss_webapp/src/components/UncontrolledModal/context.js b/SAS/TMSS/frontend/tmss_webapp/src/components/UncontrolledModal/context.js new file mode 100644 index 0000000000000000000000000000000000000000..bf325a1a345d72ce78840f32a8ac29301530120b --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/components/UncontrolledModal/context.js @@ -0,0 +1,8 @@ +import React from 'react'; + +const { Provider, Consumer } = React.createContext(); + +export { + Provider, + Consumer +} diff --git a/SAS/TMSS/frontend/tmss_webapp/src/components/UncontrolledModal/index.js b/SAS/TMSS/frontend/tmss_webapp/src/components/UncontrolledModal/index.js new file mode 100644 index 0000000000000000000000000000000000000000..3927d23c324d2b7d6a3538dd9edcfd41aea5f7ec --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/components/UncontrolledModal/index.js @@ -0,0 +1,6 @@ +import { UncontrolledModal } from './UncontrolledModal'; +import { UncontrolledModalClose } from './UncontrolledModalClose'; + +UncontrolledModal.Close = UncontrolledModalClose; + +export default UncontrolledModal; \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/components/ViewTable.js b/SAS/TMSS/frontend/tmss_webapp/src/components/ViewTable.js new file mode 100644 index 0000000000000000000000000000000000000000..0ba1cc0c61175380c24f5c078a564fd718f3fbd8 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/components/ViewTable.js @@ -0,0 +1,332 @@ +import React, {useRef } from "react"; +import { useSortBy, useTable, useFilters, useGlobalFilter, useAsyncDebounce } 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'; + + +let tbldata =[]; +let isunittest = false; +let columnclassname =[]; +// Define a default UI for filtering +function GlobalFilter({ + preGlobalFilteredRows, + globalFilter, + setGlobalFilter, + }) { + + const [value, setValue] = React.useState(globalFilter) + const onChange = useAsyncDebounce(value => {setGlobalFilter(value || undefined)}, 200) + return ( + <span> + <input + value={value || ""} + onChange={e => { + setValue(e.target.value); + onChange(e.target.value); + }} + /> {" "}<i className="fa fa-search"></i> + </span> + ) +} + +// Define a default UI for filtering +function DefaultColumnFilter({ + column: { filterValue, preFilteredRows, setFilter }, +}) { + return ( + <input + value={filterValue || ''} + onChange={e => { + setFilter(e.target.value || undefined) // Set undefined to remove the filter entirely + }} + /> + ) +} + +function fuzzyTextFilterFn(rows, id, filterValue) { + return matchSorter(rows, filterValue, { keys: [row => row.values[id]] }) +} + +// Let the table remove the filter if the string is empty +fuzzyTextFilterFn.autoRemove = val => !val + +const IndeterminateCheckbox = React.forwardRef( + ({ indeterminate, ...rest }, ref) => { + const defaultRef = React.useRef() + const resolvedRef = ref || defaultRef + React.useEffect(() => { + resolvedRef.current.indeterminate = indeterminate + }, [resolvedRef, indeterminate]) + return <input type="checkbox" ref={resolvedRef} {...rest} /> + } +) + +// Our table component +function Table({ columns, data, defaultheader, optionalheader }) { + const filterTypes = React.useMemo( + () => ({ + // Add a new fuzzyTextFilterFn filter type. + fuzzyText: fuzzyTextFilterFn, + // Or, override the default text filter to use + // "startWith" + text: (rows, id, filterValue) => { + return rows.filter(row => { + const rowValue = row.values[id] + return rowValue !== undefined + ? String(rowValue) + .toLowerCase() + .startsWith(String(filterValue).toLowerCase()) + : true + }) + }, + }), + [] + ) + + const defaultColumn = React.useMemo( + () => ({ + // Let's set up our default Filter UI + Filter: DefaultColumnFilter, + }), + [] + ) + + const { + getTableProps, + getTableBodyProps, + headerGroups, + rows, + prepareRow, + allColumns, + getToggleHideAllColumnsProps, + state, + preGlobalFilteredRows, + setGlobalFilter, + setHiddenColumns, + } = useTable( + { + columns, + data, + defaultColumn, + filterTypes, + }, + useFilters, + useGlobalFilter, + useSortBy, + ) + + React.useEffect(() => { + setHiddenColumns( + columns.filter(column => !column.isVisible).map(column => column.accessor) + ); + }, [setHiddenColumns, columns]); + + const firstPageRows = rows.slice(0, 10) + let op = useRef(null); + + return ( + <> + <div id="block_container" style={{ display: 'flex', verticalAlign: 'middle', marginTop:'20px'}}> + <div style={{textAlign:'left', marginRight:'30px'}}> + <i className="fa fa-columns col-filter-btn" label="Toggle Columns" onClick={(e) => op.current.toggle(e)} /> + <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()} /> {(defaultheader[column.id])?defaultheader[column.id]:(optionalheader[column.id]?optionalheader[column.id]:column.id)} + </div> + ))} + <br /> + </div> + </div> + </div> + </div> + </OverlayPanel> + </div> + + <div style={{textAlign:'right'}}> + {tbldata.length>0 && !isunittest && + <GlobalFilter + preGlobalFilteredRows={preGlobalFilteredRows} + globalFilter={state.globalFilter} + setGlobalFilter={setGlobalFilter} + /> + } + </div> +</div> + + <div style={{overflow: 'auto', padding: '0.75em',}}> + + <table {...getTableProps()} style={{width:'100%'}} data-testid="viewtable" className="viewtable" > + <thead> + {headerGroups.map(headerGroup => ( + <tr {...headerGroup.getHeaderGroupProps()}> + {headerGroup.headers.map(column => ( + <th {...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>) : <i className="pi pi-sort" aria-hidden="true"></i> + : "" + } */} + {/* 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()}> + {firstPageRows.map((row, i) => { + + prepareRow(row) + return ( + <tr {...row.getRowProps()}> + {row.cells.map(cell => { + if(cell.column.id !== 'actionpath') + return <td {...cell.getCellProps()} >{cell.render('Cell')}</td> + else + return ""; + })} + </tr> + ) + })} + </tbody> + </table> + </div> + </> + ) +} + + +// Define a custom filter filter function! +function filterGreaterThan(rows, id, filterValue) { + return rows.filter(row => { + const rowValue = row.values[id] + return rowValue >= filterValue + }) +} + +// This is an autoRemove method on the filter function that +// when given the new filter value and returns true, the filter +// will be automatically removed. Normally this is just an undefined +// check, but here, we want to remove the filter if it's not a number +filterGreaterThan.autoRemove = val => typeof val !== 'number' + +function ViewTable(props) { + const history = useHistory(); + // Data to show in table + tbldata = props.data; + isunittest = props.unittest; + columnclassname = props.columnclassname; + + // 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 columns = []; + let defaultdataheader = Object.keys(defaultheader[0]); + let optionaldataheader = Object.keys(optionalheader[0]); + + 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-edit" 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 =>{ + columns.push({ + Header: defaultheader[0][header], + id: defaultheader[0][header], + accessor: header, + filter: 'fuzzyText', + isVisible: true, + Cell: props => <div> {updatedCellvalue(header, props.value)} </div>, + }) + }) + + //Optional Columns + optionaldataheader.forEach(header => { + columns.push({ + Header: optionalheader[0][header], + id: header, + accessor: header, + filter: 'fuzzyText', + isVisible: false, + Cell: props => <div> {updatedCellvalue(header, props.value)} </div>, + }) + }); + + 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 == "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) + } + return value; + } + + + + return ( + <div> + <Table columns={columns} data={tbldata} defaultheader={defaultheader[0]} optionalheader={optionalheader[0]} /> + </div> + ) +} + +export default ViewTable diff --git a/SAS/TMSS/frontend/tmss_webapp/src/index.js b/SAS/TMSS/frontend/tmss_webapp/src/index.js index f5185c1ec7a5dccf30b55a8e3f89afc3eca764a1..f24975207fdc2c2496b95f2652ab096544a01a5e 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/index.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/index.js @@ -1,13 +1,15 @@ +import 'react-app-polyfill/ie11'; import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; import * as serviceWorker from './serviceWorker'; +import 'cleave.js/dist/cleave'; ReactDOM.render( - <React.StrictMode> - <App /> - </React.StrictMode>, + // <React.StrictMode> + <App />, + // </React.StrictMode>, document.getElementById('root') ); diff --git a/SAS/TMSS/frontend/tmss_webapp/src/layout/_overrides.scss b/SAS/TMSS/frontend/tmss_webapp/src/layout/_overrides.scss index 850b92b170e5957de7638ac69233197a155174af..ddf77d3e4b228914fd7fc97e74cb11820d821755 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/layout/_overrides.scss +++ b/SAS/TMSS/frontend/tmss_webapp/src/layout/_overrides.scss @@ -1 +1,25 @@ -//Suggested location to add your overrides so that migration would be easy by just updating the SASS folder in the future \ No newline at end of file +//Suggested location to add your overrides so that migration would be easy by just updating the SASS folder in the future +.layout-wrapper.layout-static.layout-static-sidebar-inactive ul li a span{ + display: none; + height: 0px; +} + +.layout-wrapper.layout-static.layout-static-sidebar-inactive ul li a { + height: auto; +} +.layout-wrapper.layout-static.layout-static-sidebar-inactive { + .layout-profile img{ + width: 30px; + margin: 0px; + } + .layout-profile p{ + display: none; + } + .layout-logo{ + display: none; + } + + .layout-sidebar-dark .layout-menu li a { + border-top: none; + } +} \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/layout/components/AppBreadcrumb.js b/SAS/TMSS/frontend/tmss_webapp/src/layout/components/AppBreadcrumb.js new file mode 100644 index 0000000000000000000000000000000000000000..0abba394b1c1190aa1c84bf7ec20af19f97c720c --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/layout/components/AppBreadcrumb.js @@ -0,0 +1,70 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import {Link, matchPath} from 'react-router-dom'; +import { routes } from '../../routes'; +export class AppBreadcrumb extends Component { + + static propTypes = { + match: PropTypes.object, + } + + constructor(props) { + super(props); + this.state = { + breadcrumbs: [] + } + } + + componentDidUpdate(prev) { + if (prev.location.pathname !== this.props.location.pathname) { + this.onRoute(); + } + } + + componentDidMount() { + this.onRoute(); + } + + onRoute() { + const { breadcrumbs } = this.state; + const currentRoute = routes.find(route => matchPath(this.props.location.pathname, {path: route.path, exact: true, strict: true})); + //for intial route ,there wont be any route object so it failed + if(!currentRoute){ + return; + } + if (!breadcrumbs.length) { + this.setState({ breadcrumbs: [{...this.props.location, name: currentRoute.name}] }); + return; + } + const index = breadcrumbs.map(i => i.name).indexOf(currentRoute.name); + if (index === -1) { + this.setState({ breadcrumbs: [...breadcrumbs, {...this.props.location, name: currentRoute.name}] }); + return; + } + this.setState({ breadcrumbs: breadcrumbs.slice(0, index+1) }); + } + + onNavigate(item) { + this.props.history.push({ + pathname: item.pathname, + state: item.state + }); + } + + render() { + const { breadcrumbs } = this.state; + return ( + <div className="p-breadcrumb" > + <span className="pi b-home"><Link className="b-link pi pi-home" to="/"/></span> + {breadcrumbs.map((item, index) => ( + <span key={"bc_" + index} > + <li className="pi pi-chevron-right b-separator"></li> + {index !== breadcrumbs.length - 1 ? + <span className="b-link" onClick={() => this.onNavigate(item)}>{item.name}</span> + : <span className="b-text">{item.name}</span>} + </span> + ))} + </div> + ); + } +} \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/layout/components/AppFooter.js b/SAS/TMSS/frontend/tmss_webapp/src/layout/components/AppFooter.js index 27edea6d5758942eaf4582237ed240d18b554082..6453a2d30d49d4d43ea767294d466d195a1eaa37 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/layout/components/AppFooter.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/layout/components/AppFooter.js @@ -5,7 +5,7 @@ export class AppFooter extends Component { render() { return ( <div className="layout-footer"> - <span className="footer-text" style={{'marginRight': '5px'}}><strong>TMSS</strong> by <strong>ASTRON</strong></span> + <span className="footer-text" style={{'marginRight': '5px', color:'#0066CC'}}><strong>TMSS</strong> by <strong>ASTRON</strong></span> </div> ); } diff --git a/SAS/TMSS/frontend/tmss_webapp/src/layout/components/AppLoader.js b/SAS/TMSS/frontend/tmss_webapp/src/layout/components/AppLoader.js new file mode 100644 index 0000000000000000000000000000000000000000..04ed40995e0b586879ec6cd0f37582bee761371a --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/layout/components/AppLoader.js @@ -0,0 +1,21 @@ +import Loader from 'react-loader-spinner'; +import React, { Component } from 'react' + +export class AppLoader extends Component{ + + render() { + const load = { + width: "100%", + height: "100", + display: "flex", + justifyContent: "center" + } + + return ( + <div style={load}> + <Loader type="ThreeDots" color="#004B93" height={80} width={80} /> + </div> + ); + } +} +export default AppLoader \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/layout/components/AppMenu.js b/SAS/TMSS/frontend/tmss_webapp/src/layout/components/AppMenu.js index 7f0e4e18c1ee09448b7ed7f2330702f7b3d875dc..c46698462cfea5aff95a48cbf167c56a0dfd736e 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/layout/components/AppMenu.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/layout/components/AppMenu.js @@ -1,4 +1,4 @@ -import React, { Component } from 'react'; + import React, { Component } from 'react'; import {NavLink} from 'react-router-dom' import PropTypes from 'prop-types'; import classNames from 'classnames'; @@ -52,6 +52,7 @@ class AppSubmenu extends Component { } } + renderLinkContent(item) { let submenuIcon = item.items && <i className="pi pi-fw pi-angle-down menuitem-toggle-icon"></i>; let badge = item.badge && <span className="menuitem-badge">{item.badge}</span>; @@ -92,11 +93,6 @@ class AppSubmenu extends Component { let active = this.state.activeIndex === i; // let styleClass = classNames(item.badgeStyleClass, {'active-menuitem': active && !item.to}); let styleClass = classNames(item.badgeStyleClass, {'active-menuitem': active && item.to}); - console.log(item.badgeStyleClass); - console.log(i); - console.log(this.state.activeIndex); - console.log(active); - console.log(styleClass); return ( <li className={styleClass} key={i}> {item.items && this.props.root===true && <div className='arrow'></div>} @@ -124,7 +120,6 @@ export class AppMenu extends Component { } render() { - // console.log(authenticationService.currentUserValue); return ( <div className={'layout-sidebar layout-sidebar-light'} > <div className="layout-menu-container"> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/layout/components/AppTopbar.js b/SAS/TMSS/frontend/tmss_webapp/src/layout/components/AppTopbar.js index ccd16881c0a2f108516bd1fc3a40e723cb6da71f..e45be034ce26e4db68ac6446a00f3517bc5f9184 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/layout/components/AppTopbar.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/layout/components/AppTopbar.js @@ -4,8 +4,7 @@ import 'primeicons/primeicons.css'; import 'primereact/resources/themes/nova-light/theme.css'; import 'primereact/resources/primereact.css'; import 'primeflex/primeflex.css'; - -// import { PropTypes } from 'prop-types'; + import { PropTypes } from 'prop-types'; export class AppTopbar extends Component { @@ -15,21 +14,22 @@ import 'primeflex/primeflex.css'; // } - // static defaultProps = { - // onToggleMenu: null - // } + static defaultProps = { + onToggleMenu: null + } - // static propTypes = { - // onToggleMenu: PropTypes.func - // } + static propTypes = { + onToggleMenu: PropTypes.func.isRequired + } render() { return ( <React.Fragment> <div className="layout-wrapper layout-static layout-static-sidebar-inactive"> <div className="layout-topbar clearfix"> - <button className="p-link layout-menu-button"><i className="pi pi-bars"></i></button> + <button className="p-link layout-menu-button" onClick={this.props.onToggleMenu}> + <i className="pi pi-bars"></i></button> <span className="header-title">TMSS</span> </div> </div> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/layout/components/NotFound.js b/SAS/TMSS/frontend/tmss_webapp/src/layout/components/NotFound.js new file mode 100644 index 0000000000000000000000000000000000000000..2d8784ebd74e08ef716c94b6763f6dcecb28c651 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/layout/components/NotFound.js @@ -0,0 +1,12 @@ +import React, { Component } from 'react'; + +export class NotFound extends Component { + + render() { + return ( + <h1>Not Found</h1> + ); + } +} + +export default NotFound; \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_breadcrumb.scss b/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_breadcrumb.scss new file mode 100644 index 0000000000000000000000000000000000000000..8c27dd9432d5d7d0bb79b84ec2e6694b0aa9d95e --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_breadcrumb.scss @@ -0,0 +1,37 @@ + +body .p-breadcrumb{ + background-color: #F1F1F1; + padding: 0em; + padding-left: 5px; + padding-top: 5px; + margin-bottom: 5px; + box-shadow: 2px 2px #d6d9d9; + border: none; +} +.p-breadcrumb .pi{ + color: #535252; + font-size : large; +} + +.b-link { + font-size: 14px; + color:#007bff; + cursor: pointer; + &:hover { + text-decoration: underline; + } +} + +.b-text { + font-size: 14px; + columns: #464748; +} + +.b-separator { + font-size: 0.9rem !important; + margin: 0 5px; +} +.b-home { + position: relative; + top: 2px; +} diff --git a/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_content.scss b/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_content.scss index 693a30ddbfb3214511538caff487fcd8bb9c1a8f..5c49ad86c0d840f2b6876fd5662d5ca981e34331 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_content.scss +++ b/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_content.scss @@ -1,6 +1,6 @@ .layout-main { @include transition(margin-left $transitionDuration); - padding: 60px 16px 16px 16px; + padding: 60px 16px 16px 25px; min-height: 95vh; background-color: white; } \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_layout.scss b/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_layout.scss index b5182b1c58420ad500fc0cb48e1b85d4ef2ae393..79f22ea107106c4e26e10b2cc375414ea77b293d 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_layout.scss +++ b/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_layout.scss @@ -9,4 +9,6 @@ @import "./_footer"; @import "./_responsive"; @import "./_utils"; -@import "./_dashboard"; \ No newline at end of file +@import "./_dashboard"; +@import "./_breadcrumb"; +@import "./_viewtable"; diff --git a/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_responsive.scss b/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_responsive.scss index 97ab84ce038e66fe3bfdf4a5a2de512d491709ec..9d189af7911bfad260e5819ed197ae303ff55569 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_responsive.scss +++ b/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_responsive.scss @@ -38,18 +38,22 @@ } &.layout-static-sidebar-inactive { + .layout-sidebar { - left: -250px; + left: 0; + width: 50px; + } .layout-topbar { - left: 0; + left: -10px; } .layout-main, .layout-footer { - margin-left: 0; + margin-left: 50px; } } + } } } diff --git a/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_sidebar.scss b/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_sidebar.scss index cca2a830c7f57a18ff9687d8d98de016709c7aa5..13e9d4bbdb3517142b813f570ca387f59a8b1aeb 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_sidebar.scss +++ b/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_sidebar.scss @@ -3,7 +3,6 @@ position: fixed; width: 250px; height: 100%; - // z-index: 999; overflow-y: auto; user-select: none; -moz-user-select: none; @@ -11,11 +10,6 @@ @include transition(left $transitionDuration); @include shadow(0 0 6px 0 rgba(0, 0, 0, 0.16)); - .layout-logo { - text-align: center; - margin-top: 24px; - } - .menuitem-badge { display: inline-block; margin-left: 4px; @@ -28,4 +22,5 @@ background-color: $menuitemBadgeBgColor; @include border-radius(50%); } + } \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_viewtable.scss b/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_viewtable.scss new file mode 100644 index 0000000000000000000000000000000000000000..9a8abc949f663e016266980de3c8df80863e0af2 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_viewtable.scss @@ -0,0 +1,45 @@ + .viewtable{ + overflow: auto !important; + padding: 0.75em; + } + +.viewtable th { + color: #7e8286; + font-size: 14px; + border-bottom: 1px solid lightgray; + border-top: 1px solid lightgray; + padding: .65rem; + vertical-align:bottom; + } + +.viewtable th>div { + vertical-align: text-bottom; +} + +.viewtable td { + font-size: 14px; + padding: .65rem; + border-bottom: 1px solid lightgray; + overflow-wrap: anywhere; +} + +.filter-input input{ + max-width: 150px; +} + +.filter-input-50 input{ + width: 50px; +} + +.filter-input-70 input{ + width: 70px; +} + +.filter-input-100 input{ + width: 100px; +} + +.filter-input-150 input{ +width: 150px; +} + \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Dashboard/index.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Dashboard/index.js index dd52fdf11774d21f1e002716cf9804f1ac581d8d..a0a798161d65edf83901902a88e3957569741bd8 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Dashboard/index.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Dashboard/index.js @@ -1,7 +1,12 @@ import React, {Component} from 'react'; + export class Dashboard extends Component { + constructor(props){ + super(props) + console.log(this.props) + } render() { return ( <h1>Dashboard</h1> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/ResourceDisplayList.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/ResourceDisplayList.js new file mode 100644 index 0000000000000000000000000000000000000000..f7f959b7feb1a125588d07ebcb3d586c1ceca889 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/ResourceDisplayList.js @@ -0,0 +1,31 @@ +import React, {Component} from 'react'; + +/** + * Component to get input for Resource allocation while creating and editing Project + */ +class ResourceDisplayList extends Component { + constructor(props) { + super(props); + this.state = { + projectQuota: props.projectQuota + } + } + + render(){ + return ( + <> + {this.props.projectQuota.length>0 && this.props.projectQuota.map((item, index) => ( + <React.Fragment key={index+10}> + <label key={'label1-'+ index} className="col-lg-3 col-md-3 col-sm-12">{item.resource.name}</label> + <span key={'div1-'+ index} className="col-lg-3 col-md-3 col-sm-12"> + {item.value/(this.props.unitMap[item.resource.quantity_value]?this.props.unitMap[item.resource.quantity_value].conversionFactor:1)} + {` ${this.props.unitMap[item.resource.quantity_value]?this.props.unitMap[item.resource.quantity_value].display:''}`} + </span> + </React.Fragment> + ))} + </> + ); + } +} + +export default ResourceDisplayList; \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/ResourceInputList.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/ResourceInputList.js new file mode 100644 index 0000000000000000000000000000000000000000..c1d9019421ff16c5570e3371e8ff338342b404f7 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/ResourceInputList.js @@ -0,0 +1,58 @@ +import React, {Component} from 'react'; +import {InputNumber} from 'primereact/inputnumber'; + +/** + * Component to get input for Resource allocation while creating and editing Project + */ +export class ResourceInputList extends Component { + constructor(props) { + super(props); + this.state = { + list: props.list, + projectQuota: props.projectQuota + } + this.updateEnabled = this.props.list.length===0?true:false; + this.onInputChange = this.onInputChange.bind(this); + } + + shouldComponentUpdate() { + return true; + } + + onInputChange(field, event) { + if (this.props.callback) { + this.props.callback(field, event); + } + } + + removeInput(field) { + if (this.props.removeInputCallback) { + this.props.removeInputCallback(field); + } + } + + render(){ + return ( + <> + {this.props.list.length>0 && this.props.list.map((item, index) => ( + <React.Fragment key={index+10}> + <label key={'label1-'+ index} className="col-lg-2 col-md-2 col-sm-12">{item.name}</label> + <div key={'div1-'+ index} className="col-lg-3 col-md-3 col-sm-12"> + <InputNumber key={'item1-'+ index} id={'item1-'+ index} name={'item1-'+ index} + suffix={` ${this.props.unitMap[item.quantity_value]?this.props.unitMap[item.quantity_value].display:''}`} + placeholder={` ${this.props.unitMap[item.quantity_value]?this.props.unitMap[item.quantity_value].display:item.name}`} min={0} useGrouping={false} + value={this.state.projectQuota[item.name]} + onChange={(e) => this.onInputChange(item.name, e)} + onBlur={(e) => this.onInputChange(item.name, e)} + style={{width:"90%", marginRight: "5px"}} + /> + <button className="p-link" data-testid={`${item.name}-btn`} onClick={(e) => this.removeInput(item.name)}> + <i className="fa fa-trash pi-error"></i></button> + </div> + <div className="col-lg-1 col-md-1 col-sm-12"></div> + </React.Fragment> + ))} + </> + ); + } +} \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/create.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/create.js new file mode 100644 index 0000000000000000000000000000000000000000..e8df560a75cc56560d2eb39adc94a404697e259d --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/create.js @@ -0,0 +1,513 @@ +import React, {Component} from 'react'; +import { Link, 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 { Button } from 'primereact/button'; +import {Dialog} from 'primereact/components/dialog/Dialog'; +import {Growl} from 'primereact/components/growl/Growl'; + +import {ResourceInputList} from './ResourceInputList'; + +import AppLoader from '../../layout/components/AppLoader'; +import CycleService from '../../services/cycle.service'; +import ProjectService from '../../services/project.service'; +import UnitConverter from '../../utils/unit.converter'; +import UIConstants from '../../utils/ui.constants'; + +/** + * Component to create a new Project + */ +export class ProjectCreate extends Component { + constructor(props) { + super(props); + this.state = { + isLoading: true, + dialog: { header: '', detail: ''}, + project: { + trigger_priority: 1000, + priority_rank: null, + quota: [], // Mandatory Field in the back end, so an empty array is passed + can_trigger: false + }, + projectQuota: {}, // Resource Allocations + validFields: {}, // For Validation + validForm: false, // To enable Save Button + errors: {}, // Validation Errors + periodCategories: [], + projectCategories: [], + resources: [], // Selected Resources for Allocation + resourceList: [], // Available Resources for Allocation + cycles: [] + } + // Validateion Rules + this.formRules = { + name: {required: true, message: "Name can not be empty"}, + description: {required: true, message: "Description can not be empty"}, + priority_rank: {required: true, message: "Enter Project Rank"} + }; + this.defaultResourcesEnabled = true; // This property and functionality to be concluded based on PO input + this.defaultResources = [{name:'LOFAR Observing Time'}, + {name:'LOFAR Observing Time prio A'}, + {name:'LOFAR Observing Time prio B'}, + {name:'CEP Processing Time'}, + {name:'LTA Storage'}, + {name:'Number of triggers'}, + {name:'LOFAR Support Time'} ]; + this.projectResourceDefaults = {}; // Default values for default resources + this.resourceUnitMap = UnitConverter.resourceUnitMap; // Resource unit conversion factor and constraints + this.cycleOptionTemplate = this.cycleOptionTemplate.bind(this); // Template for cycle multiselect + this.tooltipOptions = UIConstants.tooltipOptions; + + this.setProjectQuotaDefaults = this.setProjectQuotaDefaults.bind(this); + this.setProjectParams = this.setProjectParams.bind(this); + this.addNewResource = this.addNewResource.bind(this); + this.removeResource = this.removeResource.bind(this); + this.setProjectQuotaParams = this.setProjectQuotaParams.bind(this); + this.saveProject = this.saveProject.bind(this); + this.cancelCreate = this.cancelCreate.bind(this); + this.reset = this.reset.bind(this); + } + + componentDidMount() { + ProjectService.getDefaultProjectResources() + .then(defaults => { + this.projectResourceDefaults = defaults; + }); + CycleService.getAllCycles() + .then(cycles => { + this.setState({cycles: cycles}); + }); + ProjectService.getProjectCategories() + .then(categories => { + this.setState({projectCategories: categories}); + }); + ProjectService.getPeriodCategories() + .then(categories => { + this.setState({periodCategories: categories}); + }); + ProjectService.getResources() + .then(resourceList => { + const defaultResources = this.defaultResources; + resourceList = _.sortBy(resourceList, "name"); + const resources = _.remove(resourceList, function(resource) { return _.find(defaultResources, {'name': resource.name})!=null }); + const projectQuota = this.setProjectQuotaDefaults(resources); + this.setState({resourceList: resourceList, resources: resources, projectQuota: projectQuota, isLoading: false}); + }); + // ProjectService.getProjects().then(projects => { + // console.log(projects); + // }); + } + + /** + * Cycle option sub-component with cycle object + */ + cycleOptionTemplate(option) { + return ( + <div className="p-clearfix"> + <span style={{fontSize:'1em',float:'right',margin:'1em .5em 0 0'}}>{option.name}</span> + </div> + ); + } + + /** + * Function to set project resource allocation + * @param {Array} resources + */ + setProjectQuotaDefaults(resources) { + let projectQuota = this.state.projectQuota; + for (const resource of resources) { + const conversionFactor = this.resourceUnitMap[resource.quantity_value]?this.resourceUnitMap[resource.quantity_value].conversionFactor:1; + projectQuota[resource['name']] = this.projectResourceDefaults[resource.name]/conversionFactor; + } + return projectQuota; + } + + /** + * Function to add new resource to project + */ + addNewResource(){ + if (this.state.newResource) { + let resourceList = 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}); + } + } + + /** + * Callback function to be called from ResourceInpulList when a resource is removed from it + * @param {string} name - resource_type_id + */ + removeResource(name) { + let resources = this.state.resources; + let resourceList = this.state.resourceList; + let projectQuota = 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}); + } + + /** + * Function to call on change and blur events from input components + * @param {string} key + * @param {any} value + */ + setProjectParams(key, value, type) { + let project = this.state.project; + switch(type) { + case 'NUMBER': { + console.log("Parsing Number"); + project[key] = value?parseInt(value):0; + break; + } + default: { + project[key] = value; + break; + } + } + this.setState({project: project, validForm: this.validateForm(key)}); + } + + /** + * Callback Function to call from ResourceInputList on change and blur events + * @param {string} key + * @param {InputEvent} event + */ + setProjectQuotaParams(key, event) { + let projectQuota = this.state.projectQuota; + 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,''); + } else { + newValue = event.target.value; + } + projectQuota[key] = (newValue==="NaN" || isNaN(newValue))?0:newValue; + } else { + let projectQuota = this.state.projectQuota; + projectQuota[key] = 0; + } + this.setState({projectQuota: projectQuota}); + } + + /** + * 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.project[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.project[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 call when 'Save' button is clicked to save the project. + */ + saveProject() { + if (this.validateForm) { + let projectQuota = []; + for (const resource in this.state.projectQuota) { + let resourceType = _.find(this.state.resources, {'name': resource}); + let quota = { project: this.state.project.name, + resource_type: resourceType['url'], + value: this.state.projectQuota[resource] * (this.resourceUnitMap[resourceType.quantity_value]?this.resourceUnitMap[resourceType.quantity_value].conversionFactor:1)}; + projectQuota.push(quota); + } + ProjectService.saveProject(this.state.project, this.defaultResourcesEnabled?projectQuota:[]) + .then(project => { + if (project.url) { + let dialog = {}; + if (this.defaultResourcesEnabled) { + dialog = {header: 'Success', detail: 'Project saved successfully. Do you want to create another project?'}; + } 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}) + } else { + this.growl.show({severity: 'error', summary: 'Error Occured', detail: 'Unable to save Project'}); + this.setState({errors: project}); + } + }); + } + } + + /** + * Function to cancel form creation and navigate to other page/component + */ + cancelCreate() { + this.setState({redirect: '/project'}); + } + + /** + * Reset function to be called to reset the form fields + */ + reset() { + if (this.defaultResourcesEnabled) { + let prevResources = this.state.resources; + let resourceList = []; + let resources = []; + if (resources) { + // const nonDefaultResources = _.remove(resources, function(resource) { return _.find(defaultResources, {'name': resource.name})==null }); + // resourceList = nonDefaultResources.concat(this.state.resourceList); + const defaultResources = this.defaultResources; + resourceList = _.sortBy(prevResources.concat(this.state.resourceList), "name"); + resources = _.remove(resourceList, function(resource) { return _.find(defaultResources, {'name': resource.name})!=null }); + // const projectQuota = this.setProjectQuotaDefaults(resources); + // this.setState({resourceList: resourceList, resources: resources, projectQuota: projectQuota}); + } + const projectQuota = this.setProjectQuotaDefaults(resources); + this.setState({ + dialog: { header: '', detail: ''}, + project: { + name: '', + description: '', + trigger_priority: 1000, + priority_rank: null, + project_quota: [] + }, + projectQuota: projectQuota, + validFields: {}, + validForm: false, + errors: {}, + dialogVisible: false, + resources: resources, + resourceList: resourceList + }); + } else { + this.setState({redirect: `/project/edit/${this.state.project.name}`}) + } + } + + render() { + if (this.state.redirect) { + return <Redirect to={ {pathname: this.state.redirect} }></Redirect> + } + + return ( + <React.Fragment> + <div className="p-grid"> + <Growl ref={(el) => this.growl = el} /> + + <div className="p-col-10 p-lg-10 p-md-10"> + <h2>Project - Add</h2> + </div> + <div className="p-col-2 p-lg-2 p-md-2"> + <Link to={{ pathname: '/project'}} tite="Close Edit" style={{float: "right"}}> + <i className="fa fa-window-close" style={{marginTop: "10px"}}></i> + </Link> + </div> + </div> + { this.state.isLoading ? <AppLoader /> : + <> + <div> + <div className="p-fluid"> + <div className="p-field p-grid" style={{display: 'none'}}> + <label htmlFor="projectId" className="col-lg-2 col-md-2 col-sm-12">URL </label> + <div className="col-lg-3 col-md-3 col-sm-12"> + <input id="projectId" data-testid="projectId" value={this.state.project.url} /> + </div> + </div> + <div className="p-field p-grid"> + <label htmlFor="projectName" className="col-lg-2 col-md-2 col-sm-12">Name <span style={{color:'red'}}>*</span></label> + <div className="col-lg-3 col-md-3 col-sm-12"> + <InputText className={this.state.errors.name ?'input-error':''} id="projectName" data-testid="name" + tooltip="Enter name of the project" tooltipOptions={this.tooltipOptions} maxLength="128" + value={this.state.project.name} + onChange={(e) => this.setProjectParams('name', e.target.value)} + onBlur={(e) => this.setProjectParams('name', e.target.value)}/> + <label className={this.state.errors.name?"error":"info"}> + {this.state.errors.name ? this.state.errors.name : "Max 128 characters"} + </label> + </div> + <div className="col-lg-1 col-md-1 col-sm-12"></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-3 col-md-3 col-sm-12"> + <InputTextarea className={this.state.errors.description ?'input-error':''} rows={3} cols={30} + tooltip="Short description of the project" tooltipOptions={this.tooltipOptions} maxLength="128" + data-testid="description" value={this.state.project.description} + onChange={(e) => this.setProjectParams('description', e.target.value)} + onBlur={(e) => this.setProjectParams('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 className="p-field p-grid"> + <label htmlFor="triggerPriority" className="col-lg-2 col-md-2 col-sm-12">Trigger Priority </label> + <div className="col-lg-3 col-md-3 col-sm-12" data-testid="trig_prio"> + <InputNumber inputId="trig_prio" name="trig_prio" value={this.state.project.trigger_priority} + tooltip="Priority of this project with respect to triggers" tooltipOptions={this.tooltipOptions} + mode="decimal" showButtons min={0} max={1001} step={10} useGrouping={false} + onChange={(e) => this.setProjectParams('trigger_priority', e.value)} + onBlur={(e) => this.setProjectParams('trigger_priority', e.target.value, 'NUMBER')} /> + + <label className="error"> + {this.state.errors.trigger_priority ? this.state.errors.trigger_priority : ""} + </label> + </div> + <div className="col-lg-1 col-md-1 col-sm-12"></div> + <label htmlFor="trigger" className="col-lg-2 col-md-2 col-sm-12">Allows Trigger Submission</label> + <div className="col-lg-3 col-md-3 col-sm-12" data-testid="trigger"> + <Checkbox inputId="trigger" role="trigger" + tooltip="Is this project allowed to supply observation requests on the fly, possibly interrupting currently running observations (responsive telescope)?" + tooltipOptions={this.tooltipOptions} + checked={this.state.project.can_trigger} onChange={e => this.setProjectParams('can_trigger', e.target.checked)}></Checkbox> + </div> + </div> + <div className="p-field p-grid"> + <label htmlFor="projCat" className="col-lg-2 col-md-2 col-sm-12">Project Category </label> + <div className="col-lg-3 col-md-3 col-sm-12" data-testid="projCat" > + <Dropdown inputId="projCat" optionLabel="value" optionValue="url" + tooltip="Project Category" tooltipOptions={this.tooltipOptions} + value={this.state.project.project_category} + options={this.state.projectCategories} + onChange={(e) => {this.setProjectParams('project_category', e.value)}} + placeholder="Select Project Category" /> + </div> + <div className="col-lg-1 col-md-1 col-sm-12"></div> + <label htmlFor="periodCategory" className="col-lg-2 col-md-2 col-sm-12">Period Category</label> + <div className="col-lg-3 col-md-3 col-sm-12"> + <Dropdown data-testid="period-cat" id="period-cat" optionLabel="value" optionValue="url" + tooltip="Period Category" tooltipOptions={this.tooltipOptions} + value={this.state.project.period_category} + options={this.state.periodCategories} + onChange={(e) => {this.setProjectParams('period_category',e.value)}} + placeholder="Select Period Category" /> + </div> + </div> + <div className="p-field p-grid"> + <label htmlFor="triggerPriority" className="col-lg-2 col-md-2 col-sm-12">Cycle(s)</label> + <div className="col-lg-3 col-md-3 col-sm-12"> + <MultiSelect data-testid="cycle" id="cycle" optionLabel="name" optionValue="url" filter={true} + tooltip="Cycle(s) to which this project belongs" tooltipOptions={this.tooltipOptions} + value={this.state.project.cycles} + options={this.state.cycles} + onChange={(e) => {this.setProjectParams('cycles',e.value)}} + + /> + </div> + <div className="col-lg-1 col-md-1 col-sm-12"></div> + <label htmlFor="projRank" className="col-lg-2 col-md-2 col-sm-12">Project Rank <span style={{color:'red'}}>*</span></label> + <div className="col-lg-3 col-md-3 col-sm-12" data-testid="proj-rank" > + <InputNumber inputId="proj-rank" name="rank" data-testid="rank" value={this.state.project.priority_rank} + tooltip="Priority of this project with respect to other projects. Projects can interrupt observations of lower-priority projects." + tooltipOptions={this.tooltipOptions} + mode="decimal" showButtons min={0} max={100} + onChange={(e) => this.setProjectParams('priority_rank', e.value)} + onBlur={(e) => this.setProjectParams('priority_rank', e.target.value, 'NUMBER')} /> + <label className="error"> + {this.state.errors.priority_rank ? this.state.errors.priority_rank : ""} + </label> + </div> + </div> + + {this.defaultResourcesEnabled && this.state.resourceList && + <div className="p-fluid"> + <div className="p-field p-grid"> + <div className="col-lg-2 col-md-2 col-sm-112"> + <h5 data-testid="resource_alloc">Resource Allocations</h5> + </div> + <div className="col-lg-3 col-md-3 col-sm-10"> + <Dropdown optionLabel="name" optionValue="name" + tooltip="Resources to be allotted for the project" + tooltipOptions={this.tooltipOptions} + value={this.state.newResource} + options={this.state.resourceList} + onChange={(e) => {this.setState({'newResource': e.value})}} + placeholder="Add Resources" /> + </div> + <div className="col-lg-2 col-md-2 col-sm-2"> + <Button label="" className="p-button-primary" icon="pi pi-plus" onClick={this.addNewResource} disabled={!this.state.newResource} data-testid="add_res_btn" /> + </div> + </div> + <div className="p-field p-grid resource-input-grid"> + <ResourceInputList list={this.state.resources} unitMap={this.resourceUnitMap} + projectQuota={this.state.projectQuota} callback={this.setProjectQuotaParams} + removeInputCallback={this.removeResource} /> + </div> + </div> + } + </div> + </div> + <div className="p-grid p-justify-start act-btn-grp"> + <div className="col-lg-1 col-md-2 col-sm-6"> + <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} /> + </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.setState({dialogVisible: false}); this.cancelCreate();}} label="No" /> + <Button key="submit" type="primary" onClick={this.reset} label="Yes" /> + </div> + } > + <div className="p-grid"> + <div className="col-lg-2 col-md-2 col-sm-2"> + <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> + + </React.Fragment> + ); + } +} \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/create.test.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/create.test.js new file mode 100644 index 0000000000000000000000000000000000000000..84c2b338449d107059e43fa4396dead622a042fe --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/create.test.js @@ -0,0 +1,380 @@ +import React from 'react'; +import { BrowserRouter as Router } from 'react-router-dom'; +import { act } from "react-dom/test-utils"; +import { render, cleanup, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; + +import {ProjectCreate} from './create'; +import ProjectService from '../../services/project.service'; +import CycleService from '../../services/cycle.service'; + +import ProjectServiceMock from '../../__mocks__/project.service.data'; + +let projectCategoriesSpy, allCycleSpy, periodCategoriesSpy, saveProjectSpy, resourcesSpy, projectResourceDefaultsSpy; + +beforeEach(() => { + setMockSpy(); +}); + +afterEach(() => { + // cleanup on exiting + clearMockSpy(); + cleanup(); +}); + +/** + * To set mock spy for Services that have API calls to the back end to fetch data + */ +const setMockSpy = (() => { + projectCategoriesSpy = jest.spyOn(ProjectService, 'getProjectCategories'); + projectCategoriesSpy.mockImplementation(() => { return Promise.resolve(ProjectServiceMock.project_categories)}); + periodCategoriesSpy = jest.spyOn(ProjectService, 'getPeriodCategories'); + periodCategoriesSpy.mockImplementation(() => { return Promise.resolve(ProjectServiceMock.period_categories)}); + allCycleSpy = jest.spyOn(CycleService, 'getAllCycles'); + allCycleSpy.mockImplementation(() => { + return Promise.resolve([{url: "http://localhost:3000/api/cycle/Cycle-0", name: 'Cycle-0'}, + {url: "http://localhost:3000/api/cycle/Cycle-1", name: 'Cycle-1'}]); + }); + resourcesSpy = jest.spyOn(ProjectService, 'getResources'); + resourcesSpy.mockImplementation(() => { + return Promise.resolve(ProjectServiceMock.resources); + }); + projectResourceDefaultsSpy = jest.spyOn(ProjectService, 'getDefaultProjectResources'); + projectResourceDefaultsSpy.mockImplementation(() => { + return Promise.resolve(ProjectServiceMock.projectResourceDefaults); + }); + + saveProjectSpy = jest.spyOn(ProjectService, 'saveProject'); + saveProjectSpy.mockImplementation((project, projectQuota) => { + project.url = `http://localhost:3000/api/project/${project.name}`; + return Promise.resolve(project) + }); +}); + +const clearMockSpy = (() => { + projectCategoriesSpy.mockRestore(); + periodCategoriesSpy.mockRestore(); + allCycleSpy.mockRestore(); + saveProjectSpy.mockRestore(); +}); + +it("renders without crashing with all back-end data loaded", async () => { + console.log("renders without crashing with all back-end data loaded ------------------------"); + + let content; + await act(async () => { + content = render(<Router><ProjectCreate /></Router>); + }); + + expect(content.queryByText('Project - Add')).not.toBe(null); // Page loaded successfully + expect(projectCategoriesSpy).toHaveBeenCalled(); // Mock Spy called successfully + expect(content.queryByText('Regular')).toBeInTheDocument(); // Project Category Dropdown loaded successfully + expect(content.queryByText('Single Cycle')).toBeInTheDocument(); // Period Category Dropdown loaded successfully + expect(content.queryByText('Cycle-0')).toBeInTheDocument(); // Cycle multi-select loaded successfully + expect(content.queryAllByText('Add Resources').length).toBe(2); // Resource Dropdown loaded successfully + expect(content.queryByText('Support hours')).toBeInTheDocument(); // Resources other than Default Resources listed in dropdown + expect(content.queryByPlaceholderText('Support Hours')).toBe(null); // No resources other than Default Resources listed to get input + expect(content.queryByPlaceholderText('LOFAR Observing Time').value).toBe('1 Hours'); // Default Resource Listed with default value +}); + +it("Save button disabled initially when no data entered", async () => { + console.log("Save button disabled initially when no data entered -----------------------"); + let content; + await act(async () => { + content = render(<Router><ProjectCreate /></Router>); + }); + expect(content.queryByTestId('save-btn')).toHaveAttribute("disabled"); +}); + +it("Save button enabled when mandatory data entered", async () => { + console.log("Save button enabled when mandatory data entered -----------------------"); + let content; + await act(async () => { + content = render(<Router><ProjectCreate /></Router>); + }); + const nameInput = content.queryByTestId('name'); + const descInput = content.queryByTestId('description'); + const spinButtons = content.queryAllByRole("spinbutton"); + const rankInput = spinButtons.filter(function(element) { return element.id==="proj-rank"})[0]; + + // Set values for all mandatory input and test if save button is enabled + fireEvent.change(nameInput, { target: { value: 'OSR' } }); + expect(nameInput.value).toBe("OSR"); + fireEvent.change(descInput, { target: { value: 'OSR' } }); + expect(descInput.value).toBe("OSR"); + fireEvent.blur(rankInput, { target: { value: 1 } }); + expect(rankInput.value).toBe("1"); + expect(content.queryByTestId('save-btn').hasAttribute("disabled")).toBeFalsy(); +}); + +it("renders Save button enabled when all data entered", async () => { + console.log("renders Save button enabled when all data entered -----------------------"); + let content; + await act(async () => { + content = render(<Router><ProjectCreate /></Router>); + }); + + const nameInput = content.queryByTestId('name'); + const descInput = content.queryByTestId('description'); + const spinButtons = content.queryAllByRole("spinbutton"); + const rankInput = spinButtons.filter(function(element) { return element.id==="proj-rank"})[0]; + const trigPrioInput = spinButtons.filter(function(element) { return element.id==="trig_prio"})[0]; + const trigger = content.getByLabelText(/trigger/i); + const projCatInput = content.getAllByRole("listbox")[0].children[0] ; + const projPeriodInput = content.getAllByRole("listbox")[1].children[0] ; + const cycleInput = content.getAllByRole("listbox")[2].children[0] ; + + fireEvent.change(nameInput, { target: { value: 'OSR' } }); + expect(nameInput.value).toBe("OSR"); + + fireEvent.change(descInput, { target: { value: 'OSR' } }); + expect(descInput.value).toBe("OSR"); + + fireEvent.blur(rankInput, { target: { value: 1 } }); + expect(rankInput.value).toBe("1"); + + expect(trigPrioInput.value).toBe("1000"); // Check for default value + fireEvent.blur(trigPrioInput, { target: { value: 100 } }); + expect(trigPrioInput.value).toBe("100"); // Check for new value + + fireEvent.click(trigger); + expect(trigger.hasAttribute("checked")).toBeTruthy(); + + // Before selecting Project Category + expect(content.queryAllByText('Select Project Category').length).toBe(2); + expect(content.queryAllByText('Regular').length).toBe(1); + expect(content.getAllByRole("listbox")[0].children.length).toBe(2); + fireEvent.click(projCatInput); + // After selecting Project Category + expect(content.queryAllByText('Select Project Category').length).toBe(1); + expect(content.queryAllByText('Regular').length).toBe(3); + + // Before selecting Period Category + expect(content.queryAllByText('Select Period Category').length).toBe(2); + expect(content.queryAllByText('Single Cycle').length).toBe(1); + expect(content.getAllByRole("listbox")[1].children.length).toBe(2); + fireEvent.click(projPeriodInput); + // After selecting Period Category + expect(content.queryAllByText('Select Period Category').length).toBe(1); + expect(content.queryAllByText('Single Cycle').length).toBe(3); + + // Before selecting Cycle + expect(content.queryAllByText('Cycle-0').length).toBe(1); + expect(content.getAllByRole("listbox")[2].children.length).toBe(2); + fireEvent.click(cycleInput); + // After selecting Cycle + expect(content.queryAllByText('Cycle-0').length).toBe(2); + + expect(content.queryByTestId('save-btn').hasAttribute("disabled")).toBeFalsy(); + // }); +}); + +it("save project with mandatory fields", async () => { + console.log("save project -----------------------"); + let content; + await act(async () => { + content = render(<Router><ProjectCreate /></Router>); + }); + + const nameInput = content.queryByTestId('name'); + const descInput = content.queryByTestId('description'); + const spinButtons = content.queryAllByRole("spinbutton"); + const rankInput = spinButtons.filter(function(element) { return element.id==="proj-rank"})[0]; + + fireEvent.change(nameInput, { target: { value: 'OSR' } }); + expect(nameInput.value).toBe("OSR"); + fireEvent.change(descInput, { target: { value: 'OSR' } }); + expect(descInput.value).toBe("OSR"); + fireEvent.blur(rankInput, { target: { value: 1 } }); + expect(rankInput.value).toBe("1"); + expect(content.queryByTestId('save-btn').hasAttribute("disabled")).toBeFalsy(); + expect(content.queryByTestId('projectId').value).toBe(""); + expect(content.queryByText("Success")).toBe(null); + + await act(async () => { + fireEvent.click(content.queryByTestId('save-btn')); + }); + + // After saving project, URL should be available and Success dialog should be displayed + expect(content.queryByTestId('projectId').value).toBe("http://localhost:3000/api/project/OSR"); + expect(content.queryByText("Success")).not.toBe(null); +}); + +it("save project with default resources", async () => { + console.log("save project with default resources -----------------------"); + let content; + await act(async () => { + content = render(<Router><ProjectCreate /></Router>); + }); + + const nameInput = content.queryByTestId('name'); + const descInput = content.queryByTestId('description'); + const spinButtons = content.queryAllByRole("spinbutton"); + const rankInput = spinButtons.filter(function(element) { return element.id==="proj-rank"})[0]; + + fireEvent.change(nameInput, { target: { value: 'OSR' } }); + expect(nameInput.value).toBe("OSR"); + fireEvent.change(descInput, { target: { value: 'OSR' } }); + expect(descInput.value).toBe("OSR"); + fireEvent.blur(rankInput, { target: { value: 1 } }); + expect(rankInput.value).toBe("1"); + expect(content.queryByTestId('save-btn').hasAttribute("disabled")).toBeFalsy(); + expect(content.queryByTestId('projectId').value).toBe(""); + expect(content.queryByText("Success")).toBe(null); + + const lofarObsTimeInput = content.queryByPlaceholderText('LOFAR Observing Time'); + fireEvent.change(lofarObsTimeInput, { target: { value: 10 } }); + expect(lofarObsTimeInput.value).toBe('10'); + + const lofarObsTimeAInput = content.queryByPlaceholderText('LOFAR Observing Time prio A'); + fireEvent.change(lofarObsTimeAInput, { target: { value: 15 } }); + expect(lofarObsTimeAInput.value).toBe('15'); + + const lofarObsTimeBInput = content.queryByPlaceholderText('LOFAR Observing Time prio B'); + fireEvent.change(lofarObsTimeBInput, { target: { value: 20 } }); + expect(lofarObsTimeBInput.value).toBe('20'); + + const cepProcTimeInput = content.queryByPlaceholderText('CEP Processing Time'); + fireEvent.change(cepProcTimeInput, { target: { value: 5 } }); + expect(cepProcTimeInput.value).toBe('5'); + + const ltaStorageInput = content.queryByPlaceholderText('LTA Storage'); + fireEvent.change(ltaStorageInput, { target: { value: 2 } }); + expect(ltaStorageInput.value).toBe('2'); + + const noOfTriggerInput = content.queryByPlaceholderText('Number of triggers'); + fireEvent.change(noOfTriggerInput, { target: { value: 3 } }); + expect(noOfTriggerInput.value).toBe('3'); + + const lofarSupTimeInput = content.queryByPlaceholderText('LOFAR Support Time'); + fireEvent.change(lofarSupTimeInput, { target: { value: 25 } }); + expect(lofarSupTimeInput.value).toBe('25'); + + await act(async () => { + fireEvent.click(content.queryByTestId('save-btn')); + }); + + // After saving project, URL should be available and Success dialog should be displayed + expect(content.queryByTestId('projectId').value).toBe("http://localhost:3000/api/project/OSR"); + expect(content.queryByText("Success")).not.toBe(null); +}); + +it("save project with added resources", async () => { + console.log("save project with added resources -----------------------"); + let content; + await act(async () => { + content = render(<Router><ProjectCreate /></Router>); + }); + + const nameInput = content.queryByTestId('name'); + const descInput = content.queryByTestId('description'); + const spinButtons = content.queryAllByRole("spinbutton"); + const rankInput = spinButtons.filter(function(element) { return element.id==="proj-rank"})[0]; + + fireEvent.change(nameInput, { target: { value: 'OSR' } }); + expect(nameInput.value).toBe("OSR"); + fireEvent.change(descInput, { target: { value: 'OSR' } }); + expect(descInput.value).toBe("OSR"); + fireEvent.blur(rankInput, { target: { value: 1 } }); + expect(rankInput.value).toBe("1"); + expect(content.queryByTestId('save-btn').hasAttribute("disabled")).toBeFalsy(); + expect(content.queryByTestId('projectId').value).toBe(""); + expect(content.queryByText("Success")).toBe(null); + + const lofarObsTimeInput = content.queryByPlaceholderText('LOFAR Observing Time'); + fireEvent.change(lofarObsTimeInput, { target: { value: 10 } }); + expect(lofarObsTimeInput.value).toBe('10'); + + const lofarObsTimeAInput = content.queryByPlaceholderText('LOFAR Observing Time prio A'); + fireEvent.change(lofarObsTimeAInput, { target: { value: 15 } }); + expect(lofarObsTimeAInput.value).toBe('15'); + + const lofarObsTimeBInput = content.queryByPlaceholderText('LOFAR Observing Time prio B'); + fireEvent.change(lofarObsTimeBInput, { target: { value: 20 } }); + expect(lofarObsTimeBInput.value).toBe('20'); + + const cepProcTimeInput = content.queryByPlaceholderText('CEP Processing Time'); + fireEvent.change(cepProcTimeInput, { target: { value: 5 } }); + expect(cepProcTimeInput.value).toBe('5'); + + const ltaStorageInput = content.queryByPlaceholderText('LTA Storage'); + fireEvent.change(ltaStorageInput, { target: { value: 2 } }); + expect(ltaStorageInput.value).toBe('2'); + + const noOfTriggerInput = content.queryByPlaceholderText('Number of triggers'); + fireEvent.change(noOfTriggerInput, { target: { value: 3 } }); + expect(noOfTriggerInput.value).toBe('3'); + + const lofarSupTimeInput = content.queryByPlaceholderText('LOFAR Support Time'); + fireEvent.change(lofarSupTimeInput, { target: { value: 25 } }); + expect(lofarSupTimeInput.value).toBe('25'); + + // Before selecting New Resource + expect(content.queryAllByText('Add Resources').length).toBe(2); + expect(content.queryAllByText('Support hours').length).toBe(1); + expect(content.getAllByRole("listbox")[3].children.length).toBe(2); + expect(content.queryByPlaceholderText('Support hours')).toBe(null); + const addResourceInput = content.getAllByRole("listbox")[3].children[1] ; + fireEvent.click(addResourceInput); + // After selecting New Resource + expect(content.queryAllByText('Add Resources').length).toBe(1); + expect(content.queryAllByText('Support hours').length).toBe(3); + + const addResourceBtn = content.queryByTestId('add_res_btn'); + fireEvent.click(addResourceBtn); + expect(content.queryAllByText('Add Resources').length).toBe(2); + expect(content.queryByPlaceholderText('Support hours')).not.toBe(null); + + const newResourceInput = content.queryByPlaceholderText('Support hours'); + fireEvent.change(newResourceInput, { target: { value: 30 } }); + expect(newResourceInput.value).toBe('30'); + + + await act(async () => { + fireEvent.click(content.queryByTestId('save-btn')); + }); + + // After saving project, URL should be available and Success dialog should be displayed + expect(content.queryByTestId('projectId').value).toBe("http://localhost:3000/api/project/OSR"); + expect(content.queryByText("Success")).not.toBe(null); +}); + +it("remove default resource and added resource", async () => { + console.log("remove default resource and added resource -----------------------"); + let content; + await act(async () => { + content = render(<Router><ProjectCreate /></Router>); + }); + + // Before selecting New Resource + expect(content.queryAllByText('Add Resources').length).toBe(2); + expect(content.queryAllByText('Support hours').length).toBe(1); + expect(content.getAllByRole("listbox")[3].children.length).toBe(2); + expect(content.queryByPlaceholderText('Support hours')).toBe(null); + const addResourceInput = content.getAllByRole("listbox")[3].children[1] ; + fireEvent.click(addResourceInput); + // After selecting New Resource + expect(content.queryAllByText('Add Resources').length).toBe(1); + expect(content.queryAllByText('Support hours').length).toBe(3); + + const addResourceBtn = content.queryByTestId('add_res_btn'); + fireEvent.click(addResourceBtn); + expect(content.queryAllByText('Add Resources').length).toBe(2); + expect(content.queryByPlaceholderText('Support hours')).not.toBe(null); + + expect(content.queryByPlaceholderText('CEP Processing Time')).not.toBe(null); + expect(content.queryByTestId('CEP Processing Time-btn')).not.toBe(null); + const removeDefResBtn = content.queryByTestId('CEP Processing Time-btn'); + await act(async () => { + fireEvent.click(content.queryByTestId('CEP Processing Time-btn')); + }); + expect(content.queryByPlaceholderText('CEP Processing Time')).toBe(null); + expect(content.queryByTestId('CEP Processing Time-btn')).toBe(null); + + const removeResourceBtn = content.queryByTestId('Support hours-btn'); + fireEvent.click(removeResourceBtn); + expect(content.queryAllByText('Add Resources').length).toBe(2); + expect(content.queryAllByText('Support hours').length).toBe(1); + expect(content.getAllByRole("listbox")[3].children.length).toBe(3); + +}); \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/edit.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/edit.js new file mode 100644 index 0000000000000000000000000000000000000000..78b443a5c2d1eb457b3806a0cc4fe89e3fd2ab99 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/edit.js @@ -0,0 +1,532 @@ +import React, {Component} from 'react'; +import { Link, 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 { Button } from 'primereact/button'; +import {Dialog} from 'primereact/components/dialog/Dialog'; +import {Growl} from 'primereact/components/growl/Growl'; + +import {ResourceInputList} from './ResourceInputList'; + +import AppLoader from '../../layout/components/AppLoader'; +import CycleService from '../../services/cycle.service'; +import ProjectService from '../../services/project.service'; +import UnitConverter from '../../utils/unit.converter'; +import UIConstants from '../../utils/ui.constants'; + +export class ProjectEdit extends Component { + constructor(props) { + super(props); + this.state = { + isLoading: true, + dialog: { header: '', detail: ''}, + project: { + trigger_priority: 1000, + priority_rank: null, + quota: [] // Mandatory Field in the back end + }, + projectQuota: {}, // Holds the value of resources selected with resource_type_id as key + validFields: {}, // Holds the list of valid fields based on the form rules + validForm: false, // To enable Save Button + errors: {}, + periodCategories: [], + projectCategories: [], + resources: [], // Selected resources for the project + resourceList: [], // Available resources to select for the project + cycles: [], + redirect: this.props.match.params.id?"":'/project/list' //If no project name passed redirect to Project list page + } + this.projectQuota = [] // Holds the old list of project_quota saved for the project + // Validation Rules + this.formRules = { + name: {required: true, message: "Name can not be empty"}, + description: {required: true, message: "Description can not be empty"}, + priority_rank: {required: true, message: "Enter Project Rank"} + }; + this.defaultResources = [{name:'LOFAR Observing Time'}, + {name:'LOFAR Observing Time prio A'}, + {name:'LOFAR Observing Time prio B'}, + {name:'LOFAR Processing Time'}, + {name:'Allocation storage'}, + {name:'Number of triggers'}, + {name:'LOFAR Support hours'} ]; + this.projectResourceDefaults = {}; + this.resourceUnitMap = UnitConverter.resourceUnitMap; + this.tooltipOptions = UIConstants.tooltipOptions; + + this.getProjectDetails = this.getProjectDetails.bind(this); + this.cycleOptionTemplate = this.cycleOptionTemplate.bind(this); + this.setProjectQuotaDefaults = this.setProjectQuotaDefaults.bind(this); + this.setProjectParams = this.setProjectParams.bind(this); + this.addNewResource = this.addNewResource.bind(this); + this.removeResource = this.removeResource.bind(this); + this.setProjectQuotaParams = this.setProjectQuotaParams.bind(this); + this.saveProject = this.saveProject.bind(this); + this.saveProjectQuota = this.saveProjectQuota.bind(this); + this.cancelEdit = this.cancelEdit.bind(this); + } + + componentDidMount() { + ProjectService.getDefaultProjectResources() + .then(defaults => { + this.projectResourceDefaults = defaults; + }); + CycleService.getAllCycles() + .then(cycles => { + this.setState({cycles: cycles}); + }); + ProjectService.getProjectCategories() + .then(categories => { + this.setState({projectCategories: categories}); + }); + ProjectService.getPeriodCategories() + .then(categories => { + this.setState({periodCategories: categories}); + }); + ProjectService.getResources() + .then(resourceList => { + this.setState({resourceList: resourceList}); + }) + .then((resourceList, resources) => { + this.getProjectDetails(); + }); + } + + /** + * Function retrieve project details and resource allocations(project_quota) and assign to appropriate varaibles + */ + async getProjectDetails() { + let project = await ProjectService.getProjectDetails(this.props.match.params.id); + let resourceList = this.state.resourceList; + let projectQuota = {}; + if (project) { + // Get project_quota for the project and asssign to the component variable + for (const id of project.quota_ids) { + let quota = await ProjectService.getProjectQuota(id); + let resource = _.find(resourceList, ['name', quota.resource_type_id]); + quota.resource = resource; + this.projectQuota.push(quota); + const conversionFactor = this.resourceUnitMap[resource.quantity_value]?this.resourceUnitMap[resource.quantity_value].conversionFactor:1; + projectQuota[quota.resource_type_id] = quota.value / conversionFactor; + }; + // Remove the already assigned resources from the resoureList + const resources = _.remove(resourceList, (resource) => { return _.find(this.projectQuota, {'resource_type_id': resource.name})!=null }); + this.setState({project: project, resourceList: resourceList, resources: resources, + projectQuota: projectQuota, isLoading: false}); + + // Validate form if all values are as per the form rules and enable Save button + this.validateForm(); + } else { + this.setState({redirect: '../../not-found'}); + } + } + + /** + * Cycle option sub-component with cycle object + */ + cycleOptionTemplate(option) { + return ( + <div className="p-clearfix"> + <span style={{fontSize:'1em',float:'right',margin:'1em .5em 0 0'}}>{option.name}</span> + </div> + ); + } + + /** + * Function to set project resource allocation + * @param {Array} resources + */ + setProjectQuotaDefaults(resources) { + let projectQuota = this.state.projectQuota; + for (const resource of resources) { + const conversionFactor = this.resourceUnitMap[resource.quantity_value]?this.resourceUnitMap[resource.quantity_value].conversionFactor:1; + projectQuota[resource['name']] = this.projectResourceDefaults[resource.name]/conversionFactor; + } + return projectQuota; + } + + /** + * Function to add new resource to project + */ + addNewResource(){ + if (this.state.newResource) { + let resourceList = 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}); + } + } + + /** + * Callback function to be called from ResourceInpulList when a resource is removed from it + * @param {string} name - resource_type_id + */ + removeResource(name) { + let resources = this.state.resources; + let resourceList = this.state.resourceList; + let projectQuota = 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}); + } + + /** + * Function to call on change and blur events from input components + * @param {string} key + * @param {any} value + */ + setProjectParams(key, value, type) { + let project = this.state.project; + switch(type) { + case 'NUMBER': { + console.log("Parsing Number"); + project[key] = value?parseInt(value):0; + break; + } + default: { + project[key] = value; + break; + } + } + this.setState({project: project, validForm: this.validateForm(key)}); + } + + /** + * Callback Function to call from ResourceInputList on change and blur events + * @param {string} key + * @param {InputEvent} event + */ + setProjectQuotaParams(key, event) { + let projectQuota = this.state.projectQuota; + 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,''); + } else { + newValue = event.target.value; + } + projectQuota[key] = (newValue==="NaN" || isNaN(newValue))?0:newValue; + } else { + let projectQuota = this.state.projectQuota; + projectQuota[key] = 0; + } + this.setState({projectQuota: projectQuota}); + } + + /** + * 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.project[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.project[fieldName]; + if (rule.required) { + if (!fieldValue) { + errors[fieldName] = rule.message?rule.message:`${fieldName} is required`; + } else { + validFields[fieldName] = true; + } + } + } + } + + if (Object.keys(validFields).length === Object.keys(this.formRules).length) { + validForm = true; + } + this.setState({errors: errors, validFields: validFields, validForm: validForm}); + return validForm; + } + + /** + * Function to call when 'Save' button is clicked to update the project. + */ + saveProject() { + if (this.validateForm) { + ProjectService.updateProject(this.props.match.params.id, this.state.project) + .then(async (project) => { + if (project && this.state.project.updated_at !== project.updated_at) { + this.saveProjectQuota(project); + } else { + this.growl.show({severity: 'error', summary: 'Error Occured', detail: 'Unable to update Project'}); + this.setState({errors: project}); + } + }); + } + } + + /** + * Function to Create, Update & Delete project_quota for the project + */ + async saveProjectQuota(project) { + let dialog = {}; + let quotaError = {}; + let updatingProjectQuota = []; + let newProjectQuota = []; + let deletingProjectQuota = []; + for (const resource in this.state.projectQuota) { + const resourceType = _.find(this.state.resources, {'name': resource}); + const conversionFactor = this.resourceUnitMap[resourceType.quantity_value]?this.resourceUnitMap[resourceType.quantity_value].conversionFactor:1 + let quotaValue = this.state.projectQuota[resource] * conversionFactor; + let existingQuota = _.find(this.projectQuota, {'resource_type_id': resource}); + if (!existingQuota) { + let quota = { project: project.url, + resource_type: resourceType['url'], + value: quotaValue }; + newProjectQuota.push(quota); + } else if (existingQuota && existingQuota.value !== quotaValue) { + existingQuota.project = project.url; + existingQuota.value = quotaValue; + updatingProjectQuota.push(existingQuota); + } + } + let projectQuota = this.state.projectQuota; + deletingProjectQuota = _.filter(this.projectQuota, function(quota) { return !projectQuota[quota.resource_type_id]}); + + for (const projectQuota of deletingProjectQuota) { + const deletedProjectQuota = await ProjectService.deleteProjectQuota(projectQuota); + if (!deletedProjectQuota) { + quotaError[projectQuota.resource_type_id] = true; + } + } + for (const projectQuota of updatingProjectQuota) { + const updatedProjectQuota = await ProjectService.updateProjectQuota(projectQuota); + if (!updatedProjectQuota) { + quotaError[projectQuota.resource_type_id] = true; + } + } + for (const projectQuota of newProjectQuota) { + const createdProjectQuota = await ProjectService.saveProjectQuota(projectQuota); + if (!createdProjectQuota) { + quotaError[projectQuota.resource_type_id] = true; + } + } + if (_.keys(quotaError).length === 0) { + dialog = {header: 'Success', detail: 'Project updated successfully.'}; + } else { + dialog = {header: 'Error', detail: 'Project updated successfully but resource allocation not updated properly. Try again!'}; + } + this.setState({dialogVisible: true, dialog: dialog}); + } + + /** + * Cancel edit and redirect to Project View page + */ + cancelEdit() { + this.setState({redirect: `/project/view/${this.state.project.name}`}); + } + + render() { + if (this.state.redirect) { + return <Redirect to={ {pathname: this.state.redirect} }></Redirect> + } + + return ( + <React.Fragment> + <div className="p-grid"> + <Growl ref={(el) => this.growl = el} /> + + <div className="p-col-10 p-lg-10 p-md-10"> + <h2>Project - Edit</h2> + </div> + <div className="p-col-2 p-lg-2 p-md-2"> + <Link to={{ pathname: `/project/view/${this.state.project.name}`}} title="Close Edit" style={{float: "right"}}> + <i className="fa fa-window-close" style={{marginTop: "10px"}}></i> + </Link> + </div> + </div> + + { this.state.isLoading ? <AppLoader/> : + <> + <div> + <div className="p-fluid"> + <div className="p-field p-grid"> + <label htmlFor="projectName" className="col-lg-2 col-md-2 col-sm-12">Name <span style={{color:'red'}}>*</span></label> + <div className="col-lg-3 col-md-3 col-sm-12"> + <InputText className={this.state.errors.name ?'input-error':''} id="projectName" data-testid="name" + tooltip="Enter name of the project" tooltipOptions={this.tooltipOptions} maxLength="128" + value={this.state.project.name} + onChange={(e) => this.setProjectParams('name', e.target.value)} + onBlur={(e) => this.setProjectParams('name', e.target.value)}/> + <label className={this.state.errors.name?"error":"info"}> + {this.state.errors.name ? this.state.errors.name : "Max 128 characters"} + </label> + </div> + <div className="col-lg-1 col-md-1 col-sm-12"></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-3 col-md-3 col-sm-12"> + <InputTextarea className={this.state.errors.description ?'input-error':''} rows={3} cols={30} + tooltip="Short description of the project" tooltipOptions={this.tooltipOptions} maxLength="128" + data-testid="description" value={this.state.project.description} + onChange={(e) => this.setProjectParams('description', e.target.value)} + onBlur={(e) => this.setProjectParams('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 className="p-field p-grid"> + <label htmlFor="triggerPriority" className="col-lg-2 col-md-2 col-sm-12">Trigger Priority </label> + <div className="col-lg-3 col-md-3 col-sm-12"> + <InputNumber inputId="trig_prio" name="trig_prio" className={this.state.errors.name ?'input-error':''} + tooltip="Priority of this project with respect to triggers" tooltipOptions={this.tooltipOptions} + value={this.state.project.trigger_priority} showButtons + min={0} max={1001} step={10} useGrouping={false} + onChange={(e) => this.setProjectParams('trigger_priority', e.value)} + onBlur={(e) => this.setProjectParams('trigger_priority', e.target.value, 'NUMBER')} /> + <label className="error"> + {this.state.errors.trigger_priority ? this.state.errors.trigger_priority : ""} + </label> + </div> + <div className="col-lg-1 col-md-1 col-sm-12"></div> + <label htmlFor="trigger" className="col-lg-2 col-md-2 col-sm-12">Allows Trigger Submission</label> + <div className="col-lg-3 col-md-3 col-sm-12"> + <Checkbox inputId="trigger" role="trigger" + tooltip="Is this project allowed to supply observation requests on the fly, possibly interrupting currently running observations (responsive telescope)?" + tooltipOptions={this.tooltipOptions} + checked={this.state.project.can_trigger} onChange={e => this.setProjectParams('can_trigger', e.target.checked)}></Checkbox> + </div> + </div> + <div className="p-field p-grid"> + <label htmlFor="projCategory" className="col-lg-2 col-md-2 col-sm-12">Project Category </label> + <div className="col-lg-3 col-md-3 col-sm-12"> + <Dropdown inputId="projCat" optionLabel="value" optionValue="url" + tooltip="Project Category" tooltipOptions={this.tooltipOptions} + value={this.state.project.project_category} + options={this.state.projectCategories} + onChange={(e) => {this.setProjectParams('project_category', e.value)}} + placeholder="Select Project Category" /> + </div> + <div className="col-lg-1 col-md-1 col-sm-12"></div> + <label htmlFor="periodCategory" className="col-lg-2 col-md-2 col-sm-12">Period Category</label> + <div className="col-lg-3 col-md-3 col-sm-12"> + <Dropdown data-testid="period-cat" id="period-cat" optionLabel="value" optionValue="url" + tooltip="Period Category" tooltipOptions={this.tooltipOptions} + value={this.state.project.period_category} + options={this.state.periodCategories} + onChange={(e) => {this.setProjectParams('period_category',e.value)}} + placeholder="Select Period Category" /> + </div> + </div> + <div className="p-field p-grid"> + <label htmlFor="triggerPriority" className="col-lg-2 col-md-2 col-sm-12">Cycle(s)</label> + <div className="col-lg-3 col-md-3 col-sm-12"> + <MultiSelect data-testid="cycle" id="cycle" optionLabel="name" optionValue="url" filter={true} + tooltip="Cycle(s) to which this project belongs" tooltipOptions={this.tooltipOptions} + value={this.state.project.cycles} + options={this.state.cycles} + onChange={(e) => {this.setProjectParams('cycles',e.value)}} + + /> + </div> + <div className="col-lg-1 col-md-1 col-sm-12"></div> + <label htmlFor="projRank" className="col-lg-2 col-md-2 col-sm-12">Project Rank <span style={{color:'red'}}>*</span></label> + <div className="col-lg-3 col-md-3 col-sm-12"> + <InputNumber inputId="proj-rank" name="rank" data-testid="rank" value={this.state.project.priority_rank} + tooltip="Priority of this project with respect to other projects. Projects can interrupt observations of lower-priority projects." + tooltipOptions={this.tooltipOptions} + mode="decimal" showButtons min={0} max={100} + onChange={(e) => this.setProjectParams('priority_rank', e.value)} + onBlur={(e) => this.setProjectParams('priority_rank', e.target.value, 'NUMBER')} /> + <label className="error"> + {this.state.errors.priority_rank ? this.state.errors.priority_rank : ""} + </label> + </div> + </div> + {this.state.resourceList && + <div className="p-fluid"> + <div className="p-field p-grid"> + <div className="col-lg-2 col-md-2 col-sm-12"> + <h5>Resource Allocations:</h5> + </div> + <div className="col-lg-3 col-md-3 col-sm-10"> + <Dropdown optionLabel="name" optionValue="name" + tooltip="Resources to be allotted for the project" + tooltipOptions={this.tooltipOptions} + value={this.state.newResource} + options={_.sortBy(this.state.resourceList, ['name'])} + onChange={(e) => {this.setState({'newResource': e.value})}} + placeholder="Add Resources" /> + </div> + <div className="col-lg-2 col-md-2 col-sm-2"> + <Button label="" className="p-button-primary" icon="pi pi-plus" onClick={this.addNewResource} disabled={!this.state.newResource} data-testid="add_res_btn" /> + </div> + </div> + {/* {_.keys(this.state.projectQuota).length>0 && */} + <div className="p-field p-grid resource-input-grid"> + <ResourceInputList list={this.state.resources} unitMap={this.resourceUnitMap} + projectQuota={this.state.projectQuota} callback={this.setProjectQuotaParams} + removeInputCallback={this.removeResource} /> + </div> + {/* } */} + </div> + } + </div> + </div> + <div className="p-grid p-justify-start act-btn-grp"> + <div className="p-col-1"> + <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} /> + </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: '30vw'}} inputId="confirm_dialog" + modal={true} onHide={() => {this.setState({dialogVisible: false})}} + footer={<div> + <Button key="back" onClick={() => {this.setState({dialogVisible: false}); this.cancelEdit();}} label="Ok" /> + {/* <Button key="submit" type="primary" onClick={this.reset} label="Yes" /> */} + </div> + } > + <div className="p-grid"> + <div className="col-lg-2 col-md-2 col-sm-2"> + <i className="pi pi-check-circle pi-large pi-success"></i> + </div> + <div className="col-lg-10 col-md-10 col-sm-10"> + <span style={{marginTop:"5px"}}>{this.state.dialog.detail}</span> + </div> + </div> + </Dialog> + </div> + </React.Fragment> + ); + } +} \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/edit.test.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/edit.test.js new file mode 100644 index 0000000000000000000000000000000000000000..dd13177fac9e2e1321d5b873783a691d4b96e769 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/edit.test.js @@ -0,0 +1,267 @@ +import React from 'react'; +import { BrowserRouter as Router } from 'react-router-dom'; +import { act } from "react-dom/test-utils"; +import { render, cleanup, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; +import _ from 'lodash'; +import moment from 'moment'; + +import {ProjectEdit} from './edit'; +import ProjectService from '../../services/project.service'; +import CycleService from '../../services/cycle.service'; + +import ProjectServiceMock from '../../__mocks__/project.service.data'; + + +let projectCategoriesSpy, allCycleSpy, periodCategoriesSpy, projectDetailsSpy, resourcesSpy, projectQuotaSpy, + updateProjectSpy, savePQSpy, updatePQSpy, deletePQSpy; + +beforeEach(() => { + setMockSpy(); +}); + +afterEach(() => { + // cleanup on exiting + clearMockSpy(); + cleanup(); +}); + +/** + * To set mock spy for Services that have API calls to the back end to fetch data + */ +const setMockSpy = (() => { + projectCategoriesSpy = jest.spyOn(ProjectService, 'getProjectCategories'); + projectCategoriesSpy.mockImplementation(() => { return Promise.resolve([{url: "Regular", value: 'Regular'}, {url: "User Shared Support", value: 'User Shared Support'}])}); + periodCategoriesSpy = jest.spyOn(ProjectService, 'getPeriodCategories'); + periodCategoriesSpy.mockImplementation(() => { return Promise.resolve([{url: "Single Cycle", value: 'Single Cycle'}, {url: "Long Term", value: 'Long Term'}])}); + allCycleSpy = jest.spyOn(CycleService, 'getAllCycles'); + allCycleSpy.mockImplementation(() => { + return Promise.resolve([{url: "http://localhost:3000/api/cycle/Cycle-0/", name: 'Cycle-0'}, + {url: "http://localhost:3000/api/cycle/Cycle-1/", name: 'Cycle-1'}, + {url: "http://192.168.99.100:8008/api/cycle/Cycle%200/", name: 'Cycle 0'}]); + }); + projectDetailsSpy = jest.spyOn(ProjectService, 'getProjectDetails'); + projectDetailsSpy.mockImplementation((id) => { + return Promise.resolve(_.find(ProjectServiceMock.project, {name: id}))}); + resourcesSpy = jest.spyOn(ProjectService, 'getResources'); + resourcesSpy.mockImplementation(() => { + // console.log(ProjectServiceMock.resources); + let resourceList= []; + Object.assign(resourceList, ProjectServiceMock.resources); + return Promise.resolve(resourceList); + }); + projectQuotaSpy = jest.spyOn(ProjectService, 'getProjectQuota'); + projectQuotaSpy.mockImplementation((id) => { + let quota = {}; + Object.assign(quota, _.find(ProjectServiceMock.projectQuota, {id: id})); + return Promise.resolve(quota); + }); + updateProjectSpy = jest.spyOn(ProjectService, 'updateProject'); + updateProjectSpy.mockImplementation((id, project) => { + let updatedProject = {}; + Object.assign(updatedProject, _.find(ProjectServiceMock.project, {name: id})); + updatedProject.name = project.name; + updatedProject.updated_at = new Date(); + return Promise.resolve(updatedProject); + }); + savePQSpy = jest.spyOn(ProjectService, 'saveProjectQuota'); + savePQSpy.mockImplementation(() => { + return Promise.resolve(ProjectServiceMock.projectQuota[0]); + }); + updatePQSpy = jest.spyOn(ProjectService, 'updateProjectQuota'); + updatePQSpy.mockImplementation((quota) => { + return Promise.resolve(_.find(ProjectServiceMock.projectQuota, {id: quota.id})); + }); + deletePQSpy = jest.spyOn(ProjectService, 'deleteProjectQuota'); + deletePQSpy.mockImplementation(() => { + return Promise.resolve({message: 'deleted'}); + }); +}); + +const clearMockSpy = (() => { + projectCategoriesSpy.mockRestore(); + periodCategoriesSpy.mockRestore(); + projectDetailsSpy.mockRestore(); + resourcesSpy.mockRestore(); + projectQuotaSpy.mockRestore(); + updateProjectSpy.mockRestore(); + savePQSpy.mockRestore(); + updatePQSpy.mockRestore(); + deletePQSpy.mockRestore(); +}); + +it("renders nothing if no project details found", async () => { + console.log("renders nothing if no project details found.........."); + let content; + await act(async () => { + content = render(<Router><ProjectEdit match={{params:{id: "OSR-12"}}} location={{}} /></Router>); + }); + + expect(content.queryByText("Project - Edit")).toBe(null); +}); + +it("renders input fields with Project details if found", async () => { + console.log("renders input fields with Project details if found.........."); + let content; + await act(async () => { + content = render(<Router><ProjectEdit match={{params:{id: "OSR-11"}}} location={{}} /></Router>); + }); + + // expect(content.baseElement).toBe(null); + expect(content.queryByText("Project - Edit")).not.toBe(null); + expect(content.queryByTestId("name").value).toBe('OSR-11'); + + const spinButtons = content.queryAllByRole("spinbutton"); + const trigPrioInput = spinButtons.filter(function(element) { return element.id==="trig_prio"})[0]; + expect(trigPrioInput.value).toBe("990"); + + const rankInput = spinButtons.filter(function(element) { return element.id==="proj-rank"})[0]; + expect(rankInput.value).toBe("5"); + + const trigger = content.getAllByLabelText(/trigger/i).filter((element) => { return element.id==="trigger"})[0]; + expect(trigger.hasAttribute("checked")).toBeTruthy(); + + const projCatInput = content.getAllByRole("listbox")[0].children[0] ; + expect(content.queryAllByText('Select Project Category').length).toBe(1); + expect(content.queryAllByText('Regular').length).toBe(3); + + const projPeriodInput = content.getAllByRole("listbox")[1].children[0] ; + expect(content.queryAllByText('Select Period Category').length).toBe(1); + expect(content.queryAllByText('Single Cycle').length).toBe(3); + + const cycleInput = content.getAllByRole("listbox")[2] ; + expect(content.queryAllByText('Cycle 0').length).toBe(2); + + expect(content.queryByPlaceholderText("CEP Processing Time").value).toBe("10 Hours"); + expect(content.queryByPlaceholderText("LOFAR Observing Time").value).toBe("20 Hours"); + expect(content.queryByPlaceholderText("LOFAR Observing Time prio A").value).toBe("30 Hours"); + expect(content.queryByPlaceholderText("LOFAR Observing Time prio B").value).toBe("40 Hours"); + expect(content.queryByPlaceholderText("LOFAR Support Time").value).toBe("50 Hours"); + expect(content.queryByPlaceholderText("LTA Storage").value).toBe("6 TB"); + expect(content.queryByPlaceholderText("Number of triggers").value).toBe("7 Numbers"); + expect(content.queryByPlaceholderText("Support hours").value).toBe("8 "); + + expect(content.queryByTestId('save-btn').hasAttribute("disabled")).toBeFalsy(); + +}); + +it("save Project after editing fields", async () => { + console.log("save Project after editing fields .........."); + let content; + await act(async () => { + content = render(<Router><ProjectEdit match={{params:{id: "OSR-11"}}} location={{}} /></Router>); + }); + + // expect(content.baseElement).toBe(null); + expect(content.queryByText("Project - Edit")).not.toBe(null); + expect(content.queryByTestId("name").value).toBe('OSR-11'); + + const spinButtons = content.queryAllByRole("spinbutton"); + const trigPrioInput = spinButtons.filter(function(element) { return element.id==="trig_prio"})[0]; + fireEvent.blur(trigPrioInput, { target: { value: 900 } }); + expect(trigPrioInput.value).toBe("900"); + + const rankInput = spinButtons.filter(function(element) { return element.id==="proj-rank"})[0]; + fireEvent.blur(rankInput, { target: { value: 2 } }); + expect(rankInput.value).toBe("2"); + + const trigger = content.getAllByLabelText(/trigger/i).filter((element) => { return element.id==="trigger"})[0]; + fireEvent.click(trigger); + expect(trigger.hasAttribute("checked")).toBeFalsy(); + + const projCatInput = content.getAllByRole("listbox")[0].children[1] ; + fireEvent.click(projCatInput); + // After selecting Project Category + expect(content.queryAllByText('Select Project Category').length).toBe(1); + expect(content.queryAllByText('Regular').length).toBe(1); + expect(content.queryAllByText('User Shared Support').length).toBe(3); + + const projPeriodInput = content.getAllByRole("listbox")[1].children[1] ; + fireEvent.click(projPeriodInput); + expect(content.queryAllByText('Select Period Category').length).toBe(1); + expect(content.queryAllByText('Single Cycle').length).toBe(1); + expect(content.queryAllByText('Long Term').length).toBe(3); + + const oldCycleInput = content.getAllByRole("listbox")[2].children[2] ; + const newCycleInput = content.getAllByRole("listbox")[2].children[0] ; + fireEvent.click(oldCycleInput); + fireEvent.click(newCycleInput); + // After selecting Cycle + expect(content.queryAllByText('Cycle-0').length).toBe(2); + expect(content.queryAllByText('Cycle 0').length).toBe(1); + + const lofarObsTimeInput = content.queryByPlaceholderText('LOFAR Observing Time'); + fireEvent.blur(lofarObsTimeInput, { target: { value: 10 } }); + expect(lofarObsTimeInput.value).toBe('10 Hours'); + + const cepProcTimeInput = content.queryByPlaceholderText('CEP Processing Time'); + fireEvent.blur(cepProcTimeInput, { target: { value: 5 } }); + expect(cepProcTimeInput.value).toBe('5 Hours'); + + const ltaStorageInput = content.queryByPlaceholderText('LTA Storage'); + fireEvent.blur(ltaStorageInput, { target: { value: 2 } }); + expect(ltaStorageInput.value).toBe('2 TB'); + + const noOfTriggerInput = content.queryByPlaceholderText('Number of triggers'); + fireEvent.blur(noOfTriggerInput, { target: { value: 3 } }); + expect(noOfTriggerInput.value).toBe('3 Numbers'); + + expect(content.queryByTestId('save-btn').hasAttribute("disabled")).toBeFalsy(); + + await act(async () => { + fireEvent.click(content.queryByTestId('save-btn')); + }); + + // After saving project, Success dialog should be displayed + expect(updateProjectSpy).toHaveBeenCalledTimes(1); + expect(updatePQSpy).toHaveBeenCalledTimes(4); + expect(content.queryByText("Success")).not.toBe(null); +}); + +it("save Project after adding, modifying and deleting resources", async () => { + console.log("save Project after adding, modifying and deleting resource.........."); + let content; + await act(async () => { + content = render(<Router><ProjectEdit match={{params:{id: "OSR-11"}}} location={{}} /></Router>); + }); + + // expect(content.baseElement).toBe(null); + expect(content.queryByText("Project - Edit")).not.toBe(null); + expect(content.queryByTestId("name").value).toBe('OSR-11'); + + // Adding new resource + const addResourceInput = content.getAllByRole("listbox")[3].children[0] ; + fireEvent.click(addResourceInput); + // After selecting New Resource + expect(content.queryAllByText('Add Resources').length).toBe(1); + expect(content.queryAllByText('LOFAR Support hours').length).toBe(3); + const addResourceBtn = content.queryByTestId('add_res_btn'); + fireEvent.click(addResourceBtn); + expect(content.queryAllByText('Add Resources').length).toBe(2); + expect(content.queryByPlaceholderText('LOFAR Support hours')).not.toBe(null); + const lofarSupHrsInput = content.queryByPlaceholderText('LOFAR Support hours'); + fireEvent.blur(lofarSupHrsInput, { target: { value: 100 } }); + expect(lofarSupHrsInput.value).toBe('100 '); + + // Editing existing resource + const lofarObsTimeInput = content.queryByPlaceholderText('LOFAR Observing Time'); + fireEvent.blur(lofarObsTimeInput, { target: { value: 10 } }); + expect(lofarObsTimeInput.value).toBe('10 Hours'); + + // Deleting existing resource + const removeResourceBtn = content.queryByTestId('Support hours-btn'); + fireEvent.click(removeResourceBtn); + + expect(content.queryByTestId('save-btn').hasAttribute("disabled")).toBeFalsy(); + + await act(async () => { + fireEvent.click(content.queryByTestId('save-btn')); + }); + + // After saving project, Success dialog should be displayed + expect(updateProjectSpy).toHaveBeenCalledTimes(1); + expect(savePQSpy).toHaveBeenCalledTimes(1); + expect(updatePQSpy).toHaveBeenCalledTimes(1); + expect(deletePQSpy).toHaveBeenCalledTimes(1); + expect(content.queryByText("Success")).not.toBe(null); +}); \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/index.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/index.js new file mode 100644 index 0000000000000000000000000000000000000000..7572b27b8d71777ba3a584f56a054438cdd4b13e --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/index.js @@ -0,0 +1,6 @@ +import {ProjectCreate} from './create'; +import {ProjectView} from './view'; +import {ProjectEdit} from './edit'; +import {ProjectList} from './list'; + +export {ProjectList, ProjectCreate, ProjectView, ProjectEdit} ; diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/list.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/list.js new file mode 100644 index 0000000000000000000000000000000000000000..af3869b13dd7c31251f4dec33f350ee3f5d59100 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/list.js @@ -0,0 +1,128 @@ +import React, {Component} from 'react'; +import ProjectService from '../../services/project.service'; +import ViewTable from '../../components/ViewTable'; +import { Link } from 'react-router-dom/cjs/react-router-dom.min'; +import AppLoader from '../../layout/components/AppLoader'; + +export class ProjectList extends Component{ + constructor(props){ + super(props) + this.state = { + projectlist: [], + paths: [{ + "View": "/project/view", + }], + defaultcolumns: [ { + "name":"Name / Project Code", + "status":"Status" , + "project_category_value":"Category of Project", + "description":"Description" + }], + optionalcolumns: [{ + "priority_rank":"Project Priority", + "trigger_priority":"Trigger Priority", + "period_category_value":"Category of Period", + "cycles_ids":"Cycles", + "can_trigger": "Trigger Allowed", + "LOFAR Observing Time":"Observing time (Hrs)", + "LOFAR Observing Time prio A":"Observing time prio A (Hrs)", + "LOFAR Observing Time prio B":"Observing time prio B (Hrs)", + "CEP Processing Time":"Processing time (Hrs)", + "LTA Storage":"LTA storage (TB)", + "Number of triggers":"Number of Triggers", + "actionpath":"actionpath", + }], + columnclassname: [{ + "Observing time (Hrs)":"filter-input-50", + "Observing time prio A (Hrs)":"filter-input-50", + "Observing time prio B (Hrs)":"filter-input-50", + "Processing time (Hrs)":"filter-input-50", + "LTA storage (TB)":"filter-input-50", + "Status":"filter-input-50", + "Trigger Allowed":"filter-input-50", + "Number of Triggers":"filter-input-50", + "Project Priority":"filter-input-50", + "Trigger Priority":"filter-input-50", + "Category of Period":"filter-input-50", + "Cycles":"filter-input-100", + }], + isprocessed: false, + isLoading: true + } + } + + componentDidMount(){ + // for Unit test, Table data + this.unittestDataProvider(); + ProjectService.getProjectList() + .then(async (projects) => { + await ProjectService.getUpdatedProjectQuota(projects) + .then( async projlist => { + this.setState({ + projectlist: projlist, + isprocessed: true, + isLoading: false + }) + }) + }); + } + + render(){ + return( + <> + <div className="p-grid"> + <div className="p-col-10 p-lg-10 p-md-10"> + <h2>Project - List </h2> + </div> + <div className="p-col-2 p-lg-2 p-md-2"> + <Link to={{ pathname: '/project/create'}} title="Add New Project" style={{float: "right"}}> + <i className="fa fa-plus-square" style={{marginTop: "10px"}}></i> + </Link> + </div> + </div> + {this.state.isLoading? <AppLoader /> : this.state.isprocessed && + <ViewTable + data={this.state.projectlist} + defaultcolumns={this.state.defaultcolumns} + optionalcolumns={this.state.optionalcolumns} + columnclassname={this.state.columnclassname} + showaction="true" + paths={this.state.paths} + keyaccessor="name" + unittest={this.state.unittest} + /> + } + </> + ) + } + + // Set table data for Unit test + unittestDataProvider(){ + if(this.props.testdata){ + this.setState({ + projectlist: [{can_trigger: true, + created_at: "2020-07-27T01:29:57.348499", + cycles: ["http://localhost:3000/api/cycle/Cycle%204/"], + cycles_ids: ["Cycle 4"], + description: "string", + expert: true, + filler: true, + name: "Lofar-TMSS-Commissioning", + observing_time: "155852.10", + priority_rank: 10, + private_data: true, + project_quota: ["http://localhost:3000/api/project_quota/6/", "http://localhost:3000/api/project_quota/7/"], + project_quota_ids: [6, 7], + tags: ["Lofar TMSS Project"], + trigger_priority: 20, + triggers_allowed: "56", + updated_at: "2020-07-27T01:29:57.348522", + url: "http://localhost:3000/api/project/Lofar-TMSS-Commissioning/" + }], + isprocessed: true, + unittest: true, + }) + } + } +} + \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/list.test.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/list.test.js new file mode 100644 index 0000000000000000000000000000000000000000..323c91575f39c9aa00a5a4bd6ab43b7e54c086b1 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/list.test.js @@ -0,0 +1,39 @@ +import React from 'react'; +import ReactDOM, {unmountComponentAtNode} from 'react-dom'; +import { BrowserRouter as Router } from 'react-router-dom'; +import { act } from 'react-dom/test-utils'; +import { render} from '@testing-library/react'; +import { ProjectList} from './index'; + +let container = null; +beforeEach(() =>{ + container = document.createElement("div"); + document.body.appendChild(container); +}); + +afterEach(() =>{ + unmountComponentAtNode(container); + container.remove(); + container = null; +}) + +it("renders without crashing", () =>{ + act(() =>{ + ReactDOM.render(<Router><ProjectList /> </Router>, container); + }) +}) + +// Do check the label appear or not +it('renders Project - List Page in View Table', () => { + const content = render(<ProjectList />); + const element = content.queryByText("Project - List"); + expect(element).toBeInTheDocument() ; + }); + +// do check does the data loaded into DB or not +it('renders Project - List Data Load in View Table', () => { + const content = render(<Router><ProjectList testdata= {true} /> </Router>, container); + const element = content.queryByTestId('viewtable'); + expect(element).toBeInTheDocument(); +}); + \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/view.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/view.js new file mode 100644 index 0000000000000000000000000000000000000000..1d92160e99852a538690878a470ef9633117efbc --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/view.js @@ -0,0 +1,157 @@ +import React, {Component} from 'react'; +import {Link, Redirect} from 'react-router-dom' +import moment from 'moment'; +import _ from 'lodash'; + +import { Chips } from 'primereact/chips'; + +import ResourceDisplayList from './ResourceDisplayList'; + +import AppLoader from '../../layout/components/AppLoader'; +import ProjectService from '../../services/project.service'; +import UnitConverter from '../../utils/unit.converter'; + +/** + * Component to view the details of a project + */ +export class ProjectView extends Component { + DATE_FORMAT = 'YYYY-MMM-DD HH:mm:ss'; + constructor(props) { + super(props); + this.state = { + isLoading: true, + }; + console.log(this.props); + if (this.props.match.params.id) { + this.state.projectId = this.props.match.params.id; + } else if (this.props.location.state && this.props.location.state.id) { + this.state.projectId = this.props.location.state.id; + } + console.log(this.state.projectId); + this.state.redirect = this.state.projectId?"":'/project' // If no project id is passed, redirect to Project list page + this.resourceUnitMap = UnitConverter.resourceUnitMap; // Resource unit conversion factor and constraints + } + + componentDidMount() { + const projectId = this.state.projectId; + console.log(projectId); + if (projectId) { + console.log(projectId); + this.getProjectDetails(projectId); + } else { + this.setState({redirect: "/not-found"}); + } + } + + /** + * To get the project details from the backend using the service + * + */ + async getProjectDetails() { + let project = await ProjectService.getProjectDetails(this.state.projectId); + let projectQuota = []; + let resources = []; + + if (project) { + // If resources are allocated for the project quota fetch the resources master from the API + if (project.quota) { + resources = await ProjectService.getResources(); + } + + // For every project quota, get the resource type & assign to the resource variable of the quota object + for (const id of project.quota_ids) { + let quota = await ProjectService.getProjectQuota(id); + let resource = _.find(resources, ['name', quota.resource_type_id]); + quota.resource = resource; + projectQuota.push(quota); + }; + this.setState({project: project, projectQuota: projectQuota, isLoading: false}); + } else { + this.setState({redirect: "../../not-found"}) + } + + } + + render() { + if (this.state.redirect) { + return <Redirect to={ {pathname: this.state.redirect} }></Redirect> + } + + return ( + <React.Fragment> + <div className="p-grid"> + <div className="p-col-10 p-lg-10 p-md-10"> + <h2>Project - Details </h2> + </div> + { this.state.project && + <div className="p-col-2 p-lg-2 p-md-2"> + <Link to={{ pathname: `/project`}} title="Close View" style={{float: "right"}}> + <i className="fa fa-times" style={{marginTop: "10px", marginLeft: "5px"}}></i> + </Link> + <Link to={{ pathname: `/project/edit/${this.state.project.name}`, state: {id: this.state.project?this.state.project.name:''}}} title="Edit Project" + style={{float: "right"}}> + <i className="fa fa-edit" style={{marginTop: "10px"}}></i> + </Link> + </div> + } + </div> + { this.state.isLoading && <AppLoader /> } + { this.state.project && + <React.Fragment> + <div className="main-content"> + <div className="p-grid"> + <label className="col-lg-2 col-md-2 col-sm-12">Name</label> + <span className="col-lg-4 col-md-4 col-sm-12">{this.state.project.name}</span> + <label className="col-lg-2 col-md-2 col-sm-12">Description</label> + <span className="col-lg-4 col-md-4 col-sm-12">{this.state.project.description}</span> + </div> + <div className="p-grid"> + <label className="col-lg-2 col-md-2 col-sm-12">Created At</label> + <span className="col-lg-4 col-md-4 col-sm-12">{moment.utc(this.state.project.created_at).format(this.DATE_FORMAT)}</span> + <label className="col-lg-2 col-md-2 col-sm-12">Updated At</label> + <span className="col-lg-4 col-md-4 col-sm-12">{moment.utc(this.state.project.updated_at).format(this.DATE_FORMAT)}</span> + </div> + <div className="p-grid"> + <label className="col-lg-2 col-md-2 col-sm-12">Trigger Priority</label> + <span className="col-lg-4 col-md-4 col-sm-12">{this.state.project.trigger_priority}</span> + <label className="col-lg-2 col-md-2 col-sm-12">Allows Trigger Submission</label> + <span className="col-lg-4 col-md-4 col-sm-12"><i className={this.state.project.can_trigger?'fa fa-check-square':'fa fa-times'}></i></span> + </div> + <div className="p-grid"> + <label className="col-lg-2 col-md-2 col-sm-12">Project Category</label> + <span className="col-lg-4 col-md-4 col-sm-12">{this.state.project.project_category_value}</span> + <label className="col-lg-2 col-md-2 col-sm-12">Period Category</label> + <span className="col-lg-4 col-md-4 col-sm-12">{this.state.project.period_category_value}</span> + </div> + <div className="p-grid"> + <label className="col-lg-2 col-md-2 col-sm-12">Cycles</label> + <Chips className="col-lg-4 col-md-4 col-sm-12 chips-readonly" disabled value={this.state.project.cycles_ids}></Chips> + <label className="col-lg-2 col-md-2 col-sm-12">Project Rank</label> + <span className="col-lg-4 col-md-4 col-sm-12">{this.state.project.priority_rank}</span> + </div> + <div className="p-fluid"> + <div className="p-field p-grid"> + <div className="col-lg-3 col-md-3 col-sm-12"> + <h5 data-testid="resource_alloc">Resource Allocations</h5> + </div> + </div> + </div> + {this.state.projectQuota.length===0 && + <div className="p-field p-grid"> + <div className="col-lg-12 col-md-12 col-sm-12"> + <span>Reosurces not yet allocated. + <Link to={{ pathname: `/project/edit/${this.state.project.name}`, state: {id: this.state.project?this.state.project.name:''}}} title="Edit Project" > Click</Link> to add. + </span> + </div> + </div> + } + <div className="p-field p-grid resource-input-grid"> + <ResourceDisplayList projectQuota={this.state.projectQuota} unitMap={this.resourceUnitMap} /> + </div> + </div> + </React.Fragment> + } + </React.Fragment> + ); + } +} \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/view.test.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/view.test.js new file mode 100644 index 0000000000000000000000000000000000000000..aaf9327d0308d4076806ba99897dc7a345df0e23 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/view.test.js @@ -0,0 +1,81 @@ +import React from 'react'; +import { BrowserRouter as Router } from 'react-router-dom'; +import { act } from "react-dom/test-utils"; +import { render, cleanup, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; +import _ from 'lodash'; + +import {ProjectView} from './view'; +import ProjectService from '../../services/project.service'; + +import ProjectServiceMock from '../../__mocks__/project.service.data'; + + +let projectDetailsSpy, resourcesSpy, projectQuotaSpy; + +beforeEach(() => { + setMockSpy(); +}); + +afterEach(() => { + // cleanup on exiting + clearMockSpy(); + cleanup(); +}); + +/** + * To set mock spy for Services that have API calls to the back end to fetch data + */ +const setMockSpy = (() => { + projectDetailsSpy = jest.spyOn(ProjectService, 'getProjectDetails'); + projectDetailsSpy.mockImplementation((id) => { + return Promise.resolve(_.find(ProjectServiceMock.project, {name: id}))}); + resourcesSpy = jest.spyOn(ProjectService, 'getResources'); + resourcesSpy.mockImplementation(() => { + return Promise.resolve(ProjectServiceMock.resources); + }); + projectQuotaSpy = jest.spyOn(ProjectService, 'getProjectQuota'); + projectQuotaSpy.mockImplementation((id) => { + return Promise.resolve(_.find(ProjectServiceMock.projectQuota, {id: id})); + }); +}); + +const clearMockSpy = (() => { + projectDetailsSpy.mockRestore(); + resourcesSpy.mockRestore(); + projectQuotaSpy.mockRestore(); +}); + +it("renders Project details if found", async () => { + console.log("renders Project details if found.........."); + let content; + await act(async () => { + content = render(<Router><ProjectView match={{params:{id: "OSR-11"}}} location={{}} /></Router>); + }); + + expect(content.queryByText("Project - Details")).not.toBe(null); + expect(content.queryAllByText("OSR-11").length).toBe(2); + expect(content.queryByText("990")).not.toBe(null); + expect(content.queryByText("Regular")).not.toBe(null); + expect(content.queryByText("Single Cycle")).not.toBe(null); + expect(content.queryByText("Resource Allocations")).not.toBe(null); + expect(content.queryByText("10 Hours")).not.toBe(null); + expect(content.queryByText("20 Hours")).not.toBe(null); + expect(content.queryByText("30 Hours")).not.toBe(null); + expect(content.queryByText("40 Hours")).not.toBe(null); + expect(content.queryByText("50 Hours")).not.toBe(null); + expect(content.queryByText("6 TB")).not.toBe(null); + expect(content.queryByText("7 Numbers")).not.toBe(null); + expect(content.queryByText("8")).not.toBe(null); + +}); + +it("renders nothing if no project details found", async () => { + console.log("renders nothing if no project details found.........."); + let content; + await act(async () => { + content = render(<Router><ProjectView match={{params:{id: "OSR-12"}}} location={{}} /></Router>); + }); + + expect(content.queryByText("Project - Details")).toBe(null); +}); \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/SchedulingUnitList.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/SchedulingUnitList.js new file mode 100644 index 0000000000000000000000000000000000000000..7c2913af9f4a8da519c2044d00ad9f69ee550b42 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/SchedulingUnitList.js @@ -0,0 +1,90 @@ +import React, { Component } from 'react' +import 'primeflex/primeflex.css'; + +import AppLoader from "./../../layout/components/AppLoader"; +import ViewTable from './../../components/ViewTable'; + +import ScheduleService from '../../services/schedule.service'; + + +class SchedulingUnitList extends Component{ + + constructor(props){ + super(props) + this.state = { + scheduleunit: [], + paths: [{ + "View": "/schedulingunit/view", + }], + isLoading: true, + defaultcolumns: [ { + "name":"Name", + "description":"Description", + "created_at":"Created Date", + "updated_at":"Updated Date", + "requirements_template_id": "Template", + "start_time":"Start Time", + "stop_time":"End time", + "duration":"Duration" + }], + optionalcolumns: [{ + "actionpath":"actionpath", + }], + columnclassname: [{ + "Template":"filter-input-50", + "Duration":"filter-input-50", + }] + } + } + + componentDidMount(){ + ScheduleService.getSchedulingUnitDraft().then(scheduleunit =>{ + console.log(scheduleunit) + var scheduleunits = scheduleunit.data.results; + + for( const scheduleunit of scheduleunits){ + scheduleunit['actionpath']='/schedulingunit/view' + } + this.setState({ + scheduleunit : scheduleunit.data ,isLoading: false + }); + }) + } + + render(){ + if (this.state.isLoading) { + return <AppLoader/> + } + return( + <> + + { + + /* + * Call View table to show table data, the parameters are, + data - Pass API data + defaultcolumns - These columns will be populate by default in table with header mentioned + optionalcolumns - These columns will not appear in the table by default, but user can toggle the columns using toggle menu + showaction - {true/false} -> to show the action column + keyaccessor - This is id column for Action item + paths - specify the path for navigation - Table will set "id" value for each row in action button + + */} + {this.state.scheduleunit.results && + <ViewTable + data={this.state.scheduleunit.results} + defaultcolumns={this.state.defaultcolumns} + optionalcolumns={this.state.optionalcolumns} + columnclassname={this.state.columnclassname} + showaction="true" + keyaccessor="id" + paths={this.state.paths} + unittest={this.state.unittest} + /> + } + </> + ) + } +} + +export default SchedulingUnitList diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/ViewSchedulingUnit.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/ViewSchedulingUnit.js new file mode 100644 index 0000000000000000000000000000000000000000..4b3c24179fb7025275c45512fb753e32fb5822a5 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/ViewSchedulingUnit.js @@ -0,0 +1,156 @@ +import React, { Component } from 'react' +import {Link} from 'react-router-dom' +import 'primeflex/primeflex.css'; +import { Chips } from 'primereact/chips'; + +import AppLoader from "./../../layout/components/AppLoader"; + +import ViewTable from './../../components/ViewTable'; +import ScheduleService from '../../services/schedule.service'; +import moment from 'moment'; + +class ViewSchedulingUnit extends Component{ + constructor(props){ + super(props) + this.state = { + scheduleunit: null, + schedule_unit_task: [], + isLoading: true, + paths: [{ + "View": "/task", + }], + + defaultcolumns: [ { + "tasktype":"Task Type", + "id":"ID", + "name":"Name", + "description":"Description", + "created_at":"Created at", + "updated_at":"Updated at", + "do_cancel":"Cancelled", + "start_time":"Start Time", + "stop_time":"End Time", + "duration":"Duration", + }], + optionalcolumns: [{ + "relative_start_time":"Relative Start Time", + "relative_stop_time":"Relative End Time", + "tags":"Tags", + "blueprint_draft":"BluePrint / Task Draft link", + "url":"URL", + "actionpath":"actionpath", + }], + + columnclassname: [{ + "Task Type":"filter-input-100", + "ID":"filter-input-50", + "Cancelled":"filter-input-50", + "Duration":"filter-input-50", + "Template ID":"filter-input-50", + "BluePrint / Task Draft link":"filter-input-100", + }] + } + } + + componentDidMount(){ + let schedule_id = this.props.location.state.id + if (schedule_id) { + ScheduleService.getSchedulingUnitDraftById(schedule_id) + .then(scheduleunit =>{ + ScheduleService.getScheduleTasksBySchedulingUnitId(scheduleunit.data.id) + .then(tasks =>{ + this.setState({ + scheduleunit : scheduleunit.data, + schedule_unit_task : tasks, + isLoading: false + }); + }); + }) + } + } + + render(){ + return( + <> + <div className="p-grid"> + <div className="p-col-10"> + <h2>Scheduling Unit - Details </h2> + </div> + <div className="p-col-2"> + <Link to={{ pathname: '/schedulingunit'}} title="Close" + style={{float:'right'}}> + <i className="fa fa-times" style={{marginTop: "10px", marginLeft: '5px'}}></i> + </Link> + <Link to={{ pathname: '/schedulingunit/edit', state: {id: this.state.scheduleunit?this.state.scheduleunit.id:''}}} title="Edit" + style={{float:'right'}}> + <i className="fa fa-edit" style={{marginTop: "10px"}}></i> + </Link> + </div> + </div> + { this.state.isLoading ? <AppLoader/> :this.state.scheduleunit && + <> + <div className="p-grid"> + <label className="p-col-2">Name</label> + <span className="p-col-4">{this.state.scheduleunit.name}</span> + <label className="p-col-2">Description</label> + <span className="p-col-4">{this.state.scheduleunit.description}</span> + </div> + <div className="p-grid"> + <label className="p-col-2">Created At</label> + <span className="p-col-4">{moment(this.state.scheduleunit.created_at).format("YYYY-MMM-DD HH:mm:SS")}</span> + <label className="p-col-2">Updated At</label> + <span className="p-col-4">{moment(this.state.scheduleunit.updated_at).format("YYYY-MMM-DD HH:mm:SS")}</span> + </div> + <div className="p-grid"> + <label className="p-col-2">Start Time</label> + <span className="p-col-4">{this.state.scheduleunit.start_time && moment(this.state.scheduleunit.start_time).format("YYYY-MMM-DD HH:mm:SS")}</span> + <label className="p-col-2">End Time</label> + <span className="p-col-4">{this.state.scheduleunit.stop_time && moment(this.state.scheduleunit.stop_time).format("YYYY-MMM-DD HH:mm:SS")}</span> + </div> + <div className="p-grid"> + <label className="p-col-2">Template ID</label> + <span className="p-col-4">{this.state.scheduleunit.requirements_template_id}</span> + <label className="p-col-2">Scheduling set</label> + <span className="p-col-4">{this.state.scheduleunit.scheduling_set_id}</span> + </div> + <div className="p-grid"> + <label className="p-col-2">Duration</label> + <span className="p-col-4">{this.state.scheduleunit.duration}</span> + <label className="p-col-2">Tags</label> + <Chips className="p-col-4 chips-readonly" disabled value={this.state.scheduleunit.tags}></Chips> + <span className="p-col-4">{this.state.scheduleunit.tags}</span> + </div> + </> + + } + <div> + <h3>Tasks Details</h3> + </div> + {/* + * Call View table to show table data, the parameters are, + data - Pass API data + defaultcolumns - These columns will be populate by default in table with header mentioned + optionalcolumns - These columns will not appear in the table by default, but user can toggle the columns using toggle menu + showaction - {true/false} -> to show the action column + keyaccessor - This is id column for Action item + paths - specify the path for navigation - Table will set "id" value for each row in action button + + */} + {this.state.isLoading ? <AppLoader/> :this.state.schedule_unit_task.length>0 && + <ViewTable + data={this.state.schedule_unit_task} + defaultcolumns={this.state.defaultcolumns} + optionalcolumns={this.state.optionalcolumns} + columnclassname={this.state.columnclassname} + showaction="true" + keyaccessor="id" + paths={this.state.paths} + unittest={this.state.unittest} + /> + } + </> + ) + } +} + +export default ViewSchedulingUnit \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/index.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/index.js index 5ce2273ff25cb9371132f73e8c685415cb3ab5de..062317b2127f42ca3115e39fba3964121f83f3c4 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/index.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/index.js @@ -1,10 +1,23 @@ import React, {Component} from 'react'; +import SchedulingUnitList from './SchedulingUnitList'; export class Scheduling extends Component { - + constructor(props){ + super(props) + this.state = { + scheduleunit: [], + schedule_unit_task: [] , + isLoading:false + } + } + render() { - return ( - <h1>Scheduling Units</h1> + return ( + <> + <h2>Scheduling Unit - List</h2> + {this.state.scheduleunit && + <SchedulingUnitList /> } + </> ); } } diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/edit.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/edit.js new file mode 100644 index 0000000000000000000000000000000000000000..90ad6359847a5a18718b005ddb73608d9188a08a --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/edit.js @@ -0,0 +1,279 @@ +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 { Button } from 'primereact/button'; + +import Jeditor from '../../components/JSONEditor/JEditor'; + +import TaskService from '../../services/task.service'; +import AppLoader from "./../../layout/components/AppLoader"; + + +export class TaskEdit extends Component { + templateOutput = {}; // id: selectedTemplateId, output: values enetered in the editor form + + constructor(props) { + super(props); + this.state = { + task: { + name: "", + created_at: null, + updated_at: null, + tags:[], + do_cancel: false + }, + redirect: null, + taskTemplates:[], + validEditor: false, + validForm: false, + errors: {}, + isLoading: true + }; + this.formRules = { + name: {required: true, message: "Name can not be empty"}, + description: {required: true, message: "Description can not be empty"} + }; + this.setEditorOutput = this.setEditorOutput.bind(this); + this.setTaskParams = this.setTaskParams.bind(this); + this.changeTaskTemplate = this.changeTaskTemplate.bind(this); + this.setEditorFunction = this.setEditorFunction.bind(this); + this.validateForm = this.validateForm.bind(this); + this.saveTask = this.saveTask.bind(this); + this.cancelEdit = this.cancelEdit.bind(this); + } + + /** + * This is the callback method to be passed to the JSON editor. + * JEditor will call this function when there is change in the editor. + * @param {Object} jsonOutput + * @param {Array} errors + */ + setEditorOutput(jsonOutput, errors) { + this.templateOutput[this.state.task.specifications_template_id] = jsonOutput; + if (errors.length === 0 && !this.state.validEditor) { + this.setState({validEditor: true, validForm: this.validateForm()}); + } else if (errors.length > 0 && this.state.validEditor) { + this.setState({validEditor: false, validForm: this.validateForm()}); + } + } + + /** + * Function called when there is change in the task parameters. + * @param {String} key + * @param {*} value + */ + setTaskParams(key, value) { + let task = this.state.task; + task[key] = value; + this.setState({task: task, validForm: this.validateForm()}); + } + + /** + * JEditor's function that to be called when parent wants to trigger change in the JSON Editor + * @param {Function} editorFunction + */ + setEditorFunction(editorFunction) { + this.setState({editorFunction: editorFunction}); + } + + /** + * Function to be called when the template schema is changed + * @param {Number} templateId + */ + changeTaskTemplate(templateId) { + + const template = _.find(this.state.taskTemplates, {'id': templateId}); + let task = this.state.task; + task.specifications_template_id = templateId; + + task.specifications_template = template.url; + this.setState({taskSchema: null}); + this.setState({task: task, taskSchema: template.schema}); + + this.state.editorFunction(); + } + + /** + * Function to validate the form excluding the JSON Editor values + */ + validateForm() { + let validForm = false; + let errors = {}; + for (const fieldName in this.formRules) { + const rule = this.formRules[fieldName]; + const fieldValue = this.state.task[fieldName]; + if (rule.required) { + if (!fieldValue) { + errors[fieldName] = rule.message?rule.message:`${fieldName} is required`; + } + } + } + this.setState({errors: errors}); + if (this.state.task.name && this.state.task.description) { + validForm = true; + } + return validForm; + } + + /** + * Function to call the servie and pass the values to save + */ + saveTask() { + let task = this.state.task; + task.specifications_doc = this.templateOutput[task.specifications_template_id]; + TaskService.updateTask("draft", task) + .then( (taskDraft) => { + if (taskDraft) { + this.setState({redirect: '/task/view'}); + } + }); + } + + cancelEdit() { + this.setState({redirect: '/task/view'}); + } + + componentDidMount() { + this.setState({ isLoading: true }); + TaskService.getTaskTemplates() + .then((templates) => { + this.setState({taskTemplates: templates}); + }); + TaskService.getTaskDetails("draft", this.props.taskId?this.props.taskId:this.props.location.state.taskId) + .then((task) => { + if (task) { + TaskService.getSchedulingUnit("draft", task.scheduling_unit_draft_id) + .then((schedulingUnit) => { + this.setState({schedulingUnit: schedulingUnit,isLoading: false}); + }); + + this.templateOutput[task.specifications_template_id] = task.specifications_doc; + TaskService.getTaskTemplate(task.specifications_template_id) + .then((taskTemplate) => { + this.setState({task: task, taskSchema: taskTemplate.schema, isLoading: false}); + }); + } else { + this.setState({redirect: "/not-found"}); + } + }); + } + + render() { + + if (this.state.redirect) { + return <Redirect to={ {pathname: this.state.redirect, + state: {id: this.state.task.id}} }></Redirect> + } + const { isLoading } = this.state; + + const taskSchema = this.state.taskSchema; + + let jeditor = null; + if (this.state.taskSchema) { + + jeditor = React.createElement(Jeditor, {title: "Specification", + schema: taskSchema, + //initValue: this.state.templateOutput[this.state.task.specifications_template_id], + initValue: this.templateOutput[this.state.task.specifications_template_id], + callback: this.setEditorOutput, + parentFunction: this.setEditorFunction + }); + } + + return ( + <React.Fragment> + <div className="p-grid"> + <div className="p-col-10 p-lg-10 p-md-10"> + <h2>Task - Edit</h2> + </div> + <div className="p-col-2 p-lg-2 p-md-2"> + <Link to={{ pathname: `/task/view/draft/${this.state.task?this.state.task.id:''}`}} title="Close Edit" + style={{float: "right"}} > + <i className="fa fa-window-close" style={{marginTop: "10px"}}></i> + </Link> + </div> + </div> + + {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)}/> + <label className="error"> + {this.state.errors.name ? this.state.errors.name : ""} + </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} value={this.state.task.description} onChange={(e) => this.setTaskParams('description', e.target.value)}/> + <label className="error"> + {this.state.errors.description ? this.state.errors.description : ""} + </label> + </div> + </div> + {/* <div className="p-field p-grid"> + <label htmlFor="createdAt" className="col-lg-2 col-md-2 col-sm-12">Created At</label> + <div className="col-lg-4 col-md-4 col-sm-12"> + <Calendar showTime={true} hourFormat="24" value={created_at} onChange={(e) => this.setState({date2: e.value})}></Calendar> + </div> + <label htmlFor="updatedAt" className="col-lg-2 col-md-2 col-sm-12">Updated At</label> + <div className="col-lg-4 col-md-4 col-sm-12"> + <Calendar showTime={true} hourFormat="24" value={updated_at} onChange={(e) => this.setState({date2: e.value})}></Calendar> + </div> + </div> + */} + <div className="p-field p-grid"> + <label htmlFor="tags" className="col-lg-2 col-md-2 col-sm-12">Tags</label> + <div className="col-lg-4 col-md-4 col-sm-12"> + <Chips value={this.state.task.tags?this.state.task.tags:[]} onChange={(e) => this.setTaskParams('tags', e.value)}></Chips> + </div> + {/* <label htmlFor="doCancel" className="col-lg-2 col-md-2 col-sm-12">Do Cancel</label> + <div className="col-lg-4 col-md-4 col-sm-12"> + <Checkbox onChange={e => this.setTaskParams('do_cancel', e.checked)} checked={this.state.task.do_cancel}></Checkbox> + </div> */} + {this.state.schedulingUnit && + <> + <label className="col-lg-2 col-md-2 col-sm-12">Scheduling Unit</label> + <Link className="col-lg-4 col-md-4 col-sm-12" to={ { pathname:'/schedulingunit/view', state: {id: this.state.schedulingUnit.id}}}>{this.state.schedulingUnit?this.state.schedulingUnit.name:''}</Link> + </> + } + </div> + <div className="p-field p-grid"> + <label htmlFor="tags" className="col-lg-2 col-md-2 col-sm-12">Template</label> + <div className="col-lg-4 col-md-4 col-sm-12"> + <Dropdown optionLabel="name" optionValue="id" + value={this.state.task.specifications_template_id} + options={this.state.taskTemplates} + onChange={(e) => {this.changeTaskTemplate(e.value)}} + placeholder="Select Task Template"/> + </div> + </div> + </div> + </div> + } + <div className="p-fluid"> + <div className="p-grid"><div className="p-col-12"> + {this.state.taskSchema?jeditor:""} + </div> + </div> + </div> + + <div className="p-grid p-justify-start"> + <div className="p-col-1"> + <Button label="Save" className="p-button-primary" icon="pi pi-check" onClick={this.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} /> + </div> + </div> + </React.Fragment> + ); + } +} \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/index.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/index.js new file mode 100644 index 0000000000000000000000000000000000000000..d7e2c03a6532dbb07064d9ca603f5796bbedd986 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/index.js @@ -0,0 +1,4 @@ +import {TaskEdit} from './edit'; +import {TaskView} from './view'; + +export {TaskEdit, TaskView} ; diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/view.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/view.js new file mode 100644 index 0000000000000000000000000000000000000000..a8d612e25fed0eb865b57cd798a5ad8134e0d3a3 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/view.js @@ -0,0 +1,222 @@ +import React, {Component} from 'react'; +import {Link, Redirect} from 'react-router-dom' +import moment from 'moment'; + +import Jeditor from '../../components/JSONEditor/JEditor'; + +import TaskService from '../../services/task.service'; +import { Chips } from 'primereact/chips'; +import AppLoader from '../../layout/components/AppLoader'; + +export class TaskView extends Component { + DATE_FORMAT = 'YYYY-MMM-DD HH:mm:ss'; + constructor(props) { + super(props); + this.state = { + isLoading: true + }; + this.setEditorFunction = this.setEditorFunction.bind(this); + if (this.props.match.params.id) { + this.state.taskId = this.props.match.params.id; + } + if (this.props.match.params.type) { + this.state.taskType = this.props.match.params.type; + } + + } + + // static getDerivedStateFromProps(nextProps, prevstate){ + // console.log("DERIVED STATE FROM PROPS"); + // console.log(nextProps); + // console.log(prevstate); + // if (prevstate.task && nextProps.match.params && + // (nextProps.match.params.id === prevstate.task.id || + // nextProps.match.params.type === prevstate.taskType)) { + // return {taskId: prevstate.task.id, taskType: prevstate.taskType} + // } + // console.log("RETURNS NULL"); + // return null; + // } + + componentDidUpdate(prevProps, prevState) { + if (this.state.task && this.props.match.params && + (this.state.taskId !== this.props.match.params.id || + this.state.taskType !== this.props.match.params.type)) { + this.getTaskDetails(this.props.match.params.id, this.props.match.params.type); + } + } + + componentDidMount() { + // const taskId = this.props.location.state?this.props.location.state.id:this.state.taskId; + // let taskType = this.props.location.state?this.props.location.state.type:this.state.taskType; + // taskType = taskType?taskType:'draft'; + let {taskId, taskType} = this.state; + taskId = taskId?taskId:this.props.location.state.id; + taskType = taskType?taskType:this.props.location.state.type; + + if (taskId && taskType) { + this.getTaskDetails(taskId, taskType); + } else { + this.setState({redirect: "/not-found"}); + } + } + + /** + * JEditor's function that to be called when parent wants to trigger change in the JSON Editor + * @param {Function} editorFunction + */ + setEditorFunction(editorFunction) { + this.setState({editorFunction: editorFunction}); + } + + /** + * To get the task details from the backend using the service + * @param {number} taskId + */ + getTaskDetails(taskId, taskType) { + if (taskId) { + taskType = taskType?taskType:'draft'; + TaskService.getTaskDetails(taskType, taskId) + .then((task) => { + if (task) { + TaskService.getSchedulingUnit(taskType, (taskType==='draft'?task.scheduling_unit_draft_id:task.scheduling_unit_blueprint_id)) + .then((schedulingUnit) => { + this.setState({schedulingUnit: schedulingUnit}); + }); + TaskService.getTaskTemplate(task.specifications_template_id) + .then((taskTemplate) => { + if (this.state.editorFunction) { + this.state.editorFunction(); + } + this.setState({task: task, taskTemplate: taskTemplate, isLoading: false, taskId: taskId, taskType: taskType}); + }); + + } else { + this.setState({redirect: "/not-found"}); + } + }); + } + } + + render() { + if (this.state.redirect) { + return <Redirect to={ {pathname: this.state.redirect} }></Redirect> + } + let jeditor = null + if (this.state.taskTemplate) { + jeditor = React.createElement(Jeditor, {title: "Specification", + schema: this.state.taskTemplate.schema, + initValue: this.state.task.specifications_doc, + disabled: true, + // callback: this.setEditorOutput, + parentFunction: this.setEditorFunction + }); + } + + // Child component to render predecessors and successors list + const TaskRelationList = ({ list }) => ( + <ul className="task-list"> + {list && list.map(item => ( + <li key={item.id}> + {/* <Link to={ { pathname:'/task/view', state: {id: item.id, type: item.draft?'blueprint':'draft'}}}>{item.name}</Link> */} + <Link to={ { pathname:`/task/view/${item.draft?'blueprint':'draft'}/${item.id}`}}>{item.name}</Link> + </li> + ))} + </ul> + ); + return ( + <React.Fragment> + <div className="p-grid"> + <div className="p-col-10 p-lg-10 p-md-10"> + <h2>Task - Details </h2> + </div> + <div className="p-col-2 p-lg-2 p-md-2"> + {this.state.taskType === 'draft' && + <div> + <Link to={{ pathname: '/task'}} tooltip="Edit Task" + style={{float: 'right'}}> + <i className="fa fa-times" style={{marginLeft:"5px", marginTop: "10px"}}></i> + </Link> + <Link to={{ pathname: '/task/edit', state: {taskId: this.state.task?this.state.task.id:''}}} tooltip="Edit Task" + style={{float: 'right'}}> + <i className="fa fa-edit" style={{marginTop: "10px"}}></i> + </Link> + </div> + } + {this.state.taskType === 'blueprint' && + <i className="fa fa-lock" style={{float:"right", marginTop: "10px"}}></i> + } + </div> + </div> + { this.state.isLoading? <AppLoader /> : this.state.task && + <React.Fragment> + <div className="main-content"> + <div className="p-grid"> + <label className="col-lg-2 col-md-2 col-sm-12">Name</label> + <span className="col-lg-4 col-md-4 col-sm-12">{this.state.task.name}</span> + <label className="col-lg-2 col-md-2 col-sm-12">Description</label> + <span className="col-lg-4 col-md-4 col-sm-12">{this.state.task.description}</span> + </div> + <div className="p-grid"> + <label className="col-lg-2 col-md-2 col-sm-12">Created At</label> + <span className="col-lg-4 col-md-4 col-sm-12">{moment.utc(this.state.task.created_at).format(this.DATE_FORMAT)}</span> + <label className="col-lg-2 col-md-2 col-sm-12">Updated At</label> + <span className="col-lg-4 col-md-4 col-sm-12">{moment.utc(this.state.task.updated_at).format(this.DATE_FORMAT)}</span> + </div> + <div className="p-grid"> + <label className="col-lg-2 col-md-2 col-sm-12">Copies</label> + <span className="col-lg-4 col-md-4 col-sm-12">{this.state.taskType==='draft'?this.state.task.copies:this.state.task.draftObject.copies}</span> + <label className="col-lg-2 col-md-2 col-sm-12">Copy Reason</label> + <span className="col-lg-4 col-md-4 col-sm-12">{this.state.taskType==='draft'?this.state.task.copy_reason_value:this.state.task.draftObject.copy_reason_value}</span> + </div> + <div className="p-grid"> + <label className="col-lg-2 col-md-2 col-sm-12">Start Time</label> + <span className="col-lg-4 col-md-4 col-sm-12">{this.state.task.start?moment.utc(this.state.task.start).format(this.DATE_FORMAT):""}</span> + <label className="col-lg-2 col-md-2 col-sm-12">End Time</label> + <span className="col-lg-4 col-md-4 col-sm-12">{this.state.task.end?moment.utc(this.state.task.end).format(this.DATE_FORMAT):""}</span> + </div> + <div className="p-grid"> + <label className="col-lg-2 col-md-2 col-sm-12">Tags</label> + <Chips className="col-lg-4 col-md-4 col-sm-12 chips-readonly" disabled value={this.state.task.tags}></Chips> + {this.state.schedulingUnit && + <> + <label className="col-lg-2 col-md-2 col-sm-12">Scheduling Unit</label> + <Link className="col-lg-4 col-md-4 col-sm-12" to={ { pathname:'/schedulingunit/view', state: {id: this.state.schedulingUnit.id}}}>{this.state.schedulingUnit?this.state.schedulingUnit.name:''}</Link> + </>} + </div> + <div className="p-grid"> + <label className="col-lg-2 col-md-2 col-sm-12">Predecessors</label> + <div className="col-lg-4 col-md-4 col-sm-12"> + <TaskRelationList list={this.state.task.predecessors} /> + </div> + <label className="col-lg-2 col-md-2 col-sm-12">Successors</label> + <div className="col-lg-4 col-md-4 col-sm-12"> + <TaskRelationList list={this.state.task.successors} /> + </div> + </div> + <div className="p-grid"> + <label className="col-lg-2 col-md-2 col-sm-12">Template</label> + <span className="col-lg-4 col-md-4 col-sm-12">{this.state.taskTemplate.name}</span> + <label className="col-lg-2 col-md-2 col-sm-12">{this.state.taskType==='draft'?'Blueprints':'Draft'}</label> + <div className="col-lg-4 col-md-4 col-sm-12"> + {this.state.taskType === 'draft' && + <TaskRelationList list={this.state.task.blueprints} /> + } + {this.state.taskType === 'blueprint' && + // <Link className="col-lg-4 col-md-4 col-sm-12" to={ { pathname:'/task/view', state: {id: this.state.task.draft_id, type: 'draft'}}}>{this.state.task.draftObject.name}</Link> + <Link className="col-lg-4 col-md-4 col-sm-12" to={ { pathname:`/task/view/draft/${this.state.task.draft_id}`}}>{this.state.task.draftObject.name}</Link> + } + </div> + </div> + <div className="p-fluid"> + <div className="p-grid"><div className="p-col-12"> + {this.state.taskTemplate?jeditor:""} + </div></div> + </div> + </div> + </React.Fragment> + } + </React.Fragment> + ); + } +} \ 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 323019846cfc267c7874b64bee9ed4bd8a795f10..8d4c63793257e35d53829a1b72ea630c0cfd9b0a 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/index.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/index.js @@ -2,18 +2,77 @@ import React from 'react'; import { Route, Switch, - Redirect + Redirect, } from 'react-router-dom'; +import {NotFound} from '../layout/components/NotFound'; +import {ProjectList, ProjectCreate, ProjectView, ProjectEdit} from './Project'; import {Dashboard} from './Dashboard'; import {Scheduling} from './Scheduling'; +import {TaskEdit, TaskView} from './Task'; +import ViewSchedulingUnit from './Scheduling/ViewSchedulingUnit' + +export const routes = [ + { + path: "/not-found", + component: NotFound + },{ + path: "/dashboard", + component: Dashboard, + name: 'Dashboard' + },{ + path: "/schedulingunit", + component: Scheduling, + name: 'Scheduling Unit' + },{ + path: "/task", + component: TaskView, + name: 'Task' + },{ + path: "/task/view", + component: TaskView, + name: 'Task' + },{ + path: "/task/view/:type/:id", + component: TaskView, + name: 'Task Details' + },{ + path: "/task/edit", + component: TaskEdit, + name: 'Task Edit' + },{ + path: "/schedulingunit/view", + component: ViewSchedulingUnit, + name: 'Scheduling View' + },{ + path: "/project", + component: ProjectList, + name: 'Project List' + },{ + path: "/project/create", + component: ProjectCreate, + name: 'Project Add' + },{ + path: "/project/view", + component: ProjectView, + name: 'Project View' + },{ + path: "/project/view/:id", + component: ProjectView, + name: 'Project View' + },{ + path: "/project/edit/:id", + component: ProjectEdit, + name: 'Project Edit' + }, +]; export const RoutedContent = () => { return ( + <Switch> <Redirect from="/" to="/" exact /> - <Route path="/dashboard" exact component={Dashboard} /> - <Route path="/scheduling" exact component={Scheduling} /> + {routes.map(routeProps => <Route {...routeProps} exact key={routeProps.path} />)} </Switch> ); } \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/services/ScheduleService.js b/SAS/TMSS/frontend/tmss_webapp/src/services/ScheduleService.js new file mode 100644 index 0000000000000000000000000000000000000000..d2d5285744fd955b4ea115424a322b3be51976cf --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/services/ScheduleService.js @@ -0,0 +1,52 @@ +import axios from 'axios' + +export async function getScheduling_Unit_Draft(){ + let res = []; + await axios.get('/api/scheduling_unit_draft/?ordering=id', { + headers: { + "Content-Type": "application/json", + "Authorization": "Basic dGVzdDp0ZXN0" + } + } + ).then(function(response) { + res= response; + + }).catch(function(error) { + console.log('Error on Authentication',error); + }); + return res; +} + +export async function getScheduling_Unit_Draft_By_Id(id){ + let res = []; + await axios.get('/api/scheduling_unit_draft/'+id, { + headers: { + "Content-Type": "application/json", + "Authorization": "Basic dGVzdDp0ZXN0" + } + } + ).then(function(response) { + res= response; + }).catch(function(error) { + console.log('Error on Authentication',error); + }); + return res; +} + +export async function getTasks_Draft_By_scheduling_Unit_Id(id){ + let res=[]; + await axios.get('/api/scheduling_unit_draft/'+id+'/task_draft/?ordering=id', { + headers: { + "Content-Type": "application/json", + "Authorization": "Basic dGVzdDp0ZXN0" + } + } + ).then(function(response) { + res= response; + }).catch(function(error) { + console.log('Error on Authentication',error); + }); + return res; +} + + \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/services/cycle.service.js b/SAS/TMSS/frontend/tmss_webapp/src/services/cycle.service.js new file mode 100644 index 0000000000000000000000000000000000000000..e9ab9620b3a86be0d39ce1c13c7f9e262487d4ba --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/services/cycle.service.js @@ -0,0 +1,40 @@ +const axios = require('axios'); + +//axios.defaults.baseURL = 'http://192.168.99.100:8008/api'; +axios.defaults.headers.common['Authorization'] = 'Basic dGVzdDp0ZXN0'; + +const CycleService = { + getAllCycles: async function() { + try { + const url = `/api/cycle`; + const response = await axios.get(url); + return response.data.results; + } catch (error) { + console.error(error); + } + }, + getCycle: async function(id) { + try { + const url = `/api/cycle/${id}`; + const response = await axios.get(url); + return response.data.results; + } catch (error) { + console.error(error); + } + }, + // To be rmoved + getAllCycle: async function (){ + let res = []; + await axios.get('/api/cycle/') + .then(response => { + res= response; + }).catch(function(error) { + console.error('[cycle.services.getAllCycle]',error); + }); + + return res; + }, + +} + +export default CycleService; \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/services/project.service.js b/SAS/TMSS/frontend/tmss_webapp/src/services/project.service.js new file mode 100644 index 0000000000000000000000000000000000000000..18598cc1c90e304931d4f4e55c1a4c39a4d6c69e --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/services/project.service.js @@ -0,0 +1,200 @@ +import _ from 'lodash'; + +import UnitConverter from './../utils/unit.converter' + +const axios = require('axios'); + +axios.defaults.headers.common['Authorization'] = 'Basic dGVzdDp0ZXN0'; + +const ProjectService = { + getProjectCategories: async function() { + try { + const url = `/api/project_category/`; + const response = await axios.get(url); + return response.data.results; + } catch (error) { + console.error(error); + } + }, + getPeriodCategories: async function() { + try { + const url = `/api/period_category/`; + const response = await axios.get(url); + return response.data.results; + } catch (error) { + console.error(error); + } + }, + getResources: async function() { + try { + // const url = `/api/resource_type/?ordering=name`; + const url = `/api/resource_type`; + const response = await axios.get(url); + // console.log(response); + return response.data.results; + } catch (error) { + console.error(error); + } + }, + getDefaultProjectResources: async function() { + try { + return Promise.resolve({'LOFAR Observing Time': 3600, + 'LOFAR Observing Time prio A': 3600, + 'LOFAR Observing Time prio B': 3600, + 'CEP Processing Time': 3600, + 'LTA Storage': 1024*1024*1024*1024, + 'Number of triggers': 1, + 'LOFAR Support Time': 3600}); + } catch (error) { + console.error(error); + } + }, + saveProject: async function(project, projectQuota) { + try { + const response = await axios.post(('/api/project/'), project); + project = response.data + for (let quota of projectQuota) { + quota.project = project.url; + this.saveProjectQuota(quota); + } + return response.data; + } catch (error) { + // console.log(error); + console.log(error.response.data); + return error.response.data; + } + }, + updateProject: async function(id, project) { + try { + const response = await axios.put((`/api/project/${id}/`), project); + return response.data; + } catch (error) { + // console.log(error); + console.log(error.response.data); + return error.response.data; + } + }, + saveProjectQuota: async function(projectQuota) { + try { + const response = await axios.post(('/api/project_quota/'), projectQuota); + return response.data; + } catch (error) { + console.error(error); + return null; + } + }, + updateProjectQuota: async function(projectQuota) { + try { + const response = await axios.put(`/api/project_quota/${projectQuota.id}/`, projectQuota); + return response.data; + } catch (error) { + console.error(error); + return null; + } + }, + deleteProjectQuota: async function(projectQuota) { + try { + const response = await axios.delete(`/api/project_quota/${projectQuota.id}/`); + return response.status===204?{message: 'deleted'}:null; + } catch (error) { + console.error(error); + return null; + } + }, + getProjects: async function() { + try { + const response = await axios.get(('/api/project/')); + let projects = response.data.results; + const response1 = await axios.get(('/api/project_quota')); + const allProjectQuota = response1.data.results; + for (let project of projects) { + let projectQuota = _.filter(allProjectQuota, function(projQuota) { return _.includes(project.project_quota_ids, projQuota.id)}); + for (const quota of projectQuota) { + project[quota.resource_type_id] = quota; + } + } + return response.data.results; + } catch (error) { + console.error(error); + return []; + } + }, + getProjectDetails: async function(id) { + try { + const response = await axios.get((`/api/project/${id}`)); + let project = response.data; + return project; + } catch(error) { + console.error(error); + return null; + } + }, + getProjectQuota: async function(quotaId) { + try { + const response = await axios.get((`/api/project_quota/${quotaId}`)); + return response.data; + } catch (error) { + console.error(error); + return null; + } + }, + getProjectList: async function() { + try { + const response = await axios.get('/api/project/'); + return response.data.results; + } catch (error) { + console.error('[project.services.getProjectList]',error); + } + }, + + getResourceUnitType: async function(resource_type_id, resourceTypes){ + let res_unit_type = ''; + try{ + await resourceTypes.forEach(resourcetype => { + if(resourcetype.name === resource_type_id){ + res_unit_type = resourcetype.quantity_value; + return res_unit_type; + } + }); + } catch (error) { + console.error('[project.services.getResourceUnitType]',error); + } + return res_unit_type; + }, + + getUpdatedProjectQuota: async function(projects) { + let results = {}; + try{ + if(projects){ + await this.getResources() + .then(resourcetypes =>{ + results.resourcetypes = resourcetypes; + }) + .then( async ()=>{ + for(const project of projects){ + for(const id of project.quota_ids){ + await ProjectService.getProjectQuota(id).then(async quota =>{ + const resourceType = _.find(results.resourcetypes, ["name", quota.resource_type_id]); + project[quota.resource_type_id] = UnitConverter.getUIResourceUnit(resourceType.quantity_value, quota.value); + }) + } + projects.map((pro,index) => { + if(pro.name === project.name){ + project['actionpath']= '/project/view'; + projects[index] = project; + } + return pro; + }); + } + }); + results.projects = projects; + return results.projects; + } + } catch (error) { + console.error('[project.services.getUpdatedProjectQuota]',error); + } + return results; + }, +} + +export default ProjectService; \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/services/schedule.service.js b/SAS/TMSS/frontend/tmss_webapp/src/services/schedule.service.js new file mode 100644 index 0000000000000000000000000000000000000000..cda5342a5c616ea178c7a7ed0d8a7508236daeed --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/services/schedule.service.js @@ -0,0 +1,114 @@ +import axios from 'axios' +import _ from 'lodash'; + +axios.defaults.headers.common['Authorization'] = 'Basic dGVzdDp0ZXN0'; + +const ScheduleService = { + getSchedulingUnitDraft: async function (){ + let res = []; + await axios.get('/api/scheduling_unit_draft/?ordering=id') + .then(response => { + res= response; + }).catch(function(error) { + console.error('[schedule.services.getSchedulingUnitDraft]',error); + }); + return res; + }, + getSchedulingUnitDraftById: async function (id){ + let res = []; + await axios.get('/api/scheduling_unit_draft/'+id) + .then(response => { + res= response; + }).catch(function(error) { + console.error('[schedule.services.getSchedulingUnitDraftById]',error); + }); + return res; + }, + getScheduleTasksBySchedulingUnitId: async function(id){ + let scheduletasklist=[]; + let taskblueprints = []; + // Common keys for Task and Blueprint + let commonkeys = ['id','created_at','description','name','tags','updated_at','url','do_cancel','relative_start_time','relative_stop_time','start_time','stop_time','duration']; + await this.getTaskBlueprints().then( blueprints =>{ + taskblueprints = blueprints.data.results; + }) + await this.getTasksDraftBySchedulingUnitId(id) + .then(response =>{ + for(const task of response.data.results){ + let scheduletask = []; + scheduletask['tasktype'] = 'Task Draft'; + scheduletask['actionpath'] = '/task/view/draft/'+task['id']; + scheduletask['blueprint_draft'] = task['task_blueprints']; + + //fetch task draft details + for(const key of commonkeys){ + scheduletask[key] = task[key]; + } + + //Fetch blueprint details for Task Draft + let filteredblueprints = _.filter(taskblueprints, function(o) { + if (o.draft_id === task['id']) return o; + }); + + for(const blueprint of filteredblueprints){ + let taskblueprint = []; + taskblueprint['tasktype'] = 'Blueprint'; + taskblueprint['actionpath'] = '/task/view/blueprint/'+blueprint['id']; + taskblueprint['blueprint_draft'] = blueprint['draft']; + for(const key of commonkeys){ + taskblueprint[key] = blueprint[key]; + } + //Add Blue print details to array + scheduletasklist.push(taskblueprint); + } + //Add Task Draft details to array + scheduletasklist.push(scheduletask); + } + }).catch(function(error) { + console.error('[schedule.services.getScheduleTasksBySchedulingUnitId]',error); + }); + return scheduletasklist; + }, + getTaskBlueprints: async function (){ + let res=[]; + await axios.get('/api/task_blueprint/?ordering=id') + .then(response => { + res= response; + }).catch(function(error) { + console.error('[schedule.services.getTaskBlueprints]',error); + }); + return res; + }, + getTaskBlueprintByTaskDraftId: async function (id){ + let res=[]; + await axios.get('/api/task_draft/'+id+'/task_blueprint/?ordering=id') + .then(response => { + res= response; + }).catch(function(error) { + console.error('[schedule.services.getTaskBlueprintByTaskDraftId]',error); + }); + return res; + }, + getTasksDraftBySchedulingUnitId: async function (id){ + let res=[]; + await axios.get('/api/scheduling_unit_draft/'+id+'/task_draft/?ordering=id') + .then(response => { + res= response; + }).catch(function(error) { + console.error('[schedule.services.getTasksDraftBySchedulingUnitId]',error); + }); + return res; + }, + getBlueprintsByschedulingUnitId: async function (id){ + let res=[]; + await axios.get('/api/scheduling_unit_draft/'+id+'/scheduling_unit_blueprint/?ordering=id') + .then(response => { + res= response; + }).catch(function(error) { + console.error('[schedule.services.getBlueprintsByschedulingUnitId]',error); + }); + return res; + }, +} + +export default ScheduleService; \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/services/task.service.js b/SAS/TMSS/frontend/tmss_webapp/src/services/task.service.js new file mode 100644 index 0000000000000000000000000000000000000000..e55f34d594e5c81fd4aca1d819e9def4820155ac --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/services/task.service.js @@ -0,0 +1,143 @@ +const axios = require('axios'); + +//axios.defaults.baseURL = 'http://192.168.99.100:8008/api'; +axios.defaults.headers.common['Authorization'] = 'Basic dGVzdDp0ZXN0'; + +const TaskService = { + getTaskDetails: async function (taskType, taskId) { + try { + const url = taskType === 'blueprint'? '/api/task_blueprint/': '/api/task_draft/'; + const response = await axios.get(url + taskId); + response.data.predecessors = []; + response.data.successors = []; + if (taskType === 'blueprint') { + response.data.blueprints = []; + } else { + response.data.draftName = null; + } + return this.getTaskRelationsByTask(taskType, response.data) + .then(relations => { + response.data.predecessors = relations.predecessors; + response.data.successors = relations.successors; + if (taskType === 'draft') { + response.data.blueprints = relations.blueprints; + } else { + response.data.draftObject = relations.draft; + } + return response.data; + }); + + } catch (error) { + console.error(error); + } + }, + getTaskTemplate: async function(templateId) { + try { + const response = await axios.get('/api/task_template/' + templateId); + return response.data; + } catch (error) { + console.log(error); + } + }, + getTaskTemplates: async function() { + try { + const response = await axios.get('/api/task_template/'); + return response.data.results; + } catch (error) { + console.log(error); + } + }, + getSchedulingUnit: async function(type, id) { + try { + const response = await axios.get('/api/scheduling_unit_draft/' + id); + return response.data; + } catch (error) { + console.error(error); + } + }, + updateTask: async function(type, task) { + try { + const response = await axios.put(('/api/task_draft/' + task.id + "/"), task); + return response.data; + } catch (error) { + console.error(error); + return null; + } + }, + getTaskRelation: async function(type, id) { + try { + const url = type === 'blueprint'? '/api/task_blueprint/': `/api/task_draft/${id}/task_relation_draft/`; + const response = await axios.get(url); + return response.data; + } catch (error) { + console.error(error); + } + }, + getTaskRelationsByTask: async function(type, task) { + try { + let relations = {}; + return this.getTaskPredecessors(type, task.id) + .then( predecessors => { + relations.predecessors = predecessors; + return this.getTaskSuccessors(type, task.id); + }) + .then( successors => { + relations.successors = successors; + if (type === 'draft') { + return this.getDraftsTaskBlueprints(task.id); + } else { + return this.getTaskdraft(task.draft_id); + } + }) + .then( result => { + if (type === 'draft') { + relations.blueprints = result; + } else { + relations.draft = result; + } + return relations; + }); + } catch (error) { + console.log(error); + } + }, + getTaskPredecessors: async function(type, id) { + try { + const url = type === 'blueprint'? `/api/task_blueprint/${id}/predecessors`: `/api/task_draft/${id}/predecessors/`; + const response = await axios.get(url); + return response.data; + } catch (error) { + console.error(error); + } + }, + getTaskSuccessors: async function(type, id) { + try { + const url = type === 'blueprint'? `/api/task_blueprint/${id}/successors`: `/api/task_draft/${id}/successors/`; + const response = await axios.get(url); + return response.data; + } catch (error) { + console.error(error); + } + }, + getDraftsTaskBlueprints: async function(id) { + try { + const url = `/api/task_draft/${id}/task_blueprint`; + const response = await axios.get(url); + return response.data.results; + } catch (error) { + console.error(error); + } + }, + getTaskdraft: async function(id) { + try { + const url = `/api/task_draft/${id}`; + const response = await axios.get(url); + return response.data; + } catch (error) { + console.error(error); + } + } + +} + +export default TaskService; \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/utils/ui.constants.js b/SAS/TMSS/frontend/tmss_webapp/src/utils/ui.constants.js new file mode 100644 index 0000000000000000000000000000000000000000..732079588cf68c3938c6dc9efb8e86d029b3bb98 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/utils/ui.constants.js @@ -0,0 +1,5 @@ +const UIConstants = { + tooltipOptions: {position: 'left', event: 'hover', className:"p-tooltip-custom"} +} + +export default UIConstants; \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/utils/unit.converter.js b/SAS/TMSS/frontend/tmss_webapp/src/utils/unit.converter.js new file mode 100644 index 0000000000000000000000000000000000000000..30f95a31a9e56b78a160541e02bb63597b9dcf9a --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/utils/unit.converter.js @@ -0,0 +1,23 @@ +const UnitConverter = { + resourceUnitMap: {'time':{display: 'Hours', conversionFactor: 3600, mode:'decimal', minFractionDigits:0, maxFractionDigits: 2 }, + 'bytes': {display: 'TB', conversionFactor: (1024*1024*1024*1024), mode:'decimal', minFractionDigits:0, maxFractionDigits: 3}, + 'number': {display: 'Numbers', conversionFactor: 1, mode:'decimal', minFractionDigits:0, maxFractionDigits: 0}}, + + getDBResourceUnit: function() { + + }, + getUIResourceUnit: function(type, value) { + try{ + if(this.resourceUnitMap[type]){ + var retval = Number.parseFloat(value/(this.resourceUnitMap[type].conversionFactor)).toFixed(this.resourceUnitMap[type].maxFractionDigits) + return retval; + } + + }catch(error){ + console.error('[unit.converter.getUIResourceUnit]',error); + } + return value + } +}; + +export default UnitConverter; \ No newline at end of file diff --git a/SAS/TMSS/src/tmss/settings.py b/SAS/TMSS/src/tmss/settings.py index 01e109df38945305f0cf88b3f37565b541073264..6ad450c3e6b4f378fe1c3ea88e6a494f58471c8d 100644 --- a/SAS/TMSS/src/tmss/settings.py +++ b/SAS/TMSS/src/tmss/settings.py @@ -133,7 +133,7 @@ ROOT_URLCONF = 'lofar.sas.tmss.tmss.urls' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [BASE_DIR, os.path.join(BASE_DIR, 'templates'), os.path.join(BASE_DIR, '../frontend','frontend_poc')], + 'DIRS': [BASE_DIR, os.path.join(BASE_DIR, 'templates'), os.path.join(BASE_DIR, '../frontend','tmss_webapp')], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ @@ -146,6 +146,10 @@ TEMPLATES = [ }, ] +STATICFILES_DIRS = [ + os.path.join(BASE_DIR, '../frontend','tmss_webapp/build/static') +] + WSGI_APPLICATION = 'lofar.sas.tmss.tmss.wsgi.application' # Database @@ -328,4 +332,4 @@ SWAGGER_SETTINGS = { } }, -} \ No newline at end of file +} diff --git a/SAS/TMSS/src/tmss/tmssapp/adapters/parset.py b/SAS/TMSS/src/tmss/tmssapp/adapters/parset.py index 64ae86d3cdb23288aa6709b399ae1fc5c5afa207..29ea31b133d257425ec9804adeb7ce165ea0d478 100644 --- a/SAS/TMSS/src/tmss/tmssapp/adapters/parset.py +++ b/SAS/TMSS/src/tmss/tmssapp/adapters/parset.py @@ -45,7 +45,7 @@ def _convert_to_parset_dict_for_observationcontrol_schema(subtask: models.Subtas parset["Observation.otdbID"] = 0 # Needed by MACScheduler; should/can this be the same as subtask.pk? parset["Observation.tmssID"] = subtask.pk parset["Observation.processType"] = subtask.specifications_template.type.value.capitalize() - parset["Observation.processSubtype"] = "Beam Observation" # TODO: where to derive the processSubtype from? + parset["Observation.processSubtype"] = "Beam Observation" parset["Observation.Campaign.name"] = subtask.task_blueprint.scheduling_unit_blueprint.draft.scheduling_set.project.name parset["Observation.startTime"] = formatDatetime(subtask.start_time) if isinstance(subtask.start_time, datetime) else subtask.start_time parset["Observation.stopTime"] = formatDatetime(subtask.stop_time) if isinstance(subtask.stop_time, datetime) else subtask.stop_time @@ -55,8 +55,8 @@ def _convert_to_parset_dict_for_observationcontrol_schema(subtask: models.Subtas parset["Observation.antennaArray"] = "HBA" if "HBA" in spec["stations"]["antenna_set"] else "LBA" # maybe not mandatory? parset["Observation.antennaSet"] = spec["stations"]["antenna_set"] parset["Observation.bandFilter"] = spec["stations"]["filter"] - parset["Observation.sampleClock"] = 200 # why is this not part of the schema? for example as a required setting with a single allowed value. - parset["Observation.nrBitsPerSample"] = 8 # why is this not part of the schema? for example as a required setting with a single allowed value. + parset["Observation.sampleClock"] = 200 # fixed value, no other values are supported + parset["Observation.nrBitsPerSample"] = 8 # fixed value, no other values are supported. parset["Observation.strategy"] = "default" # maybe not mandatory? digi_beams = spec['stations']['digital_pointings'] @@ -160,7 +160,7 @@ def _convert_to_parset_dict_for_pipelinecontrol_schema(subtask: models.Subtask) # General parset["prefix"] = "LOFAR." parset["Observation.processType"] = "Pipeline" - parset["Observation.processSubtype"] = "Averaging Pipeline" # TODO: where to derive the processSubtype from? + parset["Observation.processSubtype"] = "Averaging Pipeline" parset["Observation.ObservationControl.PythonControl.pythonProgram"] = "preprocessing_pipeline.py" parset["Observation.ObservationControl.PythonControl.softwareVersion"] = "" parset["Observation.Campaign.name"] = subtask.task_blueprint.scheduling_unit_blueprint.draft.scheduling_set.project.name @@ -168,8 +168,8 @@ def _convert_to_parset_dict_for_pipelinecontrol_schema(subtask: models.Subtask) parset["Observation.Scheduler.predecessors"] = [] parset["Observation.Cluster.ProcessingCluster.clusterName"] = subtask.cluster.name parset["Observation.Cluster.ProcessingCluster.clusterPartition"] = 'cpu' - parset["Observation.Cluster.ProcessingCluster.numberOfTasks"] = 110 # TODO: compute numberOfTasks - parset["Observation.Cluster.ProcessingCluster.numberOfCoresPerTask"] = 2 # TODO: compute numberOfCoresPerTask + parset["Observation.Cluster.ProcessingCluster.numberOfTasks"] = 110 # deprecated (fixed value) to be completely removed in parset with 'JDM-patch 'soon + parset["Observation.Cluster.ProcessingCluster.numberOfCoresPerTask"] = 2 # deprecated (fixed value) to be completely removed in parset with 'JDM-patch 'soon # DPPP steps dppp_steps = [] @@ -250,7 +250,7 @@ def _convert_to_parset_dict_for_pipelinecontrol_schema(subtask: models.Subtask) parset["Observation.ObservationControl.PythonControl.DPPP.demixer.type"] = "demixer" parset["Observation.ObservationControl.PythonControl.DPPP.steps"] = "[%s]" % ",".join(dppp_steps) - parset["Observation.ObservationControl.PythonControl.DPPP.msout.storagemanager.name"] = spec["storagemanager"] # todo: needs to be emptystring when standard/basic/non-dysco? + parset["Observation.ObservationControl.PythonControl.DPPP.msout.storagemanager.name"] = spec["storagemanager"] # Dataproducts parset["Observation.DataProducts.Input_Correlated.enabled"] = "true" diff --git a/SAS/TMSS/src/tmss/tmssapp/models/scheduling.py b/SAS/TMSS/src/tmss/tmssapp/models/scheduling.py index e869c80cc92bf69d09bc0ac0b90442c2714070aa..0868f0e846e9baed309f476e779da82336ddf0b6 100644 --- a/SAS/TMSS/src/tmss/tmssapp/models/scheduling.py +++ b/SAS/TMSS/src/tmss/tmssapp/models/scheduling.py @@ -107,9 +107,7 @@ class SubtaskTemplate(Template): realtime = BooleanField(default=False) class Meta: - # TODO: move up to the abstract base class and replace with django 3.0 UniqueConstraint(... name='%*class)s_unique_name_version) - constraints = [UniqueConstraint(fields=['name', 'version'], name='SubtaskTemplate_unique_name_version')] - + pass class DefaultSubtaskTemplate(BasicCommon): name = CharField(max_length=128, unique=True) @@ -118,8 +116,7 @@ class DefaultSubtaskTemplate(BasicCommon): class DataproductSpecificationsTemplate(Template): class Meta: - # TODO: move up to the abstract base class and replace with django 3.0 UniqueConstraint(... name='%*class)s_unique_name_version) - constraints = [UniqueConstraint(fields=['name', 'version'], name='DataproductSpecificationsTemplate_unique_name_version')] + pass class DefaultDataproductSpecificationsTemplate(BasicCommon): @@ -129,8 +126,7 @@ class DefaultDataproductSpecificationsTemplate(BasicCommon): class DataproductFeedbackTemplate(Template): class Meta: - # TODO: move up to the abstract base class and replace with django 3.0 UniqueConstraint(... name='%*class)s_unique_name_version) - constraints = [UniqueConstraint(fields=['name', 'version'], name='DataproductFeedbackTemplate_unique_name_version')] + pass # todo: do we need to specify a default? diff --git a/SAS/TMSS/src/tmss/tmssapp/models/specification.py b/SAS/TMSS/src/tmss/tmssapp/models/specification.py index 26683b2cde8f68b1bb5944f8696d0aa09b905f30..c73951f9f2275285fca57e0b297b03de9916e0c1 100644 --- a/SAS/TMSS/src/tmss/tmssapp/models/specification.py +++ b/SAS/TMSS/src/tmss/tmssapp/models/specification.py @@ -225,8 +225,7 @@ class Template(NamedCommon): class Meta: abstract = True - # TODO: remove all <class>_unique_name_version UniqueConstraint's from the subclasses and replace by this line below when we start using django 3.0 - # constraints = [UniqueConstraint(fields=['name', 'version'], name='%(class)s_unique_name_version')] + constraints = [UniqueConstraint(fields=['name', 'version'], name='%(class)s_unique_name_version')] # concrete models @@ -234,8 +233,7 @@ class GeneratorTemplate(Template): create_function = CharField(max_length=128, help_text='Python function to call to execute the generator.') class Meta: - # TODO: move up to the abstract base class and replace with django 3.0 UniqueConstraint(... name='%*class)s_unique_name_version) - constraints = [UniqueConstraint(fields=['name', 'version'], name='GeneratorTemplate_unique_name_version')] + pass class DefaultGeneratorTemplate(BasicCommon): @@ -245,8 +243,7 @@ class DefaultGeneratorTemplate(BasicCommon): class SchedulingUnitTemplate(Template): class Meta: - # TODO: move up to the abstract base class and replace with django 3.0 UniqueConstraint(... name='%*class)s_unique_name_version) - constraints = [UniqueConstraint(fields=['name', 'version'], name='SchedulingUnitTemplate_unique_name_version')] + pass class DefaultSchedulingUnitTemplate(BasicCommon): @@ -259,8 +256,7 @@ class TaskTemplate(Template): type = ForeignKey('TaskType', null=False, on_delete=PROTECT) class Meta: - # TODO: move up to the abstract base class and replace with django 3.0 UniqueConstraint(... name='%*class)s_unique_name_version) - constraints = [UniqueConstraint(fields=['name', 'version'], name='TaskTemplate_unique_name_version')] + pass class DefaultTaskTemplate(BasicCommon): name = CharField(max_length=128, unique=True) @@ -269,9 +265,7 @@ class DefaultTaskTemplate(BasicCommon): class TaskRelationSelectionTemplate(Template): class Meta: - # TODO: move up to the abstract base class and replace with django 3.0 UniqueConstraint(... name='%*class)s_unique_name_version) - constraints = [UniqueConstraint(fields=['name', 'version'], name='TaskRelationSelectionTemplate_unique_name_version')] - + pass class DefaultTaskRelationSelectionTemplate(BasicCommon): name = CharField(max_length=128, unique=True) @@ -374,7 +368,7 @@ class SchedulingUnitDraft(NamedCommon): copy_reason = ForeignKey('CopyReason', null=True, on_delete=PROTECT, help_text='Reason why source was copied (NULLable).') generator_instance_doc = JSONField(null=True, help_text='Parameter value that generated this run draft (NULLable).') scheduling_set = ForeignKey('SchedulingSet', related_name='scheduling_unit_drafts', on_delete=CASCADE, help_text='Set to which this scheduling unit draft belongs.') - requirements_template = ForeignKey('SchedulingUnitTemplate', on_delete=CASCADE, help_text='Schema used for requirements_doc.') # todo: 'schema'? + requirements_template = ForeignKey('SchedulingUnitTemplate', on_delete=CASCADE, help_text='Schema used for requirements_doc.') def save(self, force_insert=False, force_update=False, using=None, update_fields=None): if self.requirements_doc and self.requirements_template_id and self.requirements_template.schema: @@ -685,7 +679,7 @@ class TaskBlueprint(NamedCommon): class TaskRelationDraft(BasicCommon): selection_doc = JSONField(help_text='Filter for selecting dataproducts from the output role.') - selection_template = ForeignKey('TaskRelationSelectionTemplate', on_delete=CASCADE, help_text='Schema used for selection_doc.') # todo: 'schema'? + selection_template = ForeignKey('TaskRelationSelectionTemplate', on_delete=CASCADE, help_text='Schema used for selection_doc.') dataformat = ForeignKey('Dataformat', null=False, on_delete=PROTECT, help_text='Selected data format to use. One of (MS, HDF5).') # caveat: it might look like producer has an incorrect related_name='consumed_by'. But it really is correct, denends on the way you look at it diff --git a/SAS/TMSS/src/tmss/tmssapp/populate.py b/SAS/TMSS/src/tmss/tmssapp/populate.py index 8de5dc5f2d0bee9de048b37ae94630d3006a83cf..dbb041b5e1f659fc9129bb7571858a3076b559ea 100644 --- a/SAS/TMSS/src/tmss/tmssapp/populate.py +++ b/SAS/TMSS/src/tmss/tmssapp/populate.py @@ -91,9 +91,12 @@ def populate_test_data(): scheduling_unit_doc = get_default_json_object_for_schema(scheduling_unit_template.schema) # create and add a calibrator task spec + # Change autoselect to False (or provide tile_beam pointings for Target Observation) to avoid Exception + json_schema_calibrator = get_default_json_object_for_schema(models.TaskTemplate.objects.get(name="calibrator schema").schema) + json_schema_calibrator['autoselect'] = False scheduling_unit_doc['tasks'].append({"name": "Calibrator Observation 1", "description": "Calibrator Observation for UC1 HBA scheduling unit", - "specifications_doc": get_default_json_object_for_schema(models.TaskTemplate.objects.get(name="calibrator schema").schema), + "specifications_doc": json_schema_calibrator, "specifications_template": "calibrator schema"}) # create and add a calibrator preprocessing spec @@ -123,7 +126,7 @@ def populate_test_data(): # create and add a calibrator task spec scheduling_unit_doc['tasks'].append({"name": "Calibrator Observation 2", "description": "Calibrator Observation for UC1 HBA scheduling unit", - "specifications_doc": get_default_json_object_for_schema(models.TaskTemplate.objects.get(name="calibrator schema").schema), + "specifications_doc": json_schema_calibrator, "specifications_template": "calibrator schema"}) # create and add a calibrator preprocessing spec diff --git a/SAS/TMSS/src/tmss/tmssapp/subtasks.py b/SAS/TMSS/src/tmss/tmssapp/subtasks.py index 493c964890c8b608c47df6971f1800c9558c9a35..110a3c609b5e3bfc99f8f9439b5b04c793ee4e1c 100644 --- a/SAS/TMSS/src/tmss/tmssapp/subtasks.py +++ b/SAS/TMSS/src/tmss/tmssapp/subtasks.py @@ -18,6 +18,7 @@ from lofar.sas.tmss.tmss.tmssapp.models import * from lofar.sas.resourceassignment.resourceassigner.rarpc import RARPC from lofar.sas.resourceassignment.resourceassignmentservice.rpc import RADBRPC from lofar.sas.tmss.tmss.tmssapp.adapters.parset import convert_to_parset_dict +from lofar.sas.resourceassignment.taskprescheduler.cobaltblocksize import CorrelatorSettings, BlockConstraints, BlockSize # ==== various create* methods to convert/create a TaskBlueprint into one or more Subtasks ==== @@ -51,6 +52,7 @@ def create_subtasks_from_task_blueprint(task_blueprint: TaskBlueprint) -> [Subta logger.error(e) return subtasks else: + logger.error('Cannot create subtasks for task id=%s because no generator exists for its schema name=%s' % (task_blueprint.pk, template_name)) raise SubtaskCreationException('Cannot create subtasks for task id=%s because no generator exists for its schema name=%s' % (task_blueprint.pk, template_name)) @@ -79,26 +81,39 @@ def create_observation_subtask_specifications_from_observation_task_blueprint(ta # The rest of it's specs are 'shared' with the target observation. # So... copy the calibrator specs first, then loop over the shared target/calibrator specs... if 'calibrator' in task_blueprint.specifications_template.name.lower(): - subtask_spec['stations']['analog_pointing'] = {"direction_type": task_spec["pointing"]["direction_type"], - "angle1": task_spec["pointing"]["angle1"], - "angle2": task_spec["pointing"]["angle2"]} - # for the calibrator, the digital pointing is equal to the analog pointing - subtask_spec['stations']['digital_pointings'] = [ {'name': 'calibrator', # there is no name for the calibrator pointing in the task spec - 'subbands': list(range(0,488)), # there are no subbands for the calibrator pointing in the task spec - 'pointing': subtask_spec['stations']['analog_pointing'] } ] - - if task_spec.get('autoselect', False): - logger.info("auto-selecting calibrator target based on elevation of target observation...") - # what to do? overrive the pointing??? - #TODO: implement - + # Calibrator requires related Target Task Observation for some specifications target_task_blueprint = get_related_target_observation_task_blueprint(task_blueprint) if target_task_blueprint is None: raise SubtaskCreationException("Cannot create calibrator observation subtask specifications from task_blueprint id=%s with template name='%s' because no related target observation task_blueprint is found" % ( task_blueprint.id, task_blueprint.specifications_template.name)) + target_task_spec = target_task_blueprint.specifications_doc - task_spec = target_task_blueprint.specifications_doc - logger.info("Using station and correlator settings for calibrator observation task_blueprint id=%s from target observation task_blueprint id=%s", task_blueprint.id, target_task_blueprint.id) + if task_spec.get('autoselect', True): + logger.info("auto-selecting calibrator target based on elevation of target observation...") + # Get related Target Observation Task + if "tile_beam" in target_task_spec: + subtask_spec['stations']['analog_pointing'] = { + "direction_type": target_task_spec["tile_beam"]["direction_type"], + "angle1": target_task_spec["tile_beam"]["angle1"], + "angle2": target_task_spec["tile_beam"]["angle2"]} + else: + raise SubtaskCreationException("Cannot determine the pointing specification from task_blueprint " + "id=%s in auto-select mode, because the related target observation " + "task_blueprint id=%s has no tile beam pointing defined" % ( + task_blueprint.id, target_task_blueprint.id)) + else: + subtask_spec['stations']['analog_pointing'] = {"direction_type": task_spec["pointing"]["direction_type"], + "angle1": task_spec["pointing"]["angle1"], + "angle2": task_spec["pointing"]["angle2"]} + + # for the calibrator, the digital pointing is equal to the analog pointing + subtask_spec['stations']['digital_pointings'] = [ {'name': 'calibrator', # there is no name for the calibrator pointing in the task spec + 'subbands': list(range(0,488)), # there are no subbands for the calibrator pointing in the task spec + 'pointing': subtask_spec['stations']['analog_pointing'] } ] + # Use the Task Specification of the Target Observation + task_spec = target_task_spec + logger.info("Using station and correlator settings for calibrator observation task_blueprint id=%s from target observation task_blueprint id=%s", + task_blueprint.id, target_task_blueprint.id) subtask_spec['stations']["antenna_set"] = task_spec["antenna_set"] subtask_spec['stations']["filter"] = task_spec["filter"] @@ -132,11 +147,12 @@ def create_observation_subtask_specifications_from_observation_task_blueprint(ta "angle2": task_spec["tile_beam"]["angle2"] } if "correlator" in task_spec: - subtask_spec["COBALT"]["correlator"]["channels_per_subband"] = task_spec["correlator"]["channels_per_subband"] - - # TODO: compute remaining subtask correlator settings from task_spec ? - # subtask_spec["COBALT"]["correlator"]["integration_time"] = task_spec["correlator"]["integration_time"] - # subtask_spec["COBALT"]["correlator"]["storage_cluster"] = task_spec["correlator"]["storage_cluster"] + corr = CorrelatorSettings() + corr.nrChannelsPerSubband = task_spec["correlator"]["channels_per_subband"] + corr.integrationTime = task_spec["correlator"]["integration_time"] + calculator = BlockSize(constraints=BlockConstraints(correlatorSettings=corr)) + subtask_spec["COBALT"]["correlator"]["blocks_per_integration"] = calculator.nrBlocks + subtask_spec["COBALT"]["correlator"]["integrations_per_block"] = calculator.nrSubblocks # make sure that the subtask_spec is valid conform the schema validate_json_against_schema(subtask_spec, subtask_template.schema) @@ -245,7 +261,7 @@ def create_qafile_subtask_from_observation_subtask(observation_subtask: Subtask) raise ValueError("Cannot create %s subtask for subtask id=%d because it is not DEFINED" % ( SubtaskType.Choices.QA_FILES.value, observation_subtask.pk)) - obs_task_spec = observation_subtask.task_blueprint.specifications_doc + obs_task_spec = get_observation_task_specification_with_check_for_calibrator(observation_subtask) obs_task_qafile_spec = obs_task_spec.get("QA", {}).get("file_conversion", {}) if not obs_task_qafile_spec.get("enabled", False): @@ -288,6 +304,7 @@ def create_qafile_subtask_from_observation_subtask(observation_subtask: Subtask) def create_qaplots_subtask_from_task_blueprint(task_blueprint: TaskBlueprint) -> Subtask: + qafile_subtasks = [st for st in task_blueprint.subtasks.all() if st.specifications_template.type.value == SubtaskType.Choices.QA_FILES.value] if qafile_subtasks: qafile_subtask = qafile_subtasks[0] # TODO: decide what to do when there are multiple qafile subtasks? @@ -309,7 +326,7 @@ def create_qaplots_subtask_from_qafile_subtask(qafile_subtask: Subtask) -> Subta SubtaskType.Choices.QA_PLOTS.value, qafile_subtask.pk, qafile_subtask.specifications_template.type, SubtaskType.Choices.QA_FILES.value)) - obs_task_spec = qafile_subtask.task_blueprint.specifications_doc + obs_task_spec = get_observation_task_specification_with_check_for_calibrator(qafile_subtask) obs_task_qaplots_spec = obs_task_spec.get("QA", {}).get("plots", {}) if not obs_task_qaplots_spec.get("enabled", False): @@ -350,6 +367,7 @@ def create_qaplots_subtask_from_qafile_subtask(qafile_subtask: Subtask) -> Subta # done, now return the subtask, and allow the system to wait for the predecessors to be finished before we schedule this qaplots_subtask return qaplots_subtask + def create_preprocessing_subtask_from_task_blueprint(task_blueprint: TaskBlueprint) -> Subtask: ''' Create a subtask to for the preprocessing pipeline. This method implements "Instantiate subtasks" step from the "Specification Flow" @@ -357,11 +375,12 @@ def create_preprocessing_subtask_from_task_blueprint(task_blueprint: TaskBluepri ''' # step 0: check pre-requisites check_prerequities_for_subtask_creation(task_blueprint) - # TODO: go more elegant lookup of predecessor observation task - observation_predecessor_tasks = [t for t in task_blueprint.predecessors.all() if any(st for st in t.subtasks.all() if st.specifications_template.type.value == SubtaskType.Choices.OBSERVATION.value)] + observation_predecessor_tasks = [t for t in task_blueprint.predecessors.all() if any(st for st in t.subtasks.all() + if st.specifications_template.type.value == SubtaskType.Choices.OBSERVATION.value)] if not observation_predecessor_tasks: - raise SubtaskCreationException("Cannot create a subtask for task_blueprint id=%s because it is not connected to an observation predecessor (sub)task." % task_blueprint.pk) + raise SubtaskCreationException("Cannot create a subtask for task_blueprint id=%s because it is not connected " + "to an observation predecessor (sub)task." % task_blueprint.pk) # step 1: create subtask in defining state, with filled-in subtask_template subtask_template = SubtaskTemplate.objects.get(name='pipelinecontrol schema') @@ -420,7 +439,8 @@ def schedule_subtask(subtask: Subtask) -> Subtask: if subtask.specifications_template.type.value == SubtaskType.Choices.QA_PLOTS.value: return schedule_qaplots_subtask(subtask) - raise SubtaskSchedulingException("Cannot schedule subtask id=%d because there is no schedule-method known for this subtasktype=%s." % (subtask.pk, subtask.specifications_template.type.value)) + raise SubtaskSchedulingException("Cannot schedule subtask id=%d because there is no schedule-method known for this subtasktype=%s." % + (subtask.pk, subtask.specifications_template.type.value)) except Exception as e: try: # set the subtask to state 'ERROR'... @@ -439,7 +459,8 @@ def check_prerequities_for_scheduling(subtask: Subtask) -> bool: for predecessor in subtask.predecessors.all(): if predecessor.state.value != SubtaskState.Choices.FINISHED.value: - raise SubtaskSchedulingException("Cannot schedule subtask id=%d because its predecessor id=%s in not FINISHED but state=%s" % (subtask.pk, predecessor.pk, predecessor.state.value)) + raise SubtaskSchedulingException("Cannot schedule subtask id=%d because its predecessor id=%s in not FINISHED but state=%s" + % (subtask.pk, predecessor.pk, predecessor.state.value)) # check if settings allow scheduling observations setting = Setting.objects.get(name='allow_scheduling_observations') @@ -451,7 +472,8 @@ def check_prerequities_for_scheduling(subtask: Subtask) -> bool: def _assign_resources(subtask: Subtask): if subtask.state.value != SubtaskState.Choices.SCHEDULING.value: - raise SubtaskSchedulingException("Cannot assign resources for subtask id=%d because it is not in SCHEDULING state. Current state=%s" % (subtask.pk, subtask.state.value)) + raise SubtaskSchedulingException("Cannot assign resources for subtask id=%d because it is not in SCHEDULING state. " + "Current state=%s" % (subtask.pk, subtask.state.value)) def create_ra_specification(_subtask): parset_dict = convert_to_parset_dict(_subtask) @@ -887,3 +909,23 @@ def specifications_doc_meets_selection_doc(specifications_doc, selection_doc): logger.debug("specs %s matches selection %s: %s" % (specifications_doc, selection_doc, meets_criteria)) return meets_criteria + + +def get_observation_task_specification_with_check_for_calibrator(subtask): + """ + Retrieve the observation task blueprint specifications_doc from the given subtask object + If the Task is a calibrator then the related Target Observation specification should be returned + :param: subtask object + :return: task_spec: the specifications_doc of the blue print task which is allways a target observation + """ + if 'calibrator' in subtask.task_blueprint.specifications_template.name.lower(): + # Calibrator requires related Target Task Observation for some specifications + target_task_blueprint = get_related_target_observation_task_blueprint(subtask.task_blueprint) + if target_task_blueprint is None: + raise SubtaskCreationException("Cannot retrieve specifications for subtask id=%d because no related target observation is found " % subtask.pk) + task_spec = target_task_blueprint.specifications_doc + logger.info("Using specifications for calibrator observation (id=%s) from target observation task_blueprint id=%s", + subtask.task_blueprint.id, target_task_blueprint.id) + else: + task_spec = subtask.task_blueprint.specifications_doc + return task_spec diff --git a/SAS/TMSS/src/tmss/tmssapp/tasks.py b/SAS/TMSS/src/tmss/tmssapp/tasks.py index 5153d5e1c30535b940e42f8a99866b4d1f0b6207..67b821e6d26b061032862045684e3d216b3c623e 100644 --- a/SAS/TMSS/src/tmss/tmssapp/tasks.py +++ b/SAS/TMSS/src/tmss/tmssapp/tasks.py @@ -18,7 +18,6 @@ def create_scheduling_unit_blueprint_from_scheduling_unit_draft(scheduling_unit_ """ logger.debug("create_scheduling_unit_blueprint_from_scheduling_unit_draft(scheduling_unit_draft.id=%s name='%s')", scheduling_unit_draft.pk, scheduling_unit_draft.name) - # TODO: copy/fill-in the properties from the draft to the blueprint scheduling_unit_blueprint = SchedulingUnitBlueprint.objects.create( name="%s (SchedulingUnitBlueprint)" % (scheduling_unit_draft.name,), description="%s (SchedulingUnitBlueprint)" % (scheduling_unit_draft.description or "<no description>",), diff --git a/SAS/TMSS/src/tmss/tmssapp/views.py b/SAS/TMSS/src/tmss/tmssapp/views.py index bfb670fd87bbbd28b9addf622ef51b2a6b8c5385..4614c940953d2a277b00cf1eb0589ef6efb1edd5 100644 --- a/SAS/TMSS/src/tmss/tmssapp/views.py +++ b/SAS/TMSS/src/tmss/tmssapp/views.py @@ -25,7 +25,7 @@ def subtask_parset(request, subtask_pk:int): return HttpResponse(str(parset), content_type='text/plain') def index(request): - return render(request, os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))), '../../frontend','frontend_poc/build/index.html')) + return render(request, os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))), '../../frontend','tmss_webapp/build/index.html')) #return render(request, "../../../frontend/frontend_poc/build/index.html") def task_specify_observation(request, pk=None): diff --git a/SAS/TMSS/src/tmss/urls.py b/SAS/TMSS/src/tmss/urls.py index 35696e914d1a97e0b128ec796ad411c9f0b2cf36..5d831d9cf5d96fef8b1733a29786192f3ce82c42 100644 --- a/SAS/TMSS/src/tmss/urls.py +++ b/SAS/TMSS/src/tmss/urls.py @@ -19,7 +19,7 @@ from django.contrib import admin from django.contrib.auth.views import LoginView, LogoutView from django.urls import path, re_path from django.conf.urls import url, include -from django.views.generic.base import TemplateView +from django.views.generic.base import TemplateView, RedirectView from collections import OrderedDict from rest_framework import routers, permissions @@ -75,7 +75,14 @@ class TMSSAPIRootView(routers.APIRootView): return response -router = routers.DefaultRouter() +class OptionalSlashRouter(routers.DefaultRouter): + + def __init__(self): + super(routers.DefaultRouter, self).__init__() + self.trailing_slash = '/?' + + +router = OptionalSlashRouter() router.APIRootView = TMSSAPIRootView router.register(r'tags', viewsets.TagsViewSet) @@ -175,6 +182,4 @@ frontend_urlpatterns = [ # prefix everything for proxy #urlpatterns = [url(r'^api/', include(urlpatterns)), url(r'^oidc/', include('mozilla_django_oidc.urls')),] -urlpatterns = [url(r'^api/', include(urlpatterns)), url(r'^oidc/', include('mozilla_django_oidc.urls')), url(r'^frontend/', include(frontend_urlpatterns)) , url(r'^frontend/(?:.*)/?$', include(frontend_urlpatterns)),] - - +urlpatterns = [url(r'^api$', RedirectView.as_view(url='/api/')), url(r'^api/', include(urlpatterns)), url(r'^oidc$', RedirectView.as_view(url='/oidc/')), url(r'^oidc/', include('mozilla_django_oidc.urls')), url(r'^.*', include(frontend_urlpatterns)),] diff --git a/SAS/TMSS/test/t_scheduling.py b/SAS/TMSS/test/t_scheduling.py index 446b75fb538ccc5fe7df866727e7fdef68514561..f4de89666fd4bb05b82009ccc46d11fd578cc769 100755 --- a/SAS/TMSS/test/t_scheduling.py +++ b/SAS/TMSS/test/t_scheduling.py @@ -59,6 +59,22 @@ from lofar.sas.tmss.test.tmss_test_data_rest import TMSSRESTTestDataCreator test_data_creator = TMSSRESTTestDataCreator(tmss_test_env.django_server.url, (tmss_test_env.ldap_server.dbcreds.user, tmss_test_env.ldap_server.dbcreds.password)) from datetime import datetime, timedelta from lofar.sas.resourceassignment.resourceassigner.rarpc import RARPC +from lofar.sas.tmss.tmss.tmssapp import models +from lofar.sas.tmss.tmss.tmssapp.subtasks import * + + +def create_subtask_object_for_testing(subtask_type_value, subtask_state_value): + """ + Helper function to create a subtask object for testing with given subtask value and subtask state value + as string (no object) + For these testcases 'pipelinecontrol schema' and 'observationcontrol schema' is relevant + """ + subtask_template_obj = models.SubtaskTemplate.objects.get(name="%scontrol schema" % subtask_type_value) + subtask_state_obj = models.SubtaskState.objects.get(value=subtask_state_value) + subtask_data = Subtask_test_data(subtask_template=subtask_template_obj, state=subtask_state_obj) + return models.Subtask.objects.create(**subtask_data) + + class SchedulingTest(unittest.TestCase): def setUp(self): @@ -224,6 +240,51 @@ class SchedulingTest(unittest.TestCase): client.set_subtask_status(subtask['id'], 'finished') +class SubtaskInputOutputTest(unittest.TestCase): + """ + Subtask Input and Output test + These testcases are located in the t_scheduling module, because during scheduling the output + dataproducts are assigned + """ + + def setUp(self) -> None: + # make sure we're allowed to schedule + setting = Setting.objects.get(name='allow_scheduling_observations') + setting.value = True + setting.save() + + def test_schedule_pipeline_subtask_filters_predecessor_output_dataproducts_for_input(self): + # setup: + # create observation subtask and outputs and dataproducts + obs_st = create_subtask_object_for_testing('observation', 'finished') + obs_out1 = models.SubtaskOutput.objects.create(**SubtaskOutput_test_data(subtask=obs_st)) + obs_out2 = models.SubtaskOutput.objects.create(**SubtaskOutput_test_data(subtask=obs_st)) + + # create connected pipeline subtask and inputs, specify input filtering + pipe_st = create_subtask_object_for_testing('pipeline', 'defined') + pipe_out = models.SubtaskOutput.objects.create(**SubtaskOutput_test_data(subtask=pipe_st)) # required by scheduling function + pipe_in1 = models.SubtaskInput.objects.create(**SubtaskInput_test_data(subtask=pipe_st, producer=obs_out1, selection_doc={'sap': [0]})) + pipe_in2 = models.SubtaskInput.objects.create(**SubtaskInput_test_data(subtask=pipe_st, producer=obs_out2, selection_doc={'sap': [1]})) + + # create obs output dataproducts with specs we can filter on + dp1_1 = models.Dataproduct.objects.create(**Dataproduct_test_data(producer=obs_out1, specifications_doc={'sap': [0]})) + dp1_2 = models.Dataproduct.objects.create(**Dataproduct_test_data(producer=obs_out1, specifications_doc={'sap': [1]})) + dp1_3 = models.Dataproduct.objects.create(**Dataproduct_test_data(producer=obs_out1, specifications_doc={'sap': [0]})) + + dp2_1 = models.Dataproduct.objects.create(**Dataproduct_test_data(producer=obs_out2, specifications_doc={'sap': [0]})) + dp2_2 = models.Dataproduct.objects.create(**Dataproduct_test_data(producer=obs_out2, specifications_doc={'sap': [1]})) + + # uncomment when RA scheduler works + # # trigger: + # # schedule pipeline, which should attach the correct subset of dataproducts to the pipeline inputs + # schedule_pipeline_subtask(pipe_st) + # + # # assert: + # # check correct input filtering + # self.assertEqual(set(pipe_in1.dataproducts.all()), {dp1_1, dp1_3}) + # self.assertEqual(set(pipe_in2.dataproducts.all()), {dp2_2}) + + if __name__ == "__main__": os.environ['TZ'] = 'UTC' unittest.main() diff --git a/SAS/TMSS/test/t_subtasks.py b/SAS/TMSS/test/t_subtasks.py index 32de3e5628b7aa8acd8fd07cddb758786f8a8676..2421ab66d1b5817adb87df5c902b637cbf500007 100755 --- a/SAS/TMSS/test/t_subtasks.py +++ b/SAS/TMSS/test/t_subtasks.py @@ -41,48 +41,88 @@ from lofar.sas.tmss.tmss.tmssapp import models from lofar.sas.tmss.tmss.tmssapp.subtasks import * -# The following methods should be tested too - -# schedule_subtask -# check_prerequities_for_scheduling -# schedule_qafile_subtask -# schedule_qaplots_subtask -# schedule_observation_subtask -# schedule_pipeline_subtask -# create_and_schedule_subtasks_from_task_blueprint - - - - +def create_subtask_object_for_testing(subtask_type_value, subtask_state_value): + """ + Helper function to create a subtask object for testing with given subtask value and subtask state value + as string (no object) + """ + template_type = models.SubtaskType.objects.get(value=subtask_type_value) + subtask_template_obj = create_subtask_template_for_testing(template_type) + subtask_state_obj = models.SubtaskState.objects.get(value=subtask_state_value) + subtask_data = Subtask_test_data(subtask_template=subtask_template_obj, state=subtask_state_obj) + return models.Subtask.objects.create(**subtask_data) + + +def create_subtask_template_for_testing(template_type: object): + """ + Helper function + :param template_type: + :return: + """ + subtask_template_data = SubtaskTemplate_test_data() + subtask_template_data['type'] = template_type + return models.SubtaskTemplate.objects.create(**subtask_template_data) + + +def create_task_blueprint_object_for_testing(task_template_name="observation schema", QA_enabled=False): + """ + Helper function to create a task blueprint object for testing with given task template name value + as string (no object) + :param task_template_name: (Optional) name of schema observation schema is target observation + :param QA_enabled: (Optional) QA plots and file_conversion + :return: task_blueprint_obj: Created Task Blueprint object + """ + task_template = models.TaskTemplate.objects.get(name=task_template_name) + task_spec = get_default_json_object_for_schema(task_template.schema) + if 'QA' in task_spec: + task_spec["QA"]['plots']['enabled'] = QA_enabled + task_spec["QA"]['file_conversion']['enabled'] = QA_enabled + task_draft_data = TaskDraft_test_data(specifications_template=task_template, specifications_doc=task_spec) + task_draft_obj = models.TaskDraft.objects.create(**task_draft_data) + + task_name = "BlueprintTask with %s" % task_template_name + task_blueprint_data = TaskBlueprint_test_data(name=task_name, task_draft=task_draft_obj) + task_blueprint_obj = models.TaskBlueprint.objects.create(**task_blueprint_data) + return task_blueprint_obj + + +def create_relation_task_blueprint_object_for_testing(blueprint_task_producer, blueprint_task_consumer): + """ + Helper function to create a task relation blueprint object for testing for given task objects consumer and producer + :param blueprint_task_producer: Blueprint task of producer, typical an observation + :param blueprint_task_consumer: Blueprint task of consumer, typical a preprocessing pipeline + :return: task_relation_obj: Created Task Relation Blueprint object + """ + task_relation_data = TaskRelationBlueprint_test_data(blueprint_task_producer, blueprint_task_consumer) + task_relation_obj = models.TaskRelationBlueprint.objects.create(**task_relation_data) + return task_relation_obj + + +def create_scheduling_relation_task_blueprint_for_testing(first_task_blueprint, second_task_blueprint): + """ + Helper function to create a task blueprint relation object between two task blueprint (calibrator and target observation) + :param first_task_blueprint: + :param second_task_blueprint: + :return: task_relation_blueprint_obj: Created Task Relation Blueprint object + """ + task_scheduling_rel_obj = models.TaskSchedulingRelationBlueprint.objects.create( + tags=[], + first=first_task_blueprint, + second=second_task_blueprint, + placement=models.SchedulingRelationPlacement.objects.get(value='before'), + time_offset=60) + return task_scheduling_rel_obj class SubTasksCreationFromSubTask(unittest.TestCase): - @staticmethod - def create_subtask_object(subtask_type_value, subtask_state_value): - """ - Helper function to create a subtask object for testing with given subtask value and subtask state value - as string (no object) - """ - template_type = models.SubtaskType.objects.get(value=subtask_type_value) - subtask_template_obj = SubTasksCreationFromSubTask.create_subtask_template(template_type) - subtask_state_obj = models.SubtaskState.objects.get(value=subtask_state_value) - subtask_data = Subtask_test_data(subtask_template=subtask_template_obj, state=subtask_state_obj) - return models.Subtask.objects.create(**subtask_data) - - @staticmethod - def create_subtask_template(template_type: object): - subtask_template_data = SubtaskTemplate_test_data() - subtask_template_data['type'] = template_type - return models.SubtaskTemplate.objects.create(**subtask_template_data) - def test_create_qafile_subtask_from_observation_subtask_failed(self): """ Test if creation of subtask qafile failed due to wrong state or wrong type of the predecessor subtask Correct state should be 'defined' and correct type should be 'observation' (for this test of course it is not) """ - subtasks = [self.create_subtask_object("pipeline", "defined"), - self.create_subtask_object("observation", "defining"), - self.create_subtask_object("observation", "defining") ] + subtasks = [create_subtask_object_for_testing("pipeline", "defined"), + create_subtask_object_for_testing("observation", "defining"), + create_subtask_object_for_testing("observation", "defining") ] for subtask in subtasks: with self.assertRaises(ValueError): create_qafile_subtask_from_observation_subtask(subtask) @@ -90,11 +130,10 @@ class SubTasksCreationFromSubTask(unittest.TestCase): def test_create_qafile_subtask_from_observation_subtask_succeed(self): """ Test if creation of subtask qafile succeed - Check if the created subtask has correct subtask state and value (TODO) + Subtask object is None because QA file conversion is by default not enabled!!!! """ - predecessor_subtask = self.create_subtask_object("observation", "defined") + predecessor_subtask = create_subtask_object_for_testing("observation", "defined") subtask = create_qafile_subtask_from_observation_subtask(predecessor_subtask) - # subtask object is None because QA file conversion is by default not enabled!!!! self.assertEqual(None, subtask) def test_create_qaplots_subtask_from_qafile_subtask_failed(self): @@ -102,9 +141,9 @@ class SubTasksCreationFromSubTask(unittest.TestCase): Test if creation of subtask qaplots failed due to wrong state or wrong type of the predecessor subtask Correct type should be 'qa_files' (for this test of course it is not) """ - subtasks = [self.create_subtask_object("pipeline", "defined"), - self.create_subtask_object("observation", "defining"), - self.create_subtask_object("observation", "defining") ] + subtasks = [create_subtask_object_for_testing("pipeline", "defined"), + create_subtask_object_for_testing("observation", "defining"), + create_subtask_object_for_testing("observation", "defining") ] for subtask in subtasks: with self.assertRaises(ValueError): create_qaplots_subtask_from_qafile_subtask(subtask) @@ -112,41 +151,21 @@ class SubTasksCreationFromSubTask(unittest.TestCase): def test_create_qaplots_subtask_from_qafile_subtask_succeed(self): """ Test if creation of subtask qaplots succeed - Check if the created subtask has correct subtask state and value (TODO) + Subtask object is None because QA plots is by default not enabled!!!! """ - predecessor_subtask = self.create_subtask_object("qa_files", "defined") + predecessor_subtask = create_subtask_object_for_testing("qa_files", "defined") subtask = create_qaplots_subtask_from_qafile_subtask(predecessor_subtask) - # subtask object is None because QA plots is by default not enabled!!!! self.assertEqual(None, subtask) class SubTasksCreationFromTaskBluePrint(unittest.TestCase): - @staticmethod - def create_task_blueprint_object(task_template_name="observation schema", QA_enabled=False): - """ - Helper function to create a task blueprint object for testing with given task template name value - as string (no object) - """ - task_template = TaskTemplate.objects.get(name=task_template_name) - task_spec = get_default_json_object_for_schema(task_template.schema) - if 'QA' in task_spec: - task_spec["QA"]['plots']['enabled'] = QA_enabled - task_spec["QA"]['file_conversion']['enabled'] = QA_enabled - - task_draft_data = TaskDraft_test_data(specifications_template=task_template, specifications_doc=task_spec) - task_draft_obj = models.TaskDraft.objects.create(**task_draft_data) - - task_blueprint_data = TaskBlueprint_test_data(task_draft=task_draft_obj) - task_blueprint_obj = models.TaskBlueprint.objects.create(**task_blueprint_data) - return task_blueprint_obj - def test_create_sequence_of_subtask_from_task_blueprint(self): """ Create multiple subtasks from a task blueprint, executed in correct order. No exception should occur, check name, type and state of the subtask """ - task_blueprint = self.create_task_blueprint_object() + task_blueprint = create_task_blueprint_object_for_testing() subtask = create_observation_control_subtask_from_task_blueprint(task_blueprint) self.assertEqual("defined", str(subtask.state)) @@ -163,7 +182,6 @@ class SubTasksCreationFromTaskBluePrint(unittest.TestCase): with self.assertRaises(SubtaskCreationException): subtask = create_qaplots_subtask_from_task_blueprint(task_blueprint) - # subtask = create_preprocessing_subtask_from_task_blueprint(task_blueprint) def test_create_sequence_of_subtask_from_task_blueprint_with_QA_enabled(self): """ @@ -171,9 +189,9 @@ class SubTasksCreationFromTaskBluePrint(unittest.TestCase): QA plots and QA file conversion enabled No exception should occur, check name, type and state of the subtasks """ - # Enable QA plot and QA conversion - task_blueprint = self.create_task_blueprint_object(QA_enabled=True) - task_blueprint_preprocessing = self.create_task_blueprint_object("preprocessing schema") + # Create Observation Task Enable QA plot and QA conversion + task_blueprint = create_task_blueprint_object_for_testing(QA_enabled=True) + task_blueprint_preprocessing = create_task_blueprint_object_for_testing("preprocessing schema") subtask = create_observation_control_subtask_from_task_blueprint(task_blueprint) self.assertEqual("defined", str(subtask.state)) @@ -189,24 +207,63 @@ class SubTasksCreationFromTaskBluePrint(unittest.TestCase): self.assertEqual("defined", str(subtask.state)) self.assertEqual("QA plots", str(subtask.specifications_template.name)) self.assertEqual("qa_plots", str(subtask.specifications_template.type)) - # TODO: check why next call failed? - #subtask = create_preprocessing_subtask_from_task_blueprint(task_blueprint_preprocessing) - #self.assertEqual("defined", str(subtask.state)) - + # Next call will fail due to missing task relation + with self.assertRaises(SubtaskCreationException): + create_preprocessing_subtask_from_task_blueprint(task_blueprint_preprocessing) + # Create that relation and check again + create_relation_task_blueprint_object_for_testing(task_blueprint, task_blueprint_preprocessing) + subtask = create_preprocessing_subtask_from_task_blueprint(task_blueprint_preprocessing) + self.assertEqual("defined", str(subtask.state)) + self.assertEqual("pipelinecontrol schema", str(subtask.specifications_template.name)) + self.assertEqual("pipeline", str(subtask.specifications_template.type)) def test_create_subtasks_from_task_blueprint_succeed(self): """ """ - task_blueprint = self.create_task_blueprint_object(QA_enabled=True) + task_blueprint = create_task_blueprint_object_for_testing(QA_enabled=True) subtasks = create_subtasks_from_task_blueprint(task_blueprint) self.assertEqual(3, len(subtasks)) -# TODO Test the Schedule calls +class SubTasksCreationFromTaskBluePrintCalibrator(unittest.TestCase): -class SubtaskInputSelectionFilteringTest(unittest.TestCase): + def test_create_sequence_of_subtask_from_task_blueprint_calibrator_failure(self): + """ + Create multiple subtasks from a task blueprint when task is a calibrator + Check that exception should occur due too missing related target observation + """ + task_blueprint = create_task_blueprint_object_for_testing(task_template_name="calibrator schema") + with self.assertRaises(SubtaskCreationException): + create_observation_control_subtask_from_task_blueprint(task_blueprint) + + def test_create_sequence_of_subtask_from_task_blueprint_calibrator(self): + """ + Create multiple subtasks from a task blueprint when task is a calibrator and is related to task blueprint + of a target observation + Check that exception should occur due too missing pointing setting in target observation, + the calibrator default is AutoSelect=True + Check NO exception, when AutoSelect=False + """ + cal_task_blueprint = create_task_blueprint_object_for_testing(task_template_name="calibrator schema") + target_task_blueprint = create_task_blueprint_object_for_testing() + create_scheduling_relation_task_blueprint_for_testing(cal_task_blueprint, target_task_blueprint) + + with self.assertRaises(SubtaskCreationException): + create_observation_control_subtask_from_task_blueprint(cal_task_blueprint) + + cal_task_blueprint.specifications_doc['autoselect'] = False + cal_task_blueprint.specifications_doc['pointing']['angle1'] = 11.11 + cal_task_blueprint.specifications_doc['pointing']['angle2'] = 22.22 + subtask = create_observation_control_subtask_from_task_blueprint(cal_task_blueprint) + self.assertEqual("defined", str(subtask.state)) + self.assertEqual("observationcontrol schema", str(subtask.specifications_template.name)) + self.assertEqual("observation", str(subtask.specifications_template.type)) + self.assertEqual('J2000', subtask.specifications_doc['stations']['analog_pointing']['direction_type']) + self.assertEqual(11.11, subtask.specifications_doc['stations']['analog_pointing']['angle1']) + self.assertEqual(22.22, subtask.specifications_doc['stations']['analog_pointing']['angle2']) - # todo: merge in tests from TMSS-207 and deduplicate staticmethods + +class SubtaskInputSelectionFilteringTest(unittest.TestCase): def setUp(self) -> None: # make sure we're allowed to schedule @@ -214,24 +271,6 @@ class SubtaskInputSelectionFilteringTest(unittest.TestCase): setting.value = True setting.save() - @staticmethod - def create_subtask_object(subtask_type_value, subtask_state_value): - """ - Helper function to create a subtask object for testing with given subtask value and subtask state value - as string (no object) - """ - template_type = models.SubtaskType.objects.get(value=subtask_type_value) - subtask_template_obj = SubtaskInputSelectionFilteringTest.create_subtask_template(template_type) - subtask_state_obj = models.SubtaskState.objects.get(value=subtask_state_value) - subtask_data = Subtask_test_data(subtask_template=subtask_template_obj, state=subtask_state_obj) - return models.Subtask.objects.create(**subtask_data) - - @staticmethod - def create_subtask_template(template_type: object): - subtask_template_data = SubtaskTemplate_test_data() - subtask_template_data['type'] = template_type - return models.SubtaskTemplate.objects.create(**subtask_template_data) - def test_specifications_doc_meets_selection_doc_returns_true_on_empty_filter(self): specs = {} selection = {} @@ -269,38 +308,6 @@ class SubtaskInputSelectionFilteringTest(unittest.TestCase): selection = {'sap': [0], 'is_relevant': True} self.assertFalse(specifications_doc_meets_selection_doc(specs, selection)) - # TODO: move to t_scheduling, cause during scheduling the output dataproducts are assigned - # def test_schedule_pipeline_subtask_filters_predecessor_output_dataproducts_for_input(self): - # - # # setup: - # # create observation subtask and outputs and dataproducts - # obs_st = self.create_subtask_object('observation', 'finished') - # obs_out1 = models.SubtaskOutput.objects.create(**SubtaskOutput_test_data(subtask=obs_st)) - # obs_out2 = models.SubtaskOutput.objects.create(**SubtaskOutput_test_data(subtask=obs_st)) - # - # # create connected pipeline subtask and inputs, specify input filtering - # pipe_st = self.create_subtask_object('pipeline', 'defined') - # pipe_out = models.SubtaskOutput.objects.create(**SubtaskOutput_test_data(subtask=pipe_st)) # required by scheduling function - # pipe_in1 = models.SubtaskInput.objects.create(**SubtaskInput_test_data(subtask=pipe_st, producer=obs_out1, selection_doc={'sap': [0]})) - # pipe_in2 = models.SubtaskInput.objects.create(**SubtaskInput_test_data(subtask=pipe_st, producer=obs_out2, selection_doc={'sap': [1]})) - # - # # create obs output dataproducts with specs we can filter on - # dp1_1 = models.Dataproduct.objects.create(**Dataproduct_test_data(producer=obs_out1, specifications_doc={'sap': [0]})) - # dp1_2 = models.Dataproduct.objects.create(**Dataproduct_test_data(producer=obs_out1, specifications_doc={'sap': [1]})) - # dp1_3 = models.Dataproduct.objects.create(**Dataproduct_test_data(producer=obs_out1, specifications_doc={'sap': [0]})) - # - # dp2_1 = models.Dataproduct.objects.create(**Dataproduct_test_data(producer=obs_out2, specifications_doc={'sap': [0]})) - # dp2_2 = models.Dataproduct.objects.create(**Dataproduct_test_data(producer=obs_out2, specifications_doc={'sap': [1]})) - # - # # trigger: - # # schedule pipeline, which should attach the correct subset of dataproducts to the pipeline inputs - # schedule_pipeline_subtask(pipe_st) - # - # # assert: - # # check correct input filtering - # self.assertEqual(set(pipe_in1.dataproducts.all()), {dp1_1, dp1_3}) - # self.assertEqual(set(pipe_in2.dataproducts.all()), {dp2_2}) - class SettingTest(unittest.TestCase): @@ -308,11 +315,12 @@ class SettingTest(unittest.TestCase): setting = Setting.objects.get(name='allow_scheduling_observations') setting.value = False setting.save() - obs_st = SubtaskInputSelectionFilteringTest.create_subtask_object('observation', 'defined') + obs_st = create_subtask_object_for_testing('observation', 'defined') with self.assertRaises(SubtaskSchedulingException): schedule_observation_subtask(obs_st) + if __name__ == "__main__": os.environ['TZ'] = 'UTC' unittest.main() diff --git a/SAS/TMSS/test/t_tasks.py b/SAS/TMSS/test/t_tasks.py index 05cb7e958324c029e95a2655d73ec3006af0330b..d9f6c1b2a79eb78f03173fa38006b2c197bfde26 100755 --- a/SAS/TMSS/test/t_tasks.py +++ b/SAS/TMSS/test/t_tasks.py @@ -106,8 +106,8 @@ class CreationFromSchedulingUnitDraft(unittest.TestCase): def test_create_task_drafts_from_scheduling_unit_draft_with_UC1_requirements(self): """ Create Scheduling Unit Draft with requirements_doc (read from file) + Create Task Blueprints (only) Check if tasks (7) are created - Check with REST-call if tasks are created """ working_dir = os.path.dirname(os.path.abspath(__file__)) with open(os.path.join(working_dir, "testdata/example_UC1_scheduling_unit.json")) as json_file: @@ -128,12 +128,18 @@ class CreationFromSchedulingUnitDraft(unittest.TestCase): task_drafts = scheduling_unit_draft.task_drafts.all() self.assertEqual(7, len(task_drafts)) - - def test_create_task_blueprints_and_subtasks_subtasks_from_scheduling_unit_draft_with_UC1_requirements(self): + def test_create_task_blueprints_and_subtasks_from_scheduling_unit_draft_with_UC1_requirements(self): """ Create Scheduling Unit Draft with requirements_doc (read from file) - Check if tasks (7) are created - Check with REST-call if tasks are created + Create Task Blueprints and Subtasks + Check if tasks (7) are created: + Calibration 1 : 1 Observation and 1 Pipeline task + Target Observation: 1 Observation and 2 Pipeline tasks + Calibration 2 : 1 Observation and 1 Pipeline task + Check if subtasks (13) are created: + Every Observation Task: 3 subtasks (1 control, 2 QA) + Every Pipeline Task: 1 subtasks (1 control) + makes 3x3 + 4x1 = 13 """ working_dir = os.path.dirname(os.path.abspath(__file__)) with open(os.path.join(working_dir, "testdata/example_UC1_scheduling_unit.json")) as json_file: @@ -160,8 +166,12 @@ class CreationFromSchedulingUnitDraft(unittest.TestCase): scheduling_unit_blueprint = scheduling_unit_blueprints[0] task_blueprints = scheduling_unit_blueprint.task_blueprints.all() self.assertEqual(7, len(task_blueprints)) + total_subtasks = 0 + for task_blueprint in task_blueprints: + total_subtasks += task_blueprint.subtasks.count() + self.assertEqual(13, total_subtasks) - def test_create_task_blueprints_and_subtasks_subtasks_from_scheduling_unit_draft(self): + def test_create_task_blueprints_and_subtasks_from_scheduling_unit_draft(self): """ Create Scheduling Unit Draft Check if the name draft (specified) is equal to name blueprint (created) @@ -184,7 +194,8 @@ class CreationFromSchedulingUnitBluePrint(unittest.TestCase): def test_create_task_blueprints_and_subtasks_from_scheduling_unit_blueprint(self): """ Create Scheduling Unit BluePrint - Check with REST-call if NO tasks are created + Check with REST-call if NO tasks are created, an Exception is raised becaus the requirements_doc of the + scheduling_unit (draft) has no tasks defined, it is an empty list """ scheduling_unit_blueprint_data = SchedulingUnitBlueprint_test_data(name="Test Scheduling Unit BluePrint") scheduling_unit_blueprint = models.SchedulingUnitBlueprint.objects.create(**scheduling_unit_blueprint_data) @@ -205,15 +216,11 @@ class CreationFromTaskDraft(unittest.TestCase): def create_task_object(task_draft_name): """ Helper function to create a task object for testing - TODO change schema to observation schema, correlator schema wil be removed - using the observation schema results in jsonschema.exceptions.ValidationError: 'stations' is a required property - so somehow its does not fill in the required fields ?? """ obs_task_template = models.TaskTemplate.objects.get(name='observation schema') task_draft_data = TaskDraft_test_data(name=task_draft_name, specifications_template=obs_task_template) models.TaskDraft.objects.create(**task_draft_data) - def test_create_task_blueprint_and_subtasks(self): """ Create task draft diff --git a/SAS/TMSS/test/t_tmssapp_scheduling_REST_API.py b/SAS/TMSS/test/t_tmssapp_scheduling_REST_API.py index 132b434cc424099d19172cd131f9e27184b6bb81..ec07eacc9f05774a3491beb36498369a819c9843 100755 --- a/SAS/TMSS/test/t_tmssapp_scheduling_REST_API.py +++ b/SAS/TMSS/test/t_tmssapp_scheduling_REST_API.py @@ -22,10 +22,10 @@ # This functional test talks to the API like a regular user would. # It is supposed to cover all REST http methods for all ViewSets. -# todo: I am still a bit under the impression that we re-test Django functionality that we can expect to just work -# todo: with some of these tests. On the other hand a lot of these provide us a nice basis for differentiating out -# todo: behavior in a controlled way. -# todo: We should probably also fully test behavior wrt mandatory and nullable fields. +# I am still a bit under the impression that we re-test Django functionality that we can expect to just work +# with some of these tests. On the other hand a lot of these provide us a nice basis for differentiating out +# behavior in a controlled way. +# We should probably also fully test behavior wrt mandatory and nullable fields. from datetime import datetime, timedelta import unittest @@ -92,7 +92,7 @@ class SubtaskTemplateTestCase(unittest.TestCase): url = r_dict['url'] GET_OK_and_assert_equal_expected_response(self, url, st_test_data) - test_patch = {"type": BASE_URL + '/subtask_type/inspection/', + test_patch = {"type": BASE_URL + '/subtask_type/inspection', "version": 'v6.28318530718', "schema": {"mykey": "my better value"}, } @@ -120,7 +120,7 @@ class SubtaskTemplateTestCase(unittest.TestCase): # create dependency that is safe to delete (enums are not populated / re-established between tests) type_data = {'value': 'kickme'} POST_and_assert_expected_response(self, BASE_URL + '/subtask_type/', type_data, 201, type_data) - type_url = BASE_URL + '/subtask_type/kickme/' + type_url = BASE_URL + '/subtask_type/kickme' # POST new item and verify test_data = dict(st_test_data) @@ -370,7 +370,7 @@ class SubtaskTestCase(unittest.TestCase): # create dependency that is safe to delete (enums are not populated / re-established between tests) state_data = {'value': 'kickme'} POST_and_assert_expected_response(self, BASE_URL + '/subtask_state/', state_data, 201, state_data) - state_url = BASE_URL + '/subtask_state/kickme/' + state_url = BASE_URL + '/subtask_state/kickme' # POST new item and verify test_data = dict(st_test_data) @@ -473,7 +473,7 @@ class SubtaskTestCase(unittest.TestCase): GET_OK_and_assert_equal_expected_response(self, BASE_URL + '/subtask_state_log/?subtask=' + identifier, {"count": 1}) # PATCH item with state update and verify log record is created - test_patch = {"state": BASE_URL + "/subtask_state/finishing/"} + test_patch = {"state": BASE_URL + "/subtask_state/finishing"} PATCH_and_assert_expected_response(self, url, test_patch, 200, test_patch) GET_OK_and_assert_equal_expected_response(self, BASE_URL + '/subtask_state_log/?subtask=' + identifier, {"count": 2}) @@ -553,7 +553,7 @@ class DataproductTestCase(unittest.TestCase): # create dependency that is safe to delete (enums are not populated / re-established between tests) dataformat_data = {'value': 'kickme'} POST_and_assert_expected_response(self, BASE_URL + '/dataformat/', dataformat_data, 201, dataformat_data) - dataformat_url = BASE_URL + '/dataformat/kickme/' + dataformat_url = BASE_URL + '/dataformat/kickme' # POST new item and verify test_data = dict(dp_test_data) @@ -934,7 +934,7 @@ class AntennaSetTestCase(unittest.TestCase): GET_OK_and_assert_equal_expected_response(self, url, antennaset_test_data) test_patch = {"rcus": [11, 12, 13, 14, 15], - "station_type": BASE_URL + '/station_type/remote/'} + "station_type": BASE_URL + '/station_type/remote'} # PATCH item and verify PATCH_and_assert_expected_response(self, url, test_patch, 200, test_patch) @@ -959,7 +959,7 @@ class AntennaSetTestCase(unittest.TestCase): # create dependency that is safe to delete (enums are not populated / re-established between tests) dataformat_data = {'value': 'kickme'} POST_and_assert_expected_response(self, BASE_URL + '/station_type/', dataformat_data, 201, dataformat_data) - dataformat_url = BASE_URL + '/station_type/kickme/' + dataformat_url = BASE_URL + '/station_type/kickme' # POST new item and verify test_data = dict(antennaset_test_data) @@ -1362,7 +1362,7 @@ class DataproductHashTestCase(unittest.TestCase): url = r_dict['url'] GET_OK_and_assert_equal_expected_response(self, url, dph_test_data) - test_patch = {"algorithm": BASE_URL + '/algorithm/aes256/', + test_patch = {"algorithm": BASE_URL + '/algorithm/aes256', "hash": 'bender-was-here'} # PATCH item and verify diff --git a/SAS/TMSS/test/t_tmssapp_specification_REST_API.py b/SAS/TMSS/test/t_tmssapp_specification_REST_API.py index 9e8b6ee34df23d70887d4fa69a865ebc05f32596..90e50eec990177cc5b076e83ec040cda187b9d30 100755 --- a/SAS/TMSS/test/t_tmssapp_specification_REST_API.py +++ b/SAS/TMSS/test/t_tmssapp_specification_REST_API.py @@ -22,10 +22,10 @@ # This functional test talks to the API like a regular user would. # It is supposed to cover all REST http methods for all ViewSets. -# todo: I am still a bit under the impression that we re-test Django functionality that we can expect to just work -# todo: with some of these tests. On the other hand a lot of these provide us a nice basis for differentiating out -# todo: behavior in a controlled way. -# todo: We should probably also fully test behavior wrt mandatory and nullable fields. +# I am still a bit under the impression that we re-test Django functionality that we can expect to just work +# with some of these tests. On the other hand a lot of these provide us a nice basis for differentiating out +# behavior in a controlled way. +# We should probably also fully test behavior wrt mandatory and nullable fields. from datetime import datetime import unittest @@ -125,8 +125,8 @@ class GeneratorTemplateTestCase(unittest.TestCase): test_data_2 = GeneratorTemplate_test_data("test_generator_template_2") id1 = models.GeneratorTemplate.objects.create(**test_data_1).id id2 = models.GeneratorTemplate.objects.create(**test_data_2).id - GET_OK_and_assert_equal_expected_response(self, BASE_URL + '/generator_template/' + str(id1), test_data_1) - GET_OK_and_assert_equal_expected_response(self, BASE_URL + '/generator_template/' + str(id2), test_data_2) + GET_OK_and_assert_equal_expected_response(self, BASE_URL + '/generator_template/' + str(id1) + '/', test_data_1) + GET_OK_and_assert_equal_expected_response(self, BASE_URL + '/generator_template/' + str(id2) + '/', test_data_2) class SchedulingUnitTemplateTestCase(unittest.TestCase): @@ -197,8 +197,8 @@ class SchedulingUnitTemplateTestCase(unittest.TestCase): test_data_2 = SchedulingUnitTemplate_test_data("scheduling_unit_template_2") id1 = models.SchedulingUnitTemplate.objects.create(**test_data_1).id id2 = models.SchedulingUnitTemplate.objects.create(**test_data_2).id - GET_OK_and_assert_equal_expected_response(self, BASE_URL + '/scheduling_unit_template/' + str(id1), test_data_1) - GET_OK_and_assert_equal_expected_response(self, BASE_URL + '/scheduling_unit_template/' + str(id2), test_data_2) + GET_OK_and_assert_equal_expected_response(self, BASE_URL + '/scheduling_unit_template/' + str(id1) + '/', test_data_1) + GET_OK_and_assert_equal_expected_response(self, BASE_URL + '/scheduling_unit_template/' + str(id2) + '/', test_data_2) class TaskTemplateTestCase(unittest.TestCase): @@ -266,8 +266,8 @@ class TaskTemplateTestCase(unittest.TestCase): test_data_2 = TaskTemplate_test_data("task_template_2") id1 = models.TaskTemplate.objects.create(**test_data_1).id id2 = models.TaskTemplate.objects.create(**test_data_2).id - GET_OK_and_assert_equal_expected_response(self, BASE_URL + '/task_template/' + str(id1), test_data_1) - GET_OK_and_assert_equal_expected_response(self, BASE_URL + '/task_template/' + str(id2), test_data_2) + GET_OK_and_assert_equal_expected_response(self, BASE_URL + '/task_template/' + str(id1) + '/', test_data_1) + GET_OK_and_assert_equal_expected_response(self, BASE_URL + '/task_template/' + str(id2) + '/', test_data_2) def test_task_template_PROTECT_behavior_on_type_choice_deleted(self): st_test_data = test_data_creator.TaskTemplate() @@ -360,8 +360,8 @@ class TaskRelationSelectionTemplateTestCase(unittest.TestCase): test_data_2 = TaskRelationSelectionTemplate_test_data("task_relation_selection_template_2") id1 = models.TaskRelationSelectionTemplate.objects.create(**test_data_1).id id2 = models.TaskRelationSelectionTemplate.objects.create(**test_data_2).id - GET_OK_and_assert_equal_expected_response(self, BASE_URL + '/task_relation_selection_template/' + str(id1), test_data_1) - GET_OK_and_assert_equal_expected_response(self, BASE_URL + '/task_relation_selection_template/' + str(id2), test_data_2) + GET_OK_and_assert_equal_expected_response(self, BASE_URL + '/task_relation_selection_template/' + str(id1) + '/', test_data_1) + GET_OK_and_assert_equal_expected_response(self, BASE_URL + '/task_relation_selection_template/' + str(id2) + '/', test_data_2) class TaskConnectorTestCase(unittest.TestCase): @@ -461,9 +461,9 @@ class TaskConnectorTestCase(unittest.TestCase): url = r_dict['url'] GET_OK_and_assert_equal_expected_response(self, url, tc_test_data) - test_patch = {"role": BASE_URL + '/role/calibrator/', - "dataformats": [BASE_URL + '/dataformat/Beamformed/', - BASE_URL + '/dataformat/MeasurementSet/']} + test_patch = {"role": BASE_URL + '/role/calibrator', + "dataformats": [BASE_URL + '/dataformat/Beamformed', + BASE_URL + '/dataformat/MeasurementSet']} # PATCH item and verify PATCH_and_assert_expected_response(self, url, test_patch, 200, test_patch) @@ -512,8 +512,8 @@ class TaskConnectorTestCase(unittest.TestCase): test_data_2 = TaskConnectorType_test_data() id1 = models.TaskConnectorType.objects.create(**test_data_1).id id2 = models.TaskConnectorType.objects.create(**test_data_2).id - GET_OK_and_assert_equal_expected_response(self, BASE_URL + '/task_connector_type/' + str(id1), test_data_1) - GET_OK_and_assert_equal_expected_response(self, BASE_URL + '/task_connector_type/' + str(id2), test_data_2) + GET_OK_and_assert_equal_expected_response(self, BASE_URL + '/task_connector_type/' + str(id1) + '/', test_data_1) + GET_OK_and_assert_equal_expected_response(self, BASE_URL + '/task_connector_type/' + str(id2) + '/', test_data_2) class DefaultTemplates(unittest.TestCase): @@ -709,8 +709,8 @@ class CycleTestCase(unittest.TestCase): test_data_2 = Cycle_test_data() id1 = models.Cycle.objects.create(**test_data_1).name # name is pk id2 = models.Cycle.objects.create(**test_data_2).name # name is pk - GET_OK_and_assert_equal_expected_response(self, BASE_URL + '/cycle/' + str(id1), test_data_1) - GET_OK_and_assert_equal_expected_response(self, BASE_URL + '/cycle/' + str(id2), test_data_2) + GET_OK_and_assert_equal_expected_response(self, BASE_URL + '/cycle/' + str(id1) + '/', test_data_1) + GET_OK_and_assert_equal_expected_response(self, BASE_URL + '/cycle/' + str(id2) + '/', test_data_2) def test_cycle_contains_list_of_releated_projects(self): @@ -725,7 +725,7 @@ class CycleTestCase(unittest.TestCase): project2 = models.Project.objects.create(**project_test_data_2) project2.cycles.set([cycle]) project2.save() - response_data = GET_OK_and_assert_equal_expected_response(self, BASE_URL + '/cycle/' + cycle.name, cycle_test_data_1) + response_data = GET_OK_and_assert_equal_expected_response(self, BASE_URL + '/cycle/' + cycle.name + '/', cycle_test_data_1) assertUrlList(self, response_data['projects'], [project1, project2]) @@ -843,8 +843,8 @@ class ProjectTestCase(unittest.TestCase): test_data_2 = Project_test_data() id1 = models.Project.objects.create(**test_data_1).name # name is pk id2 = models.Project.objects.create(**test_data_2).name - GET_OK_and_assert_equal_expected_response(self, BASE_URL + '/project/' + str(id1), test_data_1) - GET_OK_and_assert_equal_expected_response(self, BASE_URL + '/project/' + str(id2), test_data_2) + GET_OK_and_assert_equal_expected_response(self, BASE_URL + '/project/' + str(id1) + '/', test_data_1) + GET_OK_and_assert_equal_expected_response(self, BASE_URL + '/project/' + str(id2) + '/', test_data_2) def test_nested_projects_are_filtered_according_to_cycle(self): @@ -1074,8 +1074,8 @@ class SchedulingSetTestCase(unittest.TestCase): test_data_2 = SchedulingSet_test_data() id1 = models.SchedulingSet.objects.create(**test_data_1).id id2 = models.SchedulingSet.objects.create(**test_data_2).id - GET_OK_and_assert_equal_expected_response(self, BASE_URL + '/scheduling_set/' + str(id1), test_data_1) - GET_OK_and_assert_equal_expected_response(self, BASE_URL + '/scheduling_set/' + str(id2), test_data_2) + GET_OK_and_assert_equal_expected_response(self, BASE_URL + '/scheduling_set/' + str(id1) + '/', test_data_1) + GET_OK_and_assert_equal_expected_response(self, BASE_URL + '/scheduling_set/' + str(id2) + '/', test_data_2) def test_SchedulingSet_contains_list_of_releated_SchedulingUnitDraft(self): @@ -1087,7 +1087,7 @@ class SchedulingSetTestCase(unittest.TestCase): scheduling_unit_draft_2 = models.SchedulingUnitDraft.objects.create(**SchedulingUnitDraft_test_data("scheduler draft one")) scheduling_unit_draft_2.scheduling_set = scheduling_set scheduling_unit_draft_2.save() - response_data = GET_OK_and_assert_equal_expected_response(self, BASE_URL + '/scheduling_set/%d' % scheduling_set.id, test_data_1) + response_data = GET_OK_and_assert_equal_expected_response(self, BASE_URL + '/scheduling_set/%d/' % scheduling_set.id, test_data_1) assertUrlList(self, response_data['scheduling_unit_drafts'], [scheduling_unit_draft_1, scheduling_unit_draft_2]) diff --git a/SAS/TMSS/test/t_tmssapp_specification_permissions.py b/SAS/TMSS/test/t_tmssapp_specification_permissions.py index 0e8ebd686bd17a53a0746993d73ec7e4127604d6..ad0576a81665b650b63245a3a2f5faff396299fd 100755 --- a/SAS/TMSS/test/t_tmssapp_specification_permissions.py +++ b/SAS/TMSS/test/t_tmssapp_specification_permissions.py @@ -42,7 +42,7 @@ class CyclePermissionTestCase(unittest.TestCase): @classmethod def setUpClass(cls): cls.test_data_creator = TMSSRESTTestDataCreator(BASE_URL, requests.auth.HTTPBasicAuth('paulus', 'pauluspass')) - response = requests.get(cls.test_data_creator.django_api_url, auth=cls.test_data_creator.auth) + response = requests.get(cls.test_data_creator.django_api_url + '/', auth=cls.test_data_creator.auth) cls.support_group = Group.objects.create(name='support') cls.support_group.permissions.add(Permission.objects.get(codename='add_cycle')) diff --git a/SAS/TMSS/test/test_utils.py b/SAS/TMSS/test/test_utils.py index 5f515a7c97a09a2e276570b177517c24b25ea49c..c97fcab50e588742e1aa2705e9542e8d463e7fd7 100644 --- a/SAS/TMSS/test/test_utils.py +++ b/SAS/TMSS/test/test_utils.py @@ -136,12 +136,12 @@ class TMSSDjangoServerInstance(): @property def url(self): ''':returns the http url to the django server''' - return "http://%s/api" % self.address + return "http://%s/api/" % self.address @property def oidc_url(self): ''':returns the http url to the django server''' - return "http://%s/oidc" % self.address + return "http://%s/oidc/" % self.address @property def database_dbcreds_id(self) -> str: @@ -263,6 +263,10 @@ class TMSSTestEnvironment: broker=broker) self.client_credentials = TemporaryCredentials(user=self.ldap_server.dbcreds.user, password=self.ldap_server.dbcreds.password) + # Check for correct Django version, should be at least 3.0 + if django.VERSION[0] < 3: + print("\nWARNING: YOU ARE USING DJANGO VERSION '%s', WHICH WILL NOT SUPPORT ALL FEATURES IN TMSS!\n" % + django.get_version()) def start(self): self.ldap_server.start() @@ -382,4 +386,4 @@ def main_test_environment(): if __name__ == '__main__': - main_test_environment() \ No newline at end of file + main_test_environment() diff --git a/SAS/TMSS/test/tmss_test_data_rest.py b/SAS/TMSS/test/tmss_test_data_rest.py index 765910ff9ab15d675060ac0586ad04d848de4c9f..29351cd62bbe1ba892e1bd316d885563d54e5829 100644 --- a/SAS/TMSS/test/tmss_test_data_rest.py +++ b/SAS/TMSS/test/tmss_test_data_rest.py @@ -29,7 +29,8 @@ from lofar.common.json_utils import get_default_json_object_for_schema class TMSSRESTTestDataCreator(): def __init__(self, django_api_url: str, auth: requests.auth.HTTPBasicAuth): - self.django_api_url = django_api_url + self.django_api_url = django_api_url[:-1] if django_api_url.endswith('/') else django_api_url + self.auth = auth def get_response_as_json_object(self, url): @@ -113,9 +114,9 @@ class TMSSRESTTestDataCreator(): if output_of_url is None: output_of_url = self.post_data_and_get_url(self.TaskTemplate(), '/task_template/') - return {"role": self.django_api_url + '/role/%s/'%role, - "datatype": self.django_api_url + '/datatype/image/', - "dataformats": [self.django_api_url + '/dataformat/Beamformed/'], + return {"role": self.django_api_url + '/role/%s'%role, + "datatype": self.django_api_url + '/datatype/image', + "dataformats": [self.django_api_url + '/dataformat/Beamformed'], "output_of": output_of_url, "input_of": input_of_url, "tags": []} @@ -150,7 +151,7 @@ class TMSSRESTTestDataCreator(): "tags": [], "description": description, "name": 'my_resource_type_' + str(uuid.uuid4()), - "quantity": self.django_api_url + '/quantity/number/' + "quantity": self.django_api_url + '/quantity/number' } @@ -199,7 +200,7 @@ class TMSSRESTTestDataCreator(): "description": "This is my run draft", "tags": [], "requirements_doc": requirements_doc, - "copy_reason": self.django_api_url + '/copy_reason/template/', + "copy_reason": self.django_api_url + '/copy_reason/template', "generator_instance_doc": "{}", "copies": None, "scheduling_set": scheduling_set_url, @@ -218,7 +219,7 @@ class TMSSRESTTestDataCreator(): "description": "This is my task draft", "tags": [], "specifications_doc": "{}", - "copy_reason": self.django_api_url + '/copy_reason/template/', + "copy_reason": self.django_api_url + '/copy_reason/template', "copies": None, "scheduling_unit_draft": scheduling_unit_draft_url, "specifications_template": template_url, @@ -247,7 +248,7 @@ class TMSSRESTTestDataCreator(): return {"tags": [], "selection_doc": "{}", - "dataformat": self.django_api_url + "/dataformat/Beamformed/", + "dataformat": self.django_api_url + "/dataformat/Beamformed", "producer": producer_url, "consumer": consumer_url, "input_role": input_role_url, @@ -318,7 +319,7 @@ class TMSSRESTTestDataCreator(): # test data return {"tags": [], "selection_doc": "{}", - "dataformat": self.django_api_url + '/dataformat/MeasurementSet/', + "dataformat": self.django_api_url + '/dataformat/MeasurementSet', "input_role": input_role_url, "output_role": output_role_url, "draft": draft_url, @@ -334,7 +335,7 @@ class TMSSRESTTestDataCreator(): schema = {} if subtask_type_url is None: - subtask_type_url = self.django_api_url + '/subtask_type/observation/' + subtask_type_url = self.django_api_url + '/subtask_type/observation' return {"type": subtask_type_url, "name": name, @@ -356,7 +357,7 @@ class TMSSRESTTestDataCreator(): return {"tags": [], "first": first_url, "second": second_url, - "placement": self.django_api_url + '/scheduling_relation_placement/%s/'%placement, + "placement": self.django_api_url + '/scheduling_relation_placement/%s'%placement, "time_offset":60} def TaskSchedulingRelationDraft(self, first_url=None, second_url=None, placement="after"): @@ -368,7 +369,7 @@ class TMSSRESTTestDataCreator(): return {"tags": [], "first": first_url, "second": second_url, - "placement": self.django_api_url + '/scheduling_relation_placement/%s/'%placement, + "placement": self.django_api_url + '/scheduling_relation_placement/%s'%placement, "time_offset":60} def DataproductSpecificationsTemplate(self, name="my_DataproductSpecificationsTemplate", version:str=None) -> dict: @@ -421,14 +422,14 @@ class TMSSRESTTestDataCreator(): return {"start_time": datetime.utcnow().isoformat(), "stop_time": datetime.utcnow().isoformat(), - "state": self.django_api_url + '/subtask_state/%s/' % (state,), + "state": self.django_api_url + '/subtask_state/%s' % (state,), "specifications_doc": specifications_doc, "task_blueprint": task_blueprint_url, "specifications_template": specifications_template_url, "tags": ["TMSS", "TESTING"], "do_cancel": datetime.utcnow().isoformat(), "priority": 1, - "schedule_method": self.django_api_url + '/schedule_method/manual/', + "schedule_method": self.django_api_url + '/schedule_method/manual', "cluster": cluster_url} def SubtaskOutput(self, subtask_url=None): @@ -451,7 +452,7 @@ class TMSSRESTTestDataCreator(): return {"filename": filename, "directory": directory, - "dataformat": "%s/dataformat/%s/" % (self.django_api_url, dataformat), + "dataformat": "%s/dataformat/%s" % (self.django_api_url, dataformat), "deleted_since": None, "pinned_since": None, "specifications_doc": "{}", @@ -468,7 +469,7 @@ class TMSSRESTTestDataCreator(): def AntennaSet(self, name="antennaset1"): return {"name": name, "description": 'My one observation', - "station_type": self.django_api_url + '/station_type/core/', + "station_type": self.django_api_url + '/station_type/core', "rcus": [1,2,3,4,5], "inputs": ['input1', 'input2'], "tags": ['tmss', 'testing']} @@ -487,7 +488,7 @@ class TMSSRESTTestDataCreator(): def DataproductHash(self, algorithm_url=None, hash="my_hash", dataproduct_url=None): if algorithm_url is None: - algorithm_url = self.django_api_url + '/algorithm/md5/' + algorithm_url = self.django_api_url + '/algorithm/md5' if dataproduct_url is None: dataproduct_url = self.post_data_and_get_url(self.Dataproduct(), '/dataproduct/') diff --git a/SAS/TMSS/test/tmss_test_environment_unittest_setup.py b/SAS/TMSS/test/tmss_test_environment_unittest_setup.py index 98375bb80e3b66b19320ef3c129d4757f1bbc7b6..d29fbde127a8023ff29ebc6e014b4b3954c87bd5 100644 --- a/SAS/TMSS/test/tmss_test_environment_unittest_setup.py +++ b/SAS/TMSS/test/tmss_test_environment_unittest_setup.py @@ -47,8 +47,8 @@ def tearDownModule(): import json import requests AUTH = requests.auth.HTTPBasicAuth(tmss_test_env.ldap_server.dbcreds.user, tmss_test_env.ldap_server.dbcreds.password) -BASE_URL = tmss_test_env.django_server.url -OIDC_URL = tmss_test_env.django_server.oidc_url +BASE_URL = tmss_test_env.django_server.url[:-1] if tmss_test_env.django_server.url.endswith('/') else tmss_test_env.django_server.url +OIDC_URL = tmss_test_env.django_server.oidc_url[:-1] if tmss_test_env.django_server.oidc_url.endswith('/') else tmss_test_env.django_server.oidc_url from lofar.sas.tmss.test.test_utils import assertDataWithUrls import lofar.sas.tmss.tmss.settings as TMSS_SETTINGS diff --git a/SAS/TriggerEmailService/Server/bin/TriggerEmailService b/SAS/TriggerEmailService/Server/bin/TriggerEmailService old mode 100755 new mode 100644 diff --git a/SAS/TriggerEmailService/Server/test/t_TriggerEmailService.py b/SAS/TriggerEmailService/Server/test/t_TriggerEmailService.py old mode 100755 new mode 100644