diff --git a/SAS/TriggerEmailService/CMakeLists.txt b/SAS/TMSS/TriggerEmailService/CMakeLists.txt similarity index 100% rename from SAS/TriggerEmailService/CMakeLists.txt rename to SAS/TMSS/TriggerEmailService/CMakeLists.txt diff --git a/SAS/TriggerEmailService/Common/CMakeLists.txt b/SAS/TMSS/TriggerEmailService/Common/CMakeLists.txt similarity index 100% rename from SAS/TriggerEmailService/Common/CMakeLists.txt rename to SAS/TMSS/TriggerEmailService/Common/CMakeLists.txt diff --git a/SAS/TriggerEmailService/Common/__init__.py b/SAS/TMSS/TriggerEmailService/Common/__init__.py similarity index 100% rename from SAS/TriggerEmailService/Common/__init__.py rename to SAS/TMSS/TriggerEmailService/Common/__init__.py diff --git a/SAS/TriggerEmailService/Common/config.py b/SAS/TMSS/TriggerEmailService/Common/config.py similarity index 100% rename from SAS/TriggerEmailService/Common/config.py rename to SAS/TMSS/TriggerEmailService/Common/config.py diff --git a/SAS/TriggerEmailService/Server/CMakeLists.txt b/SAS/TMSS/TriggerEmailService/Server/CMakeLists.txt similarity index 100% rename from SAS/TriggerEmailService/Server/CMakeLists.txt rename to SAS/TMSS/TriggerEmailService/Server/CMakeLists.txt diff --git a/SAS/TriggerEmailService/Server/bin/CMakeLists.txt b/SAS/TMSS/TriggerEmailService/Server/bin/CMakeLists.txt similarity index 100% rename from SAS/TriggerEmailService/Server/bin/CMakeLists.txt rename to SAS/TMSS/TriggerEmailService/Server/bin/CMakeLists.txt diff --git a/SAS/TriggerEmailService/Server/bin/TriggerEmailService b/SAS/TMSS/TriggerEmailService/Server/bin/TriggerEmailService old mode 100755 new mode 100644 similarity index 100% rename from SAS/TriggerEmailService/Server/bin/TriggerEmailService rename to SAS/TMSS/TriggerEmailService/Server/bin/TriggerEmailService diff --git a/SAS/TriggerEmailService/Server/bin/TriggerEmailService.ini b/SAS/TMSS/TriggerEmailService/Server/bin/TriggerEmailService.ini similarity index 100% rename from SAS/TriggerEmailService/Server/bin/TriggerEmailService.ini rename to SAS/TMSS/TriggerEmailService/Server/bin/TriggerEmailService.ini diff --git a/SAS/TriggerEmailService/Server/lib/CMakeLists.txt b/SAS/TMSS/TriggerEmailService/Server/lib/CMakeLists.txt similarity index 100% rename from SAS/TriggerEmailService/Server/lib/CMakeLists.txt rename to SAS/TMSS/TriggerEmailService/Server/lib/CMakeLists.txt diff --git a/SAS/TriggerEmailService/Server/lib/Templates.py b/SAS/TMSS/TriggerEmailService/Server/lib/Templates.py similarity index 100% rename from SAS/TriggerEmailService/Server/lib/Templates.py rename to SAS/TMSS/TriggerEmailService/Server/lib/Templates.py diff --git a/SAS/TriggerEmailService/Server/lib/TriggerEmailService.py b/SAS/TMSS/TriggerEmailService/Server/lib/TriggerEmailService.py similarity index 100% rename from SAS/TriggerEmailService/Server/lib/TriggerEmailService.py rename to SAS/TMSS/TriggerEmailService/Server/lib/TriggerEmailService.py diff --git a/SAS/TriggerEmailService/Server/lib/__init__.py b/SAS/TMSS/TriggerEmailService/Server/lib/__init__.py similarity index 100% rename from SAS/TriggerEmailService/Server/lib/__init__.py rename to SAS/TMSS/TriggerEmailService/Server/lib/__init__.py diff --git a/SAS/TriggerEmailService/Server/test/CMakeLists.txt b/SAS/TMSS/TriggerEmailService/Server/test/CMakeLists.txt similarity index 100% rename from SAS/TriggerEmailService/Server/test/CMakeLists.txt rename to SAS/TMSS/TriggerEmailService/Server/test/CMakeLists.txt diff --git a/SAS/TriggerEmailService/Server/test/t_TriggerEmailService.py b/SAS/TMSS/TriggerEmailService/Server/test/t_TriggerEmailService.py old mode 100755 new mode 100644 similarity index 100% rename from SAS/TriggerEmailService/Server/test/t_TriggerEmailService.py rename to SAS/TMSS/TriggerEmailService/Server/test/t_TriggerEmailService.py diff --git a/SAS/TriggerEmailService/Server/test/t_TriggerEmailService.run b/SAS/TMSS/TriggerEmailService/Server/test/t_TriggerEmailService.run old mode 100755 new mode 100644 similarity index 100% rename from SAS/TriggerEmailService/Server/test/t_TriggerEmailService.run rename to SAS/TMSS/TriggerEmailService/Server/test/t_TriggerEmailService.run diff --git a/SAS/TriggerEmailService/Server/test/t_TriggerEmailService.sh b/SAS/TMSS/TriggerEmailService/Server/test/t_TriggerEmailService.sh old mode 100755 new mode 100644 similarity index 100% rename from SAS/TriggerEmailService/Server/test/t_TriggerEmailService.sh rename to SAS/TMSS/TriggerEmailService/Server/test/t_TriggerEmailService.sh diff --git a/SAS/TriggerEmailService/__init__.py b/SAS/TMSS/TriggerEmailService/__init__.py similarity index 100% rename from SAS/TriggerEmailService/__init__.py rename to SAS/TMSS/TriggerEmailService/__init__.py diff --git a/SAS/TriggerEmailService/doc/package.dox b/SAS/TMSS/TriggerEmailService/doc/package.dox similarity index 100% rename from SAS/TriggerEmailService/doc/package.dox rename to SAS/TMSS/TriggerEmailService/doc/package.dox diff --git a/SAS/TriggerEmailService/doc/trigger_email_service.md b/SAS/TMSS/TriggerEmailService/doc/trigger_email_service.md similarity index 100% rename from SAS/TriggerEmailService/doc/trigger_email_service.md rename to SAS/TMSS/TriggerEmailService/doc/trigger_email_service.md diff --git a/SAS/TMSS/frontend/tmss_webapp/package.json b/SAS/TMSS/frontend/tmss_webapp/package.json index a0e504aa281782ace1177a60ba8ed690cd9e7fbd..a3c942983a1adf1f0b6ab408ded3c9112ae413e6 100644 --- a/SAS/TMSS/frontend/tmss_webapp/package.json +++ b/SAS/TMSS/frontend/tmss_webapp/package.json @@ -29,6 +29,7 @@ "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-table": "^7.2.1", diff --git a/SAS/TMSS/frontend/tmss_webapp/src/App.css b/SAS/TMSS/frontend/tmss_webapp/src/App.css index c2f89a4b90fdaa21a6b3f6fb591b769cab222b8c..6d1f1131b5899107f7cd9aa8889252455cd26e0a 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/App.css +++ b/SAS/TMSS/frontend/tmss_webapp/src/App.css @@ -37,8 +37,19 @@ label { margin-bottom: 10px; } -.main-content span,a{ +h2, .h2 { + font-size: 1.5rem; +} + +h3, .h3 { + font-size: 1.25rem; +} + +a{ margin-bottom: 10px; +} + +.main-content span,a{ font-size: 14px; } @@ -102,8 +113,9 @@ p { margin-bottom: 1rem; } -.fa { +.layout-main .fa { color: #005b9f; + font-size: 20px; } thead { @@ -116,10 +128,24 @@ thead { 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; } diff --git a/SAS/TMSS/frontend/tmss_webapp/src/App.js b/SAS/TMSS/frontend/tmss_webapp/src/App.js index c3a5cba3f6ae1115337d65b80309a939b9421057..4bb9deec4b53a49a6c4b55d251c3d13cf7732b53 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/App.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/App.js @@ -1,24 +1,23 @@ 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 {Dashboard} from './routes/dashboard/dashboard'; +import {AppBreadcrumb } from "./layout/components/AppBreadcrumb"; +import {withRouter } from 'react-router'; import 'primeicons/primeicons.css'; import 'primereact/resources/themes/nova-light/theme.css'; import 'primereact/resources/primereact.css'; import './layout/layout.scss'; +import 'primeflex/primeflex.css'; import './App.scss'; import './App.css'; class App extends Component { - - constructor() { + constructor() { super(); this.state = { layoutMode: 'static', @@ -28,22 +27,23 @@ class App extends Component { overlayMenuActive: false, mobileMenuActive: false, }; - this.onWrapperClick = this.onWrapperClick.bind(this); + 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 = [ + this.menu = [ {label: 'Dashboard', icon: 'pi pi-fw pi-home', to:'/dashboard'}, - {label: 'Scheduling Units', icon: 'pi pi-fw pi-calendar', to:'/scheduling'}, + {label: 'Cycle', icon: 'pi pi-fw pi-spinner', to:'/cycle'}, + {label: 'Scheduling Units', icon: 'pi pi-fw pi-calendar', to:'/schedulingunit'}, {label: 'Tasks', icon: 'pi pi-fw pi-check-square', to:'/task'}, - {label: 'Cycle', icon: 'pi pi-fw pi-spinner', to:'/cycle'} + {label: 'Project', icon: 'fa fa-fw fa-binoculars', to:'/project'} ]; // this.menuComponent = {'Dashboard': Dashboard} } - onWrapperClick(event) { + onWrapperClick(event) { if (!this.menuClick) { this.setState({ overlayMenuActive: false, @@ -56,7 +56,6 @@ class App extends Component { onToggleMenu(event) { this.menuClick = true; - if (this.isDesktop()) { if (this.state.layoutMode === 'overlay') { this.setState({ @@ -75,46 +74,48 @@ class App extends Component { mobileMenuActive: !mobileMenuActive }); } - - event.preventDefault(); + event.preventDefault(); } onSidebarClick(event) { this.menuClick = true; } - onMenuItemClick(event) { - this.setState({currentMenu:event.item.label, currentPath: event.item.path}); - } + + onMenuItemClick(event) { + this.setState({currentMenu:event.item.label, currentPath: event.item.path}); + } - isDesktop() { + isDesktop() { return window.innerWidth > 1024; } render() { - 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 - }); - return ( + 'layout-mobile-sidebar-active': this.state.mobileMenuActive + }); + const AppBreadCrumbWithRouter = withRouter(AppBreadcrumb); + + return ( <React.Fragment> - <div className="App"> + <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 /> - </div> + <AppTopbar onToggleMenu={this.onToggleMenu}></AppTopbar> + <Router basename={ this.state.currentPath }> + <AppMenu model={this.menu} onMenuItemClick={this.onMenuItemClick} /> + <div className="layout-main"> + <AppBreadCrumbWithRouter/> + <RoutedContent /> + </div> </Router> - <AppFooter></AppFooter> - </div> - </div> - </React.Fragment> + <AppFooter></AppFooter> + </div> + </div> + </React.Fragment> ); } } diff --git a/SAS/TMSS/frontend/tmss_webapp/src/__mocks__/cycle.service.data.js b/SAS/TMSS/frontend/tmss_webapp/src/__mocks__/cycle.service.data.js new file mode 100644 index 0000000000000000000000000000000000000000..e69c3c9bd88800a7bf105b5c7f2c0f7dd7b1e42f --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/__mocks__/cycle.service.data.js @@ -0,0 +1,160 @@ +export default{ + getProjects: { + "results": [{ + "name": "TMSS-Commissioning", + "cycles_ids": ["Cycle 14"], + "private_data": true, + "project_category": null, + "project_category_value": null + }] + }, + getCycleQuota: { + "results": [{ + "id": 1, + "url": "http://localhost:3000/api/cycle_quota/1/", + "cycle": "http://localhost:3000/api/cycle/Cycle%2000/", + "cycle_id": "Cycle 00", + "resource_type": "http://localhost:3000/api/resource_type/observing_time/", + "resource_type_id": "observing_time", + "value": 10575360.0 + },{ + "cycle": "http://localhost:3000/api/cycle/Cycle%2000/", + "cycle_id": "Cycle 00", + "id": 5, + "resource_type": "http://localhost:3000/api/resource_type/observing_time_commissioning/", + "resource_type_id": "observing_time_commissioning", + "url": "http://localhost:3000/api/cycle_quota/5/", + "value": 660960 + }] + }, + getAllCycle: { + "results": [{ + "name": "Cycle 00", + "url": "http://localhost:3000/api/cycle/Cycle%2000/", + "created_at": "2020-08-06T12:06:09.074400", + "description": "Lofar Cycle 0", + "duration": 13219200.0, + "projects": [], + "projects_ids": [], + "quota": ["http://localhost:3000/api/cycle_quota/1/", "http://localhost:3000/api/cycle_quota/2/", "http://localhost:3000/api/cycle_quota/3/", "http://localhost:3000/api/cycle_quota/4/", "http://localhost:3000/api/cycle_quota/5/", "http://localhost:3000/api/cycle_quota/6/", "http://localhost:3000/api/cycle_quota/7/"], + "quota_ids": [1, 2, 3, 4, 5, 6, 7], + "start": "2013-06-01T00:00:00", + "stop": "2013-11-01T00:00:00", + "tags": [], + "updated_at": "2020-08-06T12:06:09.074437" + }, { + "name": "Cycle 01", + "url": "http://localhost:3000/api/cycle/Cycle%2001/", + "created_at": "2020-08-06T12:06:09.093253", + "description": "Lofar Cycle 1", + "duration": 18316800.0, + "projects": [], + "projects_ids": [], + "quota": ["http://localhost:3000/api/cycle_quota/8/", "http://localhost:3000/api/cycle_quota/9/", "http://localhost:3000/api/cycle_quota/10/", "http://localhost:3000/api/cycle_quota/11/", "http://localhost:3000/api/cycle_quota/12/", "http://localhost:3000/api/cycle_quota/13/", "http://localhost:3000/api/cycle_quota/14/"], + "quota_ids": [8, 9, 10, 11, 12, 13, 14], + "start": "2013-11-01T00:00:00", + "stop": "2014-06-01T00:00:00", + "tags": [], + "updated_at": "2020-08-06T12:06:09.093283" + }, { + "name": "Cycle 02", + "url": "http://localhost:3000/api/cycle/Cycle%2002/", + "created_at": "2020-08-06T12:06:09.107204", + "description": "Lofar Cycle 2", + "duration": 13219200.0, + "projects": [], + "projects_ids": [], + "quota": ["http://localhost:3000/api/cycle_quota/15/", "http://localhost:3000/api/cycle_quota/16/", "http://localhost:3000/api/cycle_quota/17/", "http://localhost:3000/api/cycle_quota/18/", "http://localhost:3000/api/cycle_quota/19/", "http://localhost:3000/api/cycle_quota/20/", "http://localhost:3000/api/cycle_quota/21/"], + "quota_ids": [15, 16, 17, 18, 19, 20, 21], + "start": "2014-06-01T00:00:00", + "stop": "2014-11-01T00:00:00", + "tags": [], + "updated_at": "2020-08-06T12:06:09.107234" + }] + }, + getresources: { + "results": [{ + "name": "lta_storage", + "url": "http://localhost:3000/api/resource_type/lta_storage/", + "created_at": "2020-08-10T11:33:27.742938", + "description": "Amount of storage in the LTA (in bytes)", + "quantity": "http://localhost:3000/api/quantity/bytes/", + "quantity_value": "bytes", + "tags": [], + "updated_at": "2020-08-10T11:33:27.742956" + }, { + "name": "cep_storage", + "url": "http://localhost:3000/api/resource_type/cep_storage/", + "created_at": "2020-08-10T11:33:27.747482", + "description": "Amount of storage on the CEP processing cluster (in bytes)", + "quantity": "http://localhost:3000/api/quantity/bytes/", + "quantity_value": "bytes", + "tags": [], + "updated_at": "2020-08-10T11:33:27.747498" + }, { + "name": "cep_processing_time", + "url": "http://localhost:3000/api/resource_type/cep_processing_time/", + "created_at": "2020-08-10T11:33:27.751195", + "description": "Processing time on the CEP processing cluster (in seconds)", + "quantity": "http://localhost:3000/api/quantity/time/", + "quantity_value": "time", + "tags": [], + "updated_at": "2020-08-10T11:33:27.751218" + }, { + "name": "observing_time", + "url": "http://localhost:3000/api/resource_type/observing_time/", + "created_at": "2020-08-10T11:33:27.754681", + "description": "Observing time (in seconds)", + "quantity": "http://localhost:3000/api/quantity/time/", + "quantity_value": "time", + "tags": [], + "updated_at": "2020-08-10T11:33:27.754705" + }, { + "name": "observing_time_prio_a", + "url": "http://localhost:3000/api/resource_type/observing_time_prio_a/", + "created_at": "2020-08-10T11:33:27.758486", + "description": "Observing time with priority A (in seconds)", + "quantity": "http://localhost:3000/api/quantity/time/", + "quantity_value": "time", + "tags": [], + "updated_at": "2020-08-10T11:33:27.758511" + }, { + "name": "observing_time_prio_b", + "url": "http://localhost:3000/api/resource_type/observing_time_prio_b/", + "created_at": "2020-08-10T11:33:27.762276", + "description": "Observing time with priority B (in seconds)", + "quantity": "http://localhost:3000/api/quantity/time/", + "quantity_value": "time", + "tags": [], + "updated_at": "2020-08-10T11:33:27.762296" + }, { + "name": "observing_time_commissioning", + "url": "http://localhost:3000/api/resource_type/observing_time_commissioning/", + "created_at": "2020-08-10T11:33:27.765809", + "description": "Observing time for Commissioning/DDT (in seconds)", + "quantity": "http://localhost:3000/api/quantity/time/", + "quantity_value": "time", + "tags": [], + "updated_at": "2020-08-10T11:33:27.765834" + }, { + "name": "support_time", + "url": "http://localhost:3000/api/resource_type/support_time/", + "created_at": "2020-08-10T11:33:27.769402", + "description": "Support time by human (in seconds)", + "quantity": "http://localhost:3000/api/quantity/time/", + "quantity_value": "time", + "tags": [], + "updated_at": "2020-08-10T11:33:27.769427" + }, { + "name": "number_of_triggers", + "url": "http://localhost:3000/api/resource_type/number_of_triggers/", + "created_at": "2020-08-10T11:33:27.773406", + "description": "Number of trigger events (as integer)", + "quantity": "http://localhost:3000/api/quantity/number/", + "quantity_value": "number", + "tags": [], + "updated_at": "2020-08-10T11:33:27.773434" + }] + } +} + 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 index 879092774c7b3dc4795848fda755821be76cf51b..066fd339758c56e64cc51a2ae81142eb978a583d 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/__mocks__/project.service.data.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/__mocks__/project.service.data.js @@ -1,5 +1,7 @@ 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/", @@ -189,7 +191,125 @@ const ProjectServiceMock= { '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/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/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 51e3e785c404eb0517378faafebd259825e057df..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>; 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/_layout.scss b/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_layout.scss index 3021f64976ecae0d2e7eb98650a3e1ac3c904d94..79f22ea107106c4e26e10b2cc375414ea77b293d 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_layout.scss +++ b/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_layout.scss @@ -10,4 +10,5 @@ @import "./_responsive"; @import "./_utils"; @import "./_dashboard"; -@import "./_viewtable"; \ No newline at end of file +@import "./_breadcrumb"; +@import "./_viewtable"; 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/routes/Cycle/CycleList.test.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/CycleList.test.js deleted file mode 100644 index 8cf06d9dc549a63ff22506538a69abe5aa6550f1..0000000000000000000000000000000000000000 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/CycleList.test.js +++ /dev/null @@ -1,143 +0,0 @@ -import "babel-polyfill"; -import React from 'react'; -import { render,fireEvent } from '@testing-library/react'; -import CycleList from './CycleList'; -import UnitConversion from '../../utils/unit.converter'; - -jest.mock('../../services/cycle.service', () => { - return { - getProjects: () => Promise.resolve({ data: mockData.getProjects }), - getCycleQuota: () => Promise.resolve({ data: mockData.getCycleQuota }), - getAllCycle: () => Promise.resolve({ data: mockData.getAllCycle }) - } -}); - -const flushPromises = () => new Promise(setImmediate); - -describe('<CycleList />', () => { - test('render table in the cycle list', async () => { - const { container } = render(<CycleList />); - await flushPromises(); - expect(container.querySelector('table')).toBeInTheDocument(); - }); - - test('render cycle list in row', async () => { - const { container } = render(<CycleList />); - await flushPromises(); - expect(container.querySelectorAll('tr').length).toBe(4); - }); - - test('render columns in the cycle list', async () => { - const { container } = render(<CycleList />); - await flushPromises(); - expect(container.querySelectorAll('th').length).toBe(container.querySelectorAll('th').length); - }); - - test('render cycleId - cycle name conversion', async () => { - const { container } = render(<CycleList />); - await flushPromises(); - expect(container.querySelectorAll('tr')[1].innerHTML.includes('Cycle00')).toBeTruthy(); - }); - - test('render observing time in hours', async () => { - const { container } = render(<CycleList />); - await flushPromises(); - const observing_time = UnitConversion.getUIResourceUnit('seconds',mockData.getCycleQuota.results[0].value); - expect(container.querySelectorAll('tr')[1].innerHTML.includes(observing_time)).toBeTruthy(); - }); - - test('render commissioning time in hours', async () => { - const { container } = render(<CycleList />); - await flushPromises(); - const commissioning_time = UnitConversion.getUIResourceUnit('seconds',mockData.getCycleQuota.results[0].value); - expect(container.querySelectorAll('tr')[1].innerHTML.includes(commissioning_time)).toBeTruthy(); - }); - test('toggle columns in table', async () => { - const { container } = render(<CycleList />); - await flushPromises(); - const panel = container.querySelector('#overlay_panel'); - expect(panel).toHaveStyle('display: block'); - fireEvent.click(container.querySelector('.col-filter-btn')); - await flushPromises(); - expect(panel).toHaveStyle('display: none'); - expect(container.querySelectorAll("input[type=checkbox]:checked").length).toBe(container.querySelectorAll('th').length) - }); - -}); - -const mockData = { - getProjects: { - "results": [{ - "name": "TMSS-Commissioning", - "cycles_ids": ["Cycle 14"], - "private_data": true, - "project_category": null, - "project_category_value": null - }] - }, - getCycleQuota: { - "results": [{ - "id": 1, - "url": "http://localhost:3000/api/cycle_quota/1/", - "cycle": "http://localhost:3000/api/cycle/Cycle%2000/", - "cycle_id": "Cycle 00", - "resource_type": "http://localhost:3000/api/resource_type/observing_time/", - "resource_type_id": "observing_time", - "value": 10575360.0 - },{ - "cycle": "http://localhost:3000/api/cycle/Cycle%2000/", - "cycle_id": "Cycle 00", - "id": 5, - "resource_type": "http://localhost:3000/api/resource_type/observing_time_commissioning/", - "resource_type_id": "observing_time_commissioning", - "url": "http://localhost:3000/api/cycle_quota/5/", - "value": 660960 - }] - }, - getAllCycle: { - "results": [{ - "name": "Cycle 00", - "url": "http://localhost:3000/api/cycle/Cycle%2000/", - "created_at": "2020-08-06T12:06:09.074400", - "description": "Lofar Cycle 0", - "duration": 13219200.0, - "projects": [], - "projects_ids": [], - "quota": ["http://localhost:3000/api/cycle_quota/1/", "http://localhost:3000/api/cycle_quota/2/", "http://localhost:3000/api/cycle_quota/3/", "http://localhost:3000/api/cycle_quota/4/", "http://localhost:3000/api/cycle_quota/5/", "http://localhost:3000/api/cycle_quota/6/", "http://localhost:3000/api/cycle_quota/7/"], - "quota_ids": [1, 2, 3, 4, 5, 6, 7], - "start": "2013-06-01T00:00:00", - "stop": "2013-11-01T00:00:00", - "tags": [], - "updated_at": "2020-08-06T12:06:09.074437" - }, { - "name": "Cycle 01", - "url": "http://localhost:3000/api/cycle/Cycle%2001/", - "created_at": "2020-08-06T12:06:09.093253", - "description": "Lofar Cycle 1", - "duration": 18316800.0, - "projects": [], - "projects_ids": [], - "quota": ["http://localhost:3000/api/cycle_quota/8/", "http://localhost:3000/api/cycle_quota/9/", "http://localhost:3000/api/cycle_quota/10/", "http://localhost:3000/api/cycle_quota/11/", "http://localhost:3000/api/cycle_quota/12/", "http://localhost:3000/api/cycle_quota/13/", "http://localhost:3000/api/cycle_quota/14/"], - "quota_ids": [8, 9, 10, 11, 12, 13, 14], - "start": "2013-11-01T00:00:00", - "stop": "2014-06-01T00:00:00", - "tags": [], - "updated_at": "2020-08-06T12:06:09.093283" - }, { - "name": "Cycle 02", - "url": "http://localhost:3000/api/cycle/Cycle%2002/", - "created_at": "2020-08-06T12:06:09.107204", - "description": "Lofar Cycle 2", - "duration": 13219200.0, - "projects": [], - "projects_ids": [], - "quota": ["http://localhost:3000/api/cycle_quota/15/", "http://localhost:3000/api/cycle_quota/16/", "http://localhost:3000/api/cycle_quota/17/", "http://localhost:3000/api/cycle_quota/18/", "http://localhost:3000/api/cycle_quota/19/", "http://localhost:3000/api/cycle_quota/20/", "http://localhost:3000/api/cycle_quota/21/"], - "quota_ids": [15, 16, 17, 18, 19, 20, 21], - "start": "2014-06-01T00:00:00", - "stop": "2014-11-01T00:00:00", - "tags": [], - "updated_at": "2020-08-06T12:06:09.107234" - }] - } - -} \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/index.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/index.js index d34cb92c2e78f5541348d35422456a31b931dec0..25679b1caca80e9d2eddaa82342370f11b3a5bf9 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/index.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/index.js @@ -1,21 +1,3 @@ -import React, {Component} from 'react'; +import Cyclelist from './list'; -import CycleList from './CycleList' - -export class Cycle extends Component { - constructor(props){ - super(props) - this.state = { - cyclelist: [] -} -} -render() { - return ( - <> - <h2>Cycle List</h2> - <CycleList/> - </> - ); - } -} -export default Cycle; \ No newline at end of file +export {Cyclelist}; \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/CycleList.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/list.js similarity index 59% rename from SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/CycleList.js rename to SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/list.js index 4c05a54c6799f52660c4801440117c6760070cc1..5f02f7f1542a724b000f85762fb0e7ab5c325e27 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/CycleList.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/list.js @@ -1,8 +1,10 @@ import React, { Component } from 'react' import 'primeflex/primeflex.css'; -import ViewTable from './../../components/ViewTable'; +import { Link } from 'react-router-dom/cjs/react-router-dom.min'; +import ViewTable from '../../components/ViewTable'; import CycleService from '../../services/cycle.service'; import UnitConversion from '../../utils/unit.converter'; +import AppLoader from '../../layout/components/AppLoader'; class CycleList extends Component{ constructor(props){ @@ -50,28 +52,28 @@ class CycleList extends Component{ "Lofar Observing Time Commissioning (hr)" : "filter-input-50", "Lofar Observing Time Prio A (hr)" : "filter-input-50", "Lofar Observing Time Prio B (hr)" : "filter-input-50" - }] + }], + isprocessed: false, + isLoading: true } } - secondsToHours(d,type='seconds') { - return UnitConversion.getUIResourceUnit(type,d) + conversion(d,type) { + const coversionType = this.state.resources.find(i => i.name === type).quantity_value; + return UnitConversion.getUIResourceUnit(coversionType,d) } - toBytes(d,type='bytes'){ - return UnitConversion.getUIResourceUnit(type,d) - } - - componentDidMount(){ + componentDidMount(){ const { projectCategory} = this.state; const { periodCategory} = this.state; - const promises = [CycleService.getProjects(), CycleService.getCycleQuota()] + const promises = [CycleService.getProjects(), CycleService.getCycleQuota(),CycleService.getResources()] Promise.all(promises).then(responses => { const projects = responses[0]; const cycleQuota = responses[1]; - CycleService.getAllCycle().then(cyclelist =>{ - const results = cyclelist.data.results || []; + this.setState({ resources: responses[2].data.results }); + CycleService.getAllCycles().then(cyclelist =>{ + const results = cyclelist || []; results.map(cycle => { const regularProjects = projects.data.results.filter(project => project.cycles_ids.includes(cycle.name) && projectCategory.includes(project.project_category_value)); const longterm = projects.data.results.filter(project => project.cycles_ids.includes(cycle.name) && periodCategory.includes(project.period_category_value)); @@ -81,18 +83,20 @@ class CycleList extends Component{ cycle.id = cycle.name ? cycle.name.split(' ').join('') : cycle.name; cycle.regularProjects = regularProjects.length; cycle.longterm = longterm.length; - cycle.observingTime = this.secondsToHours((cycleQuota.data.results.find(quota => quota.cycle_id === cycle.name && quota.resource_type_id === 'observing_time') || {value: 0}).value) - cycle.processingTime = this.secondsToHours((cycleQuota.data.results.find(quota => quota.cycle_id === cycle.name && quota.resource_type_id === 'cep_processing_time') || {value: 0}).value) - cycle.ltaResources = this.toBytes((cycleQuota.data.results.find(quota => quota.cycle_id === cycle.name && quota.resource_type_id === 'lta_storage') || {value: 0}).value) - cycle.support = this.secondsToHours((cycleQuota.data.results.find(quota => quota.cycle_id === cycle.name && quota.resource_type_id === 'support_time') || {value: 0}).value) - cycle.observingTimeDDT = this.secondsToHours((cycleQuota.data.results.find(quota => quota.cycle_id === cycle.name && quota.resource_type_id === 'observing_time_commissioning') || {value: 0}).value) - cycle.observingTimePrioA = this.secondsToHours((cycleQuota.data.results.find(quota => quota.cycle_id === cycle.name && quota.resource_type_id === 'observing_time_prio_a') || {value: 0}).value) - cycle.observingTimePrioB = this.secondsToHours((cycleQuota.data.results.find(quota => quota.cycle_id === cycle.name && quota.resource_type_id === 'observing_time_prio_b') || {value: 0}).value) + cycle.observingTime = this.conversion((cycleQuota.data.results.find(quota => quota.cycle_id === cycle.name && quota.resource_type_id === 'observing_time') || {value: 0}).value, 'observing_time') + cycle.processingTime = this.conversion((cycleQuota.data.results.find(quota => quota.cycle_id === cycle.name && quota.resource_type_id === 'cep_processing_time') || {value: 0}).value, 'cep_processing_time') + cycle.ltaResources = this.conversion((cycleQuota.data.results.find(quota => quota.cycle_id === cycle.name && quota.resource_type_id === 'lta_storage') || {value: 0}).value, 'lta_storage') + cycle.support = this.conversion((cycleQuota.data.results.find(quota => quota.cycle_id === cycle.name && quota.resource_type_id === 'support_time') || {value: 0}).value, 'support_time') + cycle.observingTimeDDT = this.conversion((cycleQuota.data.results.find(quota => quota.cycle_id === cycle.name && quota.resource_type_id === 'observing_time_commissioning') || {value: 0}).value, 'observing_time_commissioning') + cycle.observingTimePrioA = this.conversion((cycleQuota.data.results.find(quota => quota.cycle_id === cycle.name && quota.resource_type_id === 'observing_time_prio_a') || {value: 0}).value, 'observing_time_prio_a') + cycle.observingTimePrioB = this.conversion((cycleQuota.data.results.find(quota => quota.cycle_id === cycle.name && quota.resource_type_id === 'observing_time_prio_b') || {value: 0}).value, 'observing_time_prio_b') cycle.actionpath = "/cycle"; return cycle; }); this.setState({ - cyclelist : results + cyclelist : results, + isprocessed: true, + isLoading: false }); }) }) @@ -101,6 +105,16 @@ class CycleList extends Component{ render(){ return ( <> + <div className="p-grid"> + <div className="p-col-10 p-lg-10 p-md-10"> + <h2>Cycle - List </h2> + </div> + <div className="p-col-2 p-lg-2 p-md-2"> + <Link to={{ pathname: '/cycle'}} title="Add New Cycle" style={{float: "right"}}> + <i className="fa fa-plus-square" style={{marginTop: "10px"}}></i> + </Link> + </div> + </div> {/* * Call View table to show table data, the parameters are, data - Pass API data @@ -108,7 +122,9 @@ class CycleList extends Component{ showaction - {true/false} -> to show the action column paths - specify the path for navigation - Table will set "id" value for each row in action button */} - {(this.state.cyclelist && this.state.cyclelist.length) ? + + {this.state.isLoading? <AppLoader /> : this.state.isprocessed &&(this.state.cyclelist && this.state.cyclelist.length) ? + <ViewTable data={this.state.cyclelist} defaultcolumns={this.state.defaultcolumns} @@ -116,8 +132,11 @@ class CycleList extends Component{ columnclassname = {this.state.columnclassname} showaction="true" paths={this.state.paths} - /> : <></> - } + /> : <></> + } + + + </> ) } diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/list.test.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/list.test.js new file mode 100644 index 0000000000000000000000000000000000000000..8ce4fe1b1c06133cb25a5d07a1cbf26b6a7b69b4 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/list.test.js @@ -0,0 +1,69 @@ +import "babel-polyfill"; +import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import { render, fireEvent } from '@testing-library/react'; +import CycleList from './list'; +import UnitConversion from '../../utils/unit.converter'; +import mockData from '../../__mocks__/cycle.service.data'; + +jest.mock('../../services/cycle.service', () => { + return { + getProjects: () => Promise.resolve({ data: mockData.getProjects }), + getCycleQuota: () => Promise.resolve({ data: mockData.getCycleQuota }), + getAllCycles: () => Promise.resolve(mockData.getAllCycle.results ), + getResources: () => Promise.resolve({ data: mockData.getresources }) + } +}); + +const flushPromises = () => new Promise(setImmediate); + +describe('<CycleList />', () => { + test('render table in the cycle list', async () => { + const { container } = render(<MemoryRouter><CycleList /></MemoryRouter>); + await flushPromises(); + expect(container.querySelector('table')).toBeInTheDocument(); + }); + + test('render cycle list in row', async () => { + const { container } = render(<MemoryRouter><CycleList /></MemoryRouter>); + await flushPromises(); + expect(container.querySelectorAll('tr').length).toBe(4); + }); + + test('render columns in the cycle list', async () => { + const { container } = render(<MemoryRouter><CycleList /></MemoryRouter>); + await flushPromises(); + expect(container.querySelectorAll('th').length).toBe(11); + }); + + test('render cycleId - cycle name conversion', async () => { + const { container } = render(<MemoryRouter><CycleList /></MemoryRouter>); + await flushPromises(); + expect(container.querySelectorAll('tr')[1].innerHTML.includes('Cycle00')).toBeTruthy(); + }); + + test('render observing time in hours', async () => { + const { container } = render(<MemoryRouter><CycleList /></MemoryRouter>); + await flushPromises(); + const observing_time = Math.floor(Number(mockData.getCycleQuota.results[0].value) / 3600); + expect(container.querySelectorAll('tr')[1].innerHTML.includes(observing_time)).toBeTruthy(); + }); + + test('render commissioning time in hours', async () => { + const { container } = render(<MemoryRouter><CycleList /></MemoryRouter>); + await flushPromises(); + const commissioning_time = UnitConversion.getUIResourceUnit('bytes',Number(mockData.getCycleQuota.results[1].value)); + expect(container.querySelectorAll('tr')[1].innerHTML.includes(commissioning_time)).toBeTruthy(); + }); + + test('toggle columns in table', async () => { + const { container } = render(<MemoryRouter><CycleList /></MemoryRouter>); + await flushPromises(); + const panel = container.querySelector('#overlay_panel'); + expect(panel).toHaveStyle('display: block'); + fireEvent.click(container.querySelector('.col-filter-btn')); + await flushPromises(); + expect(panel).toHaveStyle('display: none'); + expect(container.querySelectorAll("input[type=checkbox]:checked").length).toBe(container.querySelectorAll('th').length) + }); +}); \ 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 174a7a7af740a2d36a804c4a2dc6759d9a106fea..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,5 +1,6 @@ import React, {Component} from 'react'; + export class Dashboard extends Component { constructor(props){ 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 index 365fa6f751bba5c3f1a8c0065b485fa144055240..aa608dabcab94707ddd8f5e515c550a4afbe8efd 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/ResourceInputList.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/ResourceInputList.js @@ -16,7 +16,7 @@ export class ResourceInputList extends Component { } shouldComponentUpdate() { - return this.updateEnabled; + return true; } onInputChange(field, event) { @@ -39,9 +39,10 @@ export class ResourceInputList extends Component { <label key={'label1-'+ index} className="col-lg-3 col-md-3 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.resourceUnit.name].display}`} - placeholder={item.name} + 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"}} /> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/create.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/create.js index d5ca586a3411958091c581ca762f5d7e1c9a49f7..fc07f034438730eef9d48d1691e7f8d219cafa03 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/create.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/create.js @@ -14,6 +14,7 @@ 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'; @@ -25,11 +26,12 @@ export class ProjectCreate extends Component { constructor(props) { super(props); this.state = { + isLoading: true, dialog: { header: '', detail: ''}, project: { trigger_priority: 1000, priority_rank: null, - project_quota: [], // Mandatory Field in the back end, so an empty array is passed + quota: [], // Mandatory Field in the back end, so an empty array is passed can_trigger: false }, projectQuota: {}, // Resource Allocations @@ -59,6 +61,7 @@ export class ProjectCreate extends Component { 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 = {position: 'left', event: 'hover', className:"p-tooltip-custom"}; this.setProjectQuotaDefaults = this.setProjectQuotaDefaults.bind(this); this.setProjectParams = this.setProjectParams.bind(this); @@ -93,7 +96,7 @@ export class ProjectCreate extends Component { 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}); + this.setState({resourceList: resourceList, resources: resources, projectQuota: projectQuota, isLoading: false}); }); // ProjectService.getProjects().then(projects => { // console.log(projects); @@ -118,7 +121,8 @@ export class ProjectCreate extends Component { setProjectQuotaDefaults(resources) { let projectQuota = this.state.projectQuota; for (const resource of resources) { - projectQuota[resource['name']] = this.projectResourceDefaults[resource.name]/this.resourceUnitMap[resource.resourceUnit.name].conversionFactor; + const conversionFactor = this.resourceUnitMap[resource.quantity_value]?this.resourceUnitMap[resource.quantity_value].conversionFactor:1; + projectQuota[resource['name']] = this.projectResourceDefaults[resource.name]/conversionFactor; } return projectQuota; } @@ -156,9 +160,19 @@ export class ProjectCreate extends Component { * @param {string} key * @param {any} value */ - setProjectParams(key, value) { + setProjectParams(key, value, type) { let project = this.state.project; - project[key] = value; + 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)}); } @@ -168,19 +182,23 @@ export class ProjectCreate extends Component { * @param {InputEvent} event */ setProjectQuotaParams(key, event) { + let projectQuota = this.state.projectQuota; if (event.target.value) { - let projectQuota = this.state.projectQuota; let resource = _.find(this.state.resources, {'name': key}); - const resourceUnit = resource?resource.resourceUnit:null; - // console.log(resourceUnit); - if (resourceUnit) { - projectQuota[key] = event.target.value.replace(this.resourceUnitMap[resourceUnit.name].display,''); + + 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 { - projectQuota[key] = event.target.value; + newValue = event.target.value; } - // console.log(`${key} - ${event.target.value}`); - this.setState({projectQuota: projectQuota}); + projectQuota[key] = (newValue==="NaN" || isNaN(newValue))?0:newValue; + } else { + let projectQuota = this.state.projectQuota; + projectQuota[key] = 0; } + this.setState({projectQuota: projectQuota}); } /** @@ -238,7 +256,7 @@ export class ProjectCreate extends Component { 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.resourceUnit.name].conversionFactor}; + 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:[]) @@ -263,7 +281,7 @@ export class ProjectCreate extends Component { * Function to cancel form creation and navigate to other page/component */ cancelCreate() { - this.setState({redirect: '/project/list'}); + this.setState({redirect: '/project'}); } /** @@ -274,7 +292,6 @@ export class ProjectCreate extends Component { let prevResources = this.state.resources; let resourceList = []; let resources = []; - const defaultResources = this.defaultResources; if (resources) { // const nonDefaultResources = _.remove(resources, function(resource) { return _.find(defaultResources, {'name': resource.name})==null }); // resourceList = nonDefaultResources.concat(this.state.resourceList); @@ -317,15 +334,17 @@ export class ProjectCreate extends Component { <div className="p-grid"> <Growl ref={(el) => this.growl = el} /> - <div className="p-col-10 p-lg-3 p-md-4"> + <div className="p-col-10 p-lg-10 p-md-10"> <h2>Project - Add</h2> </div> - <div className="p-col-2 p-lg-3 p-md-4"> - <Link to={{ pathname: '/project'}} tooltip="Close Edit" > + <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'}}> @@ -338,21 +357,23 @@ export class ProjectCreate extends Component { <label htmlFor="projectName" 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="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="error"> - {this.state.errors.name ? this.state.errors.name : ""} + <label className={this.state.errors.name?"error":"info"}> + {this.state.errors.name ? this.state.errors.name : "Max 128 characters"} </label> </div> <label htmlFor="description" className="col-lg-2 col-md-2 col-sm-12">Description <span style={{color:'red'}}>*</span></label> <div className="col-lg-4 col-md-4 col-sm-12"> <InputTextarea className={this.state.errors.description ?'input-error':''} rows={3} cols={30} + tooltip="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="error"> - {this.state.errors.description ? this.state.errors.description : ""} + <label className={this.state.errors.description ?"error":"info"}> + {this.state.errors.description ? this.state.errors.description : "Max 255 characters"} </label> </div> </div> @@ -360,9 +381,10 @@ export class ProjectCreate extends Component { <label htmlFor="triggerPriority" className="col-lg-2 col-md-2 col-sm-12">Trigger Priority </label> <div className="col-lg-4 col-md-4 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 w.r.t. triggers" tooltipOptions={this.tooltipOptions} mode="decimal" showButtons min={0} max={1001} step={10} useGrouping={false} - onChange={(e) => this.setProjectParams('trigger_priority', e.target.value)} - onBlur={(e) => this.setProjectParams('trigger_priority', e.target.value)} /> + 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 : ""} @@ -370,13 +392,17 @@ export class ProjectCreate extends Component { </div> <label htmlFor="trigger" className="col-lg-2 col-md-2 col-sm-12">Allows Trigger Submission</label> <div className="col-lg-4 col-md-4 col-sm-12" data-testid="trigger"> - <Checkbox inputId="trigger" role="trigger" checked={this.state.project.can_trigger} onChange={e => this.setProjectParams('can_trigger', e.target.checked)}></Checkbox> + <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-4 col-md-4 col-sm-12" data-testid="projCat" > - <Dropdown inputId="projCat" optionLabel="name" optionValue="id" + <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)}} @@ -384,7 +410,8 @@ export class ProjectCreate extends Component { </div> <label htmlFor="periodCategory" className="col-lg-2 col-md-2 col-sm-12">Period Category</label> <div className="col-lg-4 col-md-4 col-sm-12"> - <Dropdown data-testid="period-cat" id="period-cat" optionLabel="name" optionValue="id" + <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)}} @@ -395,6 +422,7 @@ export class ProjectCreate extends Component { <label htmlFor="triggerPriority" className="col-lg-2 col-md-2 col-sm-12">Cycle(s)</label> <div className="col-lg-4 col-md-4 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)}} @@ -404,9 +432,11 @@ export class ProjectCreate extends Component { <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-4 col-md-4 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 w.r.t. 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.target.value)} - onBlur={(e) => this.setProjectParams('priority_rank', e.target.value)} /> + 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> @@ -440,17 +470,19 @@ export class ProjectCreate extends Component { </div> </div> <div className="p-grid p-justify-start"> - <div className="p-col-1"> + <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="p-col-1"> + <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: '50vw'}} inputId="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" /> @@ -458,7 +490,7 @@ export class ProjectCreate extends Component { </div> } > <div className="p-grid"> - <div className="col-lg-1 col-md-1 col-sm-2"> + <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"> 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 index 4bfe0e8300128be0679851a474cd2239481c39e0..84c2b338449d107059e43fa4396dead622a042fe 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/create.test.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/create.test.js @@ -27,9 +27,9 @@ afterEach(() => { */ const setMockSpy = (() => { projectCategoriesSpy = jest.spyOn(ProjectService, 'getProjectCategories'); - projectCategoriesSpy.mockImplementation(() => { return Promise.resolve([{id: 1, name: 'Regular'}])}); + projectCategoriesSpy.mockImplementation(() => { return Promise.resolve(ProjectServiceMock.project_categories)}); periodCategoriesSpy = jest.spyOn(ProjectService, 'getPeriodCategories'); - periodCategoriesSpy.mockImplementation(() => { return Promise.resolve([{id: 1, name: 'Single Cycle'}])}); + 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'}, @@ -143,7 +143,7 @@ it("renders Save button enabled when all data entered", async () => { // 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(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); @@ -152,7 +152,7 @@ it("renders Save button enabled when all data entered", async () => { // 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(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); diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/edit.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/edit.js index 3f83814447344c197a0f3b33a23b5108a13863f6..54672a71a6834f7fb9be9875e28f95ecd065979b 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/edit.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/edit.js @@ -14,6 +14,7 @@ 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'; @@ -22,29 +23,31 @@ export class ProjectEdit extends Component { constructor(props) { super(props); this.state = { + isLoading: true, dialog: { header: '', detail: ''}, project: { trigger_priority: 1000, priority_rank: null, - project_quota: [] // Mandatory Field in the back end + quota: [] // Mandatory Field in the back end }, - projectQuota: {}, - validFields: {}, - validForm: false, + 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: [], - resourceList: [], - cycles: [] + 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.updateEnabled = true; + 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.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'}, @@ -54,15 +57,17 @@ export class ProjectEdit extends Component { {name:'LOFAR Support hours'} ]; this.projectResourceDefaults = {}; this.resourceUnitMap = UnitConverter.resourceUnitMap; + + 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.setUpdateEnabled = this.setUpdateEnabled.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); + this.saveProjectQuota = this.saveProjectQuota.bind(this); + this.cancelEdit = this.cancelEdit.bind(this); } componentDidMount() { @@ -84,16 +89,45 @@ export class ProjectEdit extends Component { }); ProjectService.getResources() .then(resourceList => { - const defaultResources = this.defaultResources; - const resources = _.remove(resourceList, function(resource) { return _.find(defaultResources, {'name': resource.name})!=null }); - // Object.assign(resources, this.defaultResources); - console.log(resources); - const projectQuota = this.setProjectQuotaDefaults(resources); - this.setState({resourceList: resourceList, resources: resources, projectQuota: projectQuota}); + 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"> @@ -102,59 +136,96 @@ export class ProjectEdit extends Component { ); } + /** + * Function to set project resource allocation + * @param {Array} resources + */ setProjectQuotaDefaults(resources) { let projectQuota = this.state.projectQuota; for (const resource of resources) { - console.log(resource['name']); - projectQuota[resource['name']] = this.projectResourceDefaults[resource.name]/this.resourceUnitMap[resource.resourceUnit.name].conversionFactor; + 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) { - console.log(this.state.newResource); let resourceList = this.state.resourceList; const newResource = _.remove(resourceList, {'name': this.state.newResource}); - console.log(newResource); let resources = this.state.resources; resources.push(newResource[0]); this.setState({resources: resources, resourceList: resourceList, newResource: null}); } } - setProjectParams(key, value) { - let project = this.state.project; - project[key] = value; - console.log(`${key} - ${value}`); - this.setState({project: project, validForm: this.validateForm(key)}); + /** + * 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}); } - setUpdateEnabled(enable) { - this.updateEnabled = enable; + /** + * 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) { - console.log(key); - console.log(event.target) - console.log(event.target.value); + let projectQuota = this.state.projectQuota; if (event.target.value) { - let projectQuota = this.state.projectQuota; let resource = _.find(this.state.resources, {'name': key}); - const resourceUnit = resource?resource.resourceUnit:null; - console.log(resourceUnit); - if (resourceUnit) { - projectQuota[key] = event.target.value.replace(this.resourceUnitMap[resourceUnit.name].display,''); + + 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 { - projectQuota[key] = event.target.value; + newValue = event.target.value; } - console.log(`${key} - ${event.target.value}`); - this.setState({projectQuota: projectQuota}); + projectQuota[key] = (newValue==="NaN" || isNaN(newValue))?0:newValue; + } else { + let projectQuota = this.state.projectQuota; + projectQuota[key] = 0; } + this.setState({projectQuota: projectQuota}); } /** - * Function to validate the form excluding the JSON Editor values + * 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; @@ -189,79 +260,90 @@ export class ProjectEdit extends Component { } } } - console.log(errors); - this.setState({errors: errors, validFields: validFields}); + 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) { - console.log(this.state.project); - console.log(this.state.projectQuota); - 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.resourceUnit.name].conversionFactor}; - projectQuota.push(quota); - } - console.log(projectQuota); - 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({dialogVisible: true, dialog: dialog}) + 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 { - console.log(this.growl); - console.log(project); - this.growl.show({severity: 'error', summary: 'Error Occured', detail: 'Unable to save Project'}); + this.growl.show({severity: 'error', summary: 'Error Occured', detail: 'Unable to update Project'}); this.setState({errors: project}); } }); } } - cancelCreate() {} - - reset() { - let resources = this.state.resources; - let resourceList = []; - const defaultResources = this.defaultResources; - if (resources) { - const nonDefaultResources = _.remove(resources, function(resource) { return _.find(defaultResources, {'name': resource.name})==null }); - resourceList = nonDefaultResources.concat(this.state.resourceList); + /** + * 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); + } } - 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 - }); + 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}); } - shouldComponentUpdate() { - return this.updateEnabled; + /** + * Cancel edit and redirect to Project View page + */ + cancelEdit() { + this.setState({redirect: `/project/view/${this.state.project.name}`}); } render() { @@ -269,49 +351,29 @@ export class ProjectEdit extends Component { return <Redirect to={ {pathname: this.state.redirect} }></Redirect> } - - console.log(this.defaultResources); - console.log(this.state.resourceList); - console.log(this.state.resources); - return ( <React.Fragment> - <div className="p-grid"> - <Dialog header={this.state.dialog.header} visible={this.state.dialogVisible} style={{width: '50vw'}} - modal={true} onHide={() => {this.setState({dialogVisible: false})}} - footer={<div> - <Button key="back" onClick={() => {this.setState({dialogVisible: false})}} label="No" /> - <Button key="submit" type="primary" onClick={this.reset} label="Yes" /> - </div> - } > - <div className="p-grid"> - <div className="col-lg-1 col-md-1 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> <div className="p-grid"> <Growl ref={(el) => this.growl = el} /> - <div className="p-col-10 p-lg-3 p-md-4"> + <div className="p-col-10 p-lg-10 p-md-10"> <h2>Project - Edit</h2> </div> - <div className="p-col-2 p-lg-3 p-md-4"> - <Link to={{ pathname: '/project'}} tooltip="Close Edit" > + <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-4 col-md-4 col-sm-12"> - <InputText className={this.state.errors.name ?'input-error':''} id="projectName" type="text" + <InputText className={this.state.errors.name ?'input-error':''} id="projectName" data-testid="name" value={this.state.project.name} onChange={(e) => this.setProjectParams('name', e.target.value)} onBlur={(e) => this.setProjectParams('name', e.target.value)}/> @@ -322,7 +384,7 @@ export class ProjectEdit extends Component { <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.project.description} + 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="error"> @@ -333,22 +395,24 @@ export class ProjectEdit extends Component { <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-4 col-md-4 col-sm-12"> - <InputNumber className={this.state.errors.name ?'input-error':''} id="triggerPriority" - value={this.state.project.trigger_priority} showButtons step={10} - onChange={(e) => this.setProjectParams('trigger_priority', e.target.value)}/> + <InputNumber inputId="trig_prio" name="trig_prio" className={this.state.errors.name ?'input-error':''} + 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> <label htmlFor="trigger" className="col-lg-2 col-md-2 col-sm-12">Allows Trigger Submission</label> <div className="col-lg-4 col-md-4 col-sm-12"> - <Checkbox checked={this.state.project.can_trigger} onChange={e => this.setProjectParams('can_trigger', e.target.checked)}></Checkbox> + <Checkbox inputId="trigger" role="trigger" 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-4 col-md-4 col-sm-12"> - <Dropdown optionLabel="name" optionValue="id" + <Dropdown inputId="projCat" optionLabel="value" optionValue="url" value={this.state.project.project_category} options={this.state.projectCategories} onChange={(e) => {this.setProjectParams('project_category', e.value)}} @@ -356,7 +420,7 @@ export class ProjectEdit extends Component { </div> <label htmlFor="periodCategory" className="col-lg-2 col-md-2 col-sm-12">Period Category</label> <div className="col-lg-4 col-md-4 col-sm-12"> - <Dropdown optionLabel="name" optionValue="id" + <Dropdown data-testid="period-cat" id="period-cat" optionLabel="value" optionValue="url" value={this.state.project.period_category} options={this.state.periodCategories} onChange={(e) => {this.setProjectParams('period_category',e.value)}} @@ -366,7 +430,7 @@ export class ProjectEdit extends Component { <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-4 col-md-4 col-sm-12"> - <MultiSelect optionLabel="name" optionValue="url" filter={true} + <MultiSelect data-testid="cycle" id="cycle" optionLabel="name" optionValue="url" filter={true} value={this.state.project.cycles} options={this.state.cycles} onChange={(e) => {this.setProjectParams('cycles',e.value)}} @@ -375,15 +439,16 @@ export class ProjectEdit extends Component { </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-4 col-md-4 col-sm-12"> - <InputNumber id="projRank" value={this.state.project.priority_rank} mode="decimal" showButtons min={0} max={100} + <InputNumber inputId="proj-rank" name="rank" data-testid="rank" value={this.state.project.priority_rank} + mode="decimal" showButtons min={0} max={100} onChange={(e) => this.setProjectParams('priority_rank', e.value)} - onBlur={(e) => this.setProjectParams('priority_rank', e.target.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 && + {this.state.resourceList && <div className="p-fluid"> <div className="p-field p-grid"> <div className="col-lg-3 col-md-3 col-sm-112"> @@ -392,30 +457,55 @@ export class ProjectEdit extends Component { <div className="col-lg-3 col-md-3 col-sm-10"> <Dropdown optionLabel="name" optionValue="name" value={this.state.newResource} - options={this.state.resourceList} + 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} /> + <Button label="" className="p-button-primary" icon="pi pi-plus" onClick={this.addNewResource} 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} /> - </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"> <div className="p-col-1"> - <Button label="Save" className="p-button-primary" icon="pi pi-check" onClick={this.saveProject} disabled={!this.state.validForm} /> + <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.cancelCreate} /> + <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> ); } 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 index ece808e56c5c5b2eaa05ba6209b6cc8bebda1af8..7572b27b8d71777ba3a584f56a054438cdd4b13e 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/index.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/index.js @@ -1,4 +1,6 @@ import {ProjectCreate} from './create'; +import {ProjectView} from './view'; import {ProjectEdit} from './edit'; +import {ProjectList} from './list'; -export {ProjectCreate, ProjectEdit} ; +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 index d7b118a645897188244666a1ac12206c5bf64ba9..9b2164202a15e7826ad10a6efd3cf69c110d121c 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/SchedulingUnitList.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/SchedulingUnitList.js @@ -1,7 +1,11 @@ import React, { Component } from 'react' import 'primeflex/primeflex.css'; + +import AppLoader from "./../../layout/components/AppLoader"; import ViewTable from './../../components/ViewTable'; -import {getScheduling_Unit_Draft} from '../../services/ScheduleService' + +import ScheduleService from '../../services/schedule.service'; + class SchedulingUnitList extends Component{ @@ -11,43 +15,95 @@ class SchedulingUnitList extends Component{ 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", }] } } +<<<<<<< Updated upstream componentDidMount(){ - getScheduling_Unit_Draft().then(scheduleunit =>{ + 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 +======= + async getSchedulingUnitList () { + const bluePrint = await ScheduleService.getSchedulingUnitBlueprint(); + ScheduleService.getSchedulingUnitDraft().then(scheduleunit =>{ + var scheduleunits = scheduleunit.data.results; + for( const scheduleunit of scheduleunits){ + const blueprintdata = bluePrint.data.results.find(i => i.draft_id === scheduleunit.id); + scheduleunit['actionpath']='/schedulingunit/view'; + scheduleunit['start_time'] = blueprintdata.start_time; + scheduleunit['stop_time'] = blueprintdata.stop_time; + } this.setState({ - scheduleunit : scheduleunit.data + scheduleunit : scheduleunit.data ,isLoading:false +>>>>>>> Stashed changes }); }) } + componentDidMount(){ + this.getSchedulingUnitList(); + + } + render(){ - - if(this.state.scheduleunit.results){ - this.state.scheduleunit.results.forEach(item =>{ - delete item['requirements_doc'] - }) + if (this.state.isLoading) { + return <AppLoader/> } - // The default table column value and header to show in UI - // let defaultcolumns = [ {"name":"Name","description":"Description","created_at":"Created Date","updated_at":"Updated Date","requirements_template_id": "Requirement Temp","scheduling_set_id":" Scheduling Unit"}] - let defaultcolumns = [ {"name":"Name","description":"Description","created_at":"Created Date","updated_at":"Updated Date","requirements_template_id": "Template"}] return( <> - {/* +<<<<<<< Updated upstream + + { + +======= + { +>>>>>>> Stashed changes + /* * Call View table to show table data, the parameters are, data - Pass API data - defaultcolumns - This colum will be populate by default in table with header mentioned + 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 && + {this.state.scheduleunit.results && <ViewTable data={this.state.scheduleunit.results} - defaultcolumns={defaultcolumns} + defaultcolumns={this.state.defaultcolumns} + optionalcolumns={this.state.optionalcolumns} + columnclassname={this.state.columnclassname} showaction="true" + keyaccessor="id" paths={this.state.paths} + unittest={this.state.unittest} /> } </> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/ViewSchedulingUnit.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/ViewSchedulingUnit.js index b890b01651899dc41059e2b3b7d593f433788b53..4b3c24179fb7025275c45512fb753e32fb5822a5 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/ViewSchedulingUnit.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/ViewSchedulingUnit.js @@ -2,18 +2,52 @@ 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 {getScheduling_Unit_Draft_By_Id, getTasks_Draft_By_scheduling_Unit_Id} from '../../services/ScheduleService' +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", }] } } @@ -21,44 +55,41 @@ class ViewSchedulingUnit extends Component{ componentDidMount(){ let schedule_id = this.props.location.state.id if (schedule_id) { - getScheduling_Unit_Draft_By_Id(schedule_id).then(scheduleunit =>{ - getTasks_Draft_By_scheduling_Unit_Id(scheduleunit.data.id).then(tasks =>{ + ScheduleService.getSchedulingUnitDraftById(schedule_id) + .then(scheduleunit =>{ + ScheduleService.getScheduleTasksBySchedulingUnitId(scheduleunit.data.id) + .then(tasks =>{ this.setState({ - scheduleunit : scheduleunit.data,schedule_unit_task : tasks.data.results + scheduleunit : scheduleunit.data, + schedule_unit_task : tasks, + isLoading: false }); - }) - }) - } + }); + }) + } } - + render(){ - - if (this.state.schedule_unit_task.length>0) { - this.state.schedule_unit_task.forEach(item =>{ - delete item['specifications_doc'] - }); - } - - - // Default column for Schedule Unit-Task Details - // let defaultcolumns = [ {"id":"Task Identifier","name":"Task Name","description":"Task Description","created_at":"Created Date","updated_at":"Updated Date","requirements_template_id": "Requirement Temp"}] - let defaultcolumns = [ {"id":"Task Identifier","name":"Task Name","description":"Task Description","created_at":"Created Date","updated_at":"Updated Date"}] return( - <> + <> <div className="p-grid"> - <div className="p-col-5"> - <h2>Scheduling Unit - Details </h2> - </div> - <div className="p-col-1"> - <Link to={{ pathname: '/sheduling/edit', state: {id: this.state.scheduleunit?this.state.scheduleunit.id:''}}} tooltip="Edit" > - <i className="fa fa-edit" style={{marginTop: "10px"}}></i> - </Link> - </div> + <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> - - {this.state.scheduleunit && - <> - <div className="p-grid"> + </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> @@ -66,37 +97,58 @@ class ViewSchedulingUnit extends Component{ </div> <div className="p-grid"> <label className="p-col-2">Created At</label> - <span className="p-col-4">{this.state.scheduleunit.created_at}</span> + <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">{this.state.scheduleunit.updated_at}</span> + <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 style={{marginTop: '20px'}}> + <div> <h3>Tasks Details</h3> </div> {/* * Call View table to show table data, the parameters are, - data - Pass API data - defaultcolumns - This colum will be populate by default in table with header mentioned + 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.schedule_unit_task.length>0 && + {this.state.isLoading ? <AppLoader/> :this.state.schedule_unit_task.length>0 && <ViewTable data={this.state.schedule_unit_task} - defaultcolumns={defaultcolumns} + defaultcolumns={this.state.defaultcolumns} + optionalcolumns={this.state.optionalcolumns} + columnclassname={this.state.columnclassname} showaction="true" + keyaccessor="id" paths={this.state.paths} + unittest={this.state.unittest} /> } </> - ) } } 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 886fd7a544c90aafa55362200812dca83129a145..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,23 +1,23 @@ import React, {Component} from 'react'; -import SchedulingUnitList from './SchedulingUnitList' +import SchedulingUnitList from './SchedulingUnitList'; export class Scheduling extends Component { constructor(props){ super(props) this.state = { scheduleunit: [], - schedule_unit_task: [] + schedule_unit_task: [] , + isLoading:false } } render() { - return ( + return ( <> <h2>Scheduling Unit - List</h2> - {this.state.scheduleunit && - <SchedulingUnitList /> - } - </> + {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 index 52c91365f5b166f13fd6d759eb445adc0aa3270d..90ad6359847a5a18718b005ddb73608d9188a08a 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/edit.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/edit.js @@ -10,7 +10,9 @@ import { Button } from 'primereact/button'; import Jeditor from '../../components/JSONEditor/JEditor'; -import TaskService from '../../services/task.services'; +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 @@ -29,7 +31,8 @@ export class TaskEdit extends Component { taskTemplates:[], validEditor: false, validForm: false, - errors: {} + errors: {}, + isLoading: true }; this.formRules = { name: {required: true, message: "Name can not be empty"}, @@ -83,12 +86,15 @@ export class TaskEdit extends Component { * @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(); } @@ -133,6 +139,7 @@ export class TaskEdit extends Component { } componentDidMount() { + this.setState({ isLoading: true }); TaskService.getTaskTemplates() .then((templates) => { this.setState({taskTemplates: templates}); @@ -142,13 +149,13 @@ export class TaskEdit extends Component { if (task) { TaskService.getSchedulingUnit("draft", task.scheduling_unit_draft_id) .then((schedulingUnit) => { - this.setState({schedulingUnit: 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}); + this.setState({task: task, taskSchema: taskTemplate.schema, isLoading: false}); }); } else { this.setState({redirect: "/not-found"}); @@ -157,13 +164,18 @@ export class TaskEdit extends Component { } 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], @@ -176,85 +188,90 @@ export class TaskEdit extends Component { return ( <React.Fragment> <div className="p-grid"> - <div className="p-col-10 p-lg-3 p-md-4"> + <div className="p-col-10 p-lg-10 p-md-10"> <h2>Task - Edit</h2> </div> - <div className="p-col-2 p-lg-3 p-md-4"> - <Link to={{ pathname: '/task', state: {id: this.state.task?this.state.task.id:''}}} tooltip="Close Edit" > + <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 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> - {/* <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> - </> - } + <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 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 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> + } <div className="p-fluid"> - <div className="p-grid"><div className="p-col-12"> - {this.state.taskSchema?jeditor:""} - </div></div> + <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 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> ); diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/view.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/view.js index e33bcdb5c5945961622585eb8ebfaffc75f43cd3..a8d612e25fed0eb865b57cd798a5ad8134e0d3a3 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/view.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/view.js @@ -4,14 +4,16 @@ import moment from 'moment'; import Jeditor from '../../components/JSONEditor/JEditor'; -import TaskService from '../../services/task.services'; +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) { @@ -20,29 +22,38 @@ export class TaskView extends Component { if (this.props.match.params.type) { this.state.taskType = this.props.match.params.type; } + } - static getDerivedStateFromProps(nextProps, prevstate){ - if (prevstate.task && nextProps.location.state && - (nextProps.location.state.taskId === prevstate.task.id || - nextProps.location.state.taskType === prevstate.taskType)) { - return {taskId: prevstate.task.id, taskType: prevstate.taskType} - } - return null; - } + // 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.location.state && - (this.state.task.id !== this.props.location.state.taskId || - this.state.taskType !== this.props.location.state.taskType)) { - this.getTaskDetails(this.props.location.state.taskId, this.props.location.state.taskType); - } + 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'; + // 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 { @@ -77,7 +88,7 @@ export class TaskView extends Component { if (this.state.editorFunction) { this.state.editorFunction(); } - this.setState({task: task, taskTemplate: taskTemplate, taskType: taskType}); + this.setState({task: task, taskTemplate: taskTemplate, isLoading: false, taskId: taskId, taskType: taskType}); }); } else { @@ -107,7 +118,8 @@ export class TaskView extends Component { <ul className="task-list"> {list && list.map(item => ( <li key={item.id}> - <Link to={ { pathname:'/task', state: {taskId: item.id, taskType: item.draft?'blueprint':'draft'}}}>{item.name}</Link> + {/* <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> @@ -115,21 +127,28 @@ export class TaskView extends Component { return ( <React.Fragment> <div className="p-grid"> - <div className="p-col-10 p-lg-3 p-md-4"> + <div className="p-col-10 p-lg-10 p-md-10"> <h2>Task - Details </h2> </div> - <div className="p-col-2 p-lg-3 p-md-4"> + <div className="p-col-2 p-lg-2 p-md-2"> {this.state.taskType === 'draft' && - <Link to={{ pathname: '/task/edit', state: {taskId: this.state.task?this.state.task.id:''}}} tooltip="Edit Task" > + <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={{marginTop: "10px"}}></i> + <i className="fa fa-lock" style={{float:"right", marginTop: "10px"}}></i> } </div> </div> - { this.state.task && + { this.state.isLoading? <AppLoader /> : this.state.task && <React.Fragment> <div className="main-content"> <div className="p-grid"> @@ -184,7 +203,8 @@ export class TaskView extends Component { <TaskRelationList list={this.state.task.blueprints} /> } {this.state.taskType === 'blueprint' && - <Link className="col-lg-4 col-md-4 col-sm-12" to={ { pathname:'/task', state: {taskId: this.state.task.draft_id, taskType: 'draft'}}}>{this.state.task.draftObject.name}</Link> + // <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> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/index.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/index.js index ed55703aac6ac7d832720f174ae3db2cd96d8c13..ffe0ff60bb8d7be4c43f943fdbaf0dad6f4530ae 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/index.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/index.js @@ -2,34 +2,84 @@ import React from 'react'; import { Route, Switch, - Redirect + Redirect, } from 'react-router-dom'; import {NotFound} from '../layout/components/NotFound'; -import {ProjectCreate, ProjectEdit} from './Project'; +import {ProjectList, ProjectCreate, ProjectView, ProjectEdit} from './Project'; import {Dashboard} from './Dashboard'; import {Scheduling} from './Scheduling'; import {TaskEdit, TaskView} from './Task'; -import {Cycle} from './Cycle'; +import {list} from './Cycle'; import ViewSchedulingUnit from './Scheduling/ViewSchedulingUnit' +import CycleList from './Cycle/list'; + +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' + }, + { + path: "/cycle", + component: CycleList, + name: 'Cycle List' + }, +]; export const RoutedContent = () => { return ( + <Switch> <Redirect from="/" to="/" exact /> - <Route path="/not-found" exact component= {NotFound} /> - <Route path="/dashboard" exact component={Dashboard} /> - <Route path="/project" exact component={NotFound} /> - <Route path="/project/create" exact component={ProjectCreate} /> - <Route path="/project/edit" exact component={ProjectEdit} /> - <Route path="/project/edit/:id" exact component={ProjectEdit} /> - <Route path="/scheduling" exact component={Scheduling} /> - <Route path="/task" exact component={TaskView} /> - <Route path="/task/view" exact component={TaskView} /> - <Route path="/task/view/:type/:id" exact component={TaskView} /> - <Route path="/task/edit" exact component={TaskEdit} /> - <Route path="/schedulingunit/view" exact component={ViewSchedulingUnit} /> - <Route path="/cycle" exact component={Cycle} /> + {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 index 4d06342047389af3579698288dfba8ce0e307342..d2d5285744fd955b4ea115424a322b3be51976cf 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/services/ScheduleService.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/services/ScheduleService.js @@ -10,6 +10,7 @@ export async function getScheduling_Unit_Draft(){ } ).then(function(response) { res= response; + }).catch(function(error) { console.log('Error on Authentication',error); }); diff --git a/SAS/TMSS/frontend/tmss_webapp/src/services/cycle.service.js b/SAS/TMSS/frontend/tmss_webapp/src/services/cycle.service.js index 0995e6e5da2a6e5cda80ee966bf6a3fafcad6983..1d78f83001f79aaa2502a279ef75995b6f02349d 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/services/cycle.service.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/services/cycle.service.js @@ -4,17 +4,26 @@ const axios = require('axios'); axios.defaults.headers.common['Authorization'] = 'Basic dGVzdDp0ZXN0'; const CycleService = { - 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; - }, - getProjects: async function() { + 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); + } + }, + + getProjects: async function() { let res = []; await axios.get('/api/project/') .then(response => { @@ -33,7 +42,18 @@ const CycleService = { console.error('[cycle.services.cycle_quota]',error); }); return res; - } + }, + getResources: async function() { + let res = []; + await axios.get('/api/resource_type') + .then(response => { + res= response; + }).catch(function(error) { + console.error('[cycle.services.resource_type]',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 index d373b9215cf921e91487217703787adc1c73ec00..36bc7a17016df8168cc36f7167a20d0c38e1aee2 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/services/project.service.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/services/project.service.js @@ -1,5 +1,7 @@ import _ from 'lodash'; +import UnitConverter from './../utils/unit.converter' + const axios = require('axios'); axios.defaults.headers.common['Authorization'] = 'Basic dGVzdDp0ZXN0'; @@ -7,47 +9,23 @@ axios.defaults.headers.common['Authorization'] = 'Basic dGVzdDp0ZXN0'; const ProjectService = { getProjectCategories: async function() { try { - const url = `/api/cycle`; + const url = `/api/project_category/`; const response = await axios.get(url); - //return response.data.results; - return [ - {id: 1, name: "Regular"}, - {id: 2, name: "User Shared Support"}, - {id: 3, name: "Commissioning"}, - {id: 4, name: "DDT"}, - {id: 5, name: "Test"} - ]; + return response.data.results; } catch (error) { console.error(error); } }, getPeriodCategories: async function() { try { - const url = `/api/cycle`; + const url = `/api/period_category/`; const response = await axios.get(url); - // return response.data.results; - return [ - {id: 1, name: "Single Cycle"}, - {id: 2, name: "Long Term"}, - {id: 3, name: "Unbounded"} - ]; + return response.data.results; } catch (error) { console.error(error); } }, getResources: async function() { - return this.getResourceTypes() - .then(resourceTypes => { - return this.getResourceUnits() - .then(resourceUnits => { - for (let resourceType of resourceTypes) { - resourceType.resourceUnit = _.find(resourceUnits, ['name', resourceType.resource_unit_id]); - } - return resourceTypes; - }) - }) - }, - getResourceTypes: async function() { try { // const url = `/api/resource_type/?ordering=name`; const url = `/api/resource_type`; @@ -58,28 +36,15 @@ const ProjectService = { console.error(error); } }, - getResourceUnits: async function() { - try { - const url = `/api/resource_unit`; - const response = await axios.get(url); - // console.log(response); - return response.data.results; - } catch (error) { - console.error(error); - } - }, getDefaultProjectResources: async function() { try { - const url = `/api/resource_unit`; - const response = await axios.get(url); - // return response.data.results; - return {'LOFAR Observing Time': 3600, + 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}; + 'LOFAR Support Time': 3600}); } catch (error) { console.error(error); } @@ -99,6 +64,16 @@ const ProjectService = { 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); @@ -108,6 +83,24 @@ const ProjectService = { 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/')); @@ -122,6 +115,16 @@ const ProjectService = { } 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; } @@ -134,7 +137,63 @@ const ProjectService = { 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; + } + }) + } + }); + 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..271e6f4a5376d5b1df857c33f10b2ca9df49ee18 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/services/schedule.service.js @@ -0,0 +1,133 @@ +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; + }, +<<<<<<< Updated upstream +======= + + getSchedulingUnitBlueprint: async function (){ + let res = []; + await axios.get('/api/scheduling_unit_blueprint/?ordering=id') + .then(response => { + res= response; + }).catch(function(error) { + console.error('[schedule.services.getSchedulingUnitBlueprint]',error); + }); + return res; + }, + +>>>>>>> Stashed changes + 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; + }, +<<<<<<< Updated upstream +======= + +>>>>>>> Stashed changes + 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.services.js b/SAS/TMSS/frontend/tmss_webapp/src/services/task.service.js similarity index 100% rename from SAS/TMSS/frontend/tmss_webapp/src/services/task.services.js rename to SAS/TMSS/frontend/tmss_webapp/src/services/task.service.js diff --git a/SAS/TMSS/frontend/tmss_webapp/src/utils/unit.converter.js b/SAS/TMSS/frontend/tmss_webapp/src/utils/unit.converter.js index 4e5ad901b79f84e741024001eebc54f8d69b6bc6..bd492544a3ef58793a6bfd24e996757d78d0e132 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/utils/unit.converter.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/utils/unit.converter.js @@ -1,5 +1,5 @@ const UnitConverter = { - resourceUnitMap: {'seconds':{display: 'Hours', conversionFactor: 3600, mode:'decimal', minFractionDigits:0, maxFractionDigits: 2 }, + 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}}, @@ -16,9 +16,8 @@ const UnitConverter = { }catch(error){ console.error('[unit.converter.getUIResourceUnit]',error); } - return value - }, - + return value; + } }; export default UnitConverter; \ No newline at end of file