From 577cae15878be2b8eca19c2151f6c38eeb8b5130 Mon Sep 17 00:00:00 2001 From: Muthu <rspmkn@gmail.com> Date: Wed, 26 Aug 2020 14:58:38 +0530 Subject: [PATCH] TMSS-297 : Component for page header updated Component for page header updated --- SAS/TMSS/frontend/tmss_webapp/src/App.js | 3 +- .../tmss_webapp/src/layout/_overrides.scss | 11 + .../src/layout/components/AppHeader.js | 34 ++ .../src/routes/Cycle/ResourceDisplayList.js | 31 ++ .../src/routes/Cycle/ResourceInputList.js | 58 ++ .../tmss_webapp/src/routes/Cycle/create.js | 477 ++++++++++++++++ .../src/routes/Cycle/create.test.js | 346 ++++++++++++ .../tmss_webapp/src/routes/Cycle/edit.js | 507 ++++++++++++++++++ .../tmss_webapp/src/routes/Cycle/index.js | 6 + .../tmss_webapp/src/routes/Cycle/list.js | 160 ++++++ .../tmss_webapp/src/routes/Cycle/list.test.js | 69 +++ .../tmss_webapp/src/routes/Cycle/view.js | 143 +++++ .../tmss_webapp/src/routes/Project/create.js | 27 +- .../tmss_webapp/src/routes/Project/edit.js | 12 +- .../tmss_webapp/src/routes/Project/list.js | 11 +- .../tmss_webapp/src/routes/Project/view.js | 20 +- .../routes/Scheduling/SchedulingUnitList.js | 45 +- .../routes/Scheduling/ViewSchedulingUnit.js | 81 +-- .../src/routes/Scheduling/index.js | 3 +- .../tmss_webapp/src/routes/Task/edit.js | 12 +- .../tmss_webapp/src/routes/Task/view.js | 15 +- .../frontend/tmss_webapp/src/routes/index.js | 36 +- .../tmss_webapp/src/services/cycle.service.js | 123 ++++- .../src/services/schedule.service.js | 12 +- 24 files changed, 2141 insertions(+), 101 deletions(-) create mode 100644 SAS/TMSS/frontend/tmss_webapp/src/layout/components/AppHeader.js create mode 100644 SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/ResourceDisplayList.js create mode 100644 SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/ResourceInputList.js create mode 100644 SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/create.js create mode 100644 SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/create.test.js create mode 100644 SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/edit.js create mode 100644 SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/index.js create mode 100644 SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/list.js create mode 100644 SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/list.test.js create mode 100644 SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/view.js diff --git a/SAS/TMSS/frontend/tmss_webapp/src/App.js b/SAS/TMSS/frontend/tmss_webapp/src/App.js index c35c53eb523..5bce851b911 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/App.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/App.js @@ -36,6 +36,7 @@ class App extends Component { this.menu = [ {label: 'Dashboard', icon: 'pi pi-fw pi-home', to:'/dashboard'}, + {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: 'Project', icon: 'fa fa-fw fa-binoculars', to:'/project'} @@ -117,8 +118,6 @@ class App extends Component { <AppMenu model={this.menu} onMenuItemClick={this.onMenuItemClick} /> <div className="layout-main"> <AppBreadCrumbWithRouter setPageTitle={this.setPageTitle} /> - {/* Display current pagename */} - <h2>{this.state.PageTitle}</h2> <RoutedContent /> </div> </Router> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/layout/_overrides.scss b/SAS/TMSS/frontend/tmss_webapp/src/layout/_overrides.scss index ddf77d3e4b2..87a69cd8d25 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/layout/_overrides.scss +++ b/SAS/TMSS/frontend/tmss_webapp/src/layout/_overrides.scss @@ -22,4 +22,15 @@ .layout-sidebar-dark .layout-menu li a { border-top: none; } +} +.app-header { + display: flex; + justify-content: space-between; + align-items: baseline; +} +.app-header .app-header-name { + margin-bottom: 0; +} +.app-header .app-header-actions i { + margin-left: 20px; } \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/layout/components/AppHeader.js b/SAS/TMSS/frontend/tmss_webapp/src/layout/components/AppHeader.js new file mode 100644 index 00000000000..12c866148bf --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/layout/components/AppHeader.js @@ -0,0 +1,34 @@ +import React, { useEffect, useState } from 'react'; +import { routes } from '../../routes'; +import {matchPath, Link} from 'react-router-dom'; + +export default (props) => { + const [PageTitle, setPageTitle] = useState({}); + + useEffect(() => { + const currentRoute = routes.find(route => matchPath(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; + } + setPageTitle(currentRoute); + }, []); + + return ( + <div className="app-header"> + <div className="title"> + <h2 className="app-header-name">{PageTitle.name}</h2> + {PageTitle.subtitle && <h6 className="app-header-subtitle">{PageTitle.subtitle}</h6>} + </div> + <div className="app-header-actions"> + {(props.actions || []).map(action => { + console.log(action.props); + return ( + <Link to={{ ...action.props }}> + <i className={`fa ${action.name}`}></i> + </Link> + )})} + </div> + </div> + ); +} \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/ResourceDisplayList.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/ResourceDisplayList.js new file mode 100644 index 00000000000..c1a00381eeb --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/ResourceDisplayList.js @@ -0,0 +1,31 @@ +import React, {Component} from 'react'; + +/** + * Component to get input for Resource allocation while creating and editing Cycle + */ +class ResourceDisplayList extends Component { + constructor(props) { + super(props); + this.state = { + cycleQuota: props.cycleQuota + } + } + + render(){ + return ( + <> + {this.props.cycleQuota.length>0 && this.props.cycleQuota.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/Cycle/ResourceInputList.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/ResourceInputList.js new file mode 100644 index 00000000000..a188d348b29 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/ResourceInputList.js @@ -0,0 +1,58 @@ +import React, {Component} from 'react'; +import {InputNumber} from 'primereact/inputnumber'; + +/** + * Component to get input for Resource allocation while creating and editing Cycle + */ +export class ResourceInputList extends Component { + constructor(props) { + super(props); + this.state = { + list: props.list, + cycleQuota: props.cycleQuota + } + this.updateEnabled = this.props.list.length===0?true:false; + this.onInputChange = this.onInputChange.bind(this); + } + + shouldComponentUpdate() { + return true; + } + + onInputChange(field, event) { + if (this.props.callback) { + this.props.callback(field, event); + } + } + + removeInput(field) { + if (this.props.removeInputCallback) { + this.props.removeInputCallback(field); + } + } + + render(){ + return ( + <> + {this.props.list.length>0 && this.props.list.map((item, index) => ( + <React.Fragment key={index+10}> + <label key={'label1-'+ index} className="col-lg-2 col-md-2 col-sm-12">{item.name}</label> + <div key={'div1-'+ index} className="col-lg-3 col-md-3 col-sm-12"> + <InputNumber key={'item1-'+ index} id={'item1-'+ index} name={'item1-'+ index} + suffix={` ${this.props.unitMap[item.quantity_value]?this.props.unitMap[item.quantity_value].display:''}`} + placeholder={` ${this.props.unitMap[item.quantity_value]?this.props.unitMap[item.quantity_value].display:item.name}`} min={0} useGrouping={false} + value={this.props.cycleQuota[item.name]} + onChange={(e) => this.onInputChange(item.name, e)} + onBlur={(e) => this.onInputChange(item.name, e)} + style={{width:"90%", marginRight: "5px"}} + /> + <button className="p-link" data-testid={`${item.name}-btn`} onClick={(e) => this.removeInput(item.name)}> + <i className="fa fa-trash pi-error"></i></button> + </div> + <div className="col-lg-1 col-md-1 col-sm-12"></div> + </React.Fragment> + ))} + </> + ); + } +} \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/create.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/create.js new file mode 100644 index 00000000000..be4206019f9 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/create.js @@ -0,0 +1,477 @@ +import React, {Component} from 'react'; +import { Link, Redirect } from 'react-router-dom'; +import {InputText} from 'primereact/inputtext'; +import {Calendar} from 'primereact/calendar'; +import {InputTextarea} from 'primereact/inputtextarea'; +import {Dropdown} from 'primereact/dropdown'; +import {Button} from 'primereact/button'; +import {Dialog} from 'primereact/components/dialog/Dialog'; +import {Growl} from 'primereact/components/growl/Growl'; +import {ResourceInputList} from './ResourceInputList'; +import moment from 'moment' +import _ from 'lodash'; + +import AppLoader from '../../layout/components/AppLoader'; +import AppHeader from '../../layout/components/AppHeader'; +import CycleService from '../../services/cycle.service'; +import UnitConverter from '../../utils/unit.converter'; +import UIConstants from '../../utils/ui.constants'; + +/** + * Component to create a new Cycle + */ +export class CycleCreate extends Component { + constructor(props) { + super(props); + this.state = { + isLoading: true, + dialog: { header: '', detail: ''}, + cycle: { + projects: [], + quota: [], + start: "", + stop: "", + }, + cycleQuota: {}, // Resource Allocations + validFields: {}, // For Validation + validForm: false, // To enable Save Button + errors: {}, // Validation Errors + resources: [], // Selected Resources for Allocation + resourceList: [], // Available Resources for Allocation + } + // Validateion Rules + this.formRules = { + name: {required: true, message: "Name can not be empty"}, + description: {required: true, message: "Description can not be empty"}, + start: {required: true, message: "Start Date can not be empty"}, + stop: {required: true, message: "Stop Date can not be empty"} + }; + this.defaultResourcesEnabled = true; // This property and functionality to be concluded based on PO input + this.defaultResources = [ + {name:'LOFAR Observing Time'}, + {name:'LOFAR Observing Time prio A'}, + {name:'LOFAR Observing Time prio B'}, + {name:'CEP Processing Time'}, + {name:'LTA Storage'}, + {name:'LOFAR LTA resources SARA'}, + {name:'LOFAR LTA resources Jülich'}, + {name:'LOFAR LTA resources Poznan'}, + {name:'LOFAR Observing time DDT/Commissioning'}, + {name:'LOFAR Support Time'}]; + this.cycleResourceDefaults = {}; // Default values for default resources + this.resourceUnitMap = UnitConverter.resourceUnitMap; // Resource unit conversion factor and constraints + this.tooltipOptions = UIConstants.tooltipOptions; + this.setCycleQuotaDefaults = this.setCycleQuotaDefaults.bind(this); + this.addNewResource = this.addNewResource.bind(this); + this.removeResource = this.removeResource.bind(this); + this.setCycleQuotaParams = this.setCycleQuotaParams.bind(this); + this.saveCycle = this.saveCycle.bind(this); + this.cancelCreate = this.cancelCreate.bind(this); + this.reset = this.reset.bind(this); + } + + componentDidMount() { + CycleService.getResources() + .then(resourceList => { + const defaultResources = this.defaultResources; + resourceList = _.sortBy(resourceList, "name"); + const resources = _.remove(resourceList, function(resource) { return _.find(defaultResources, {'name': resource.name})!=null }); + const cycleQuota = this.setCycleQuotaDefaults(resources); + this.setState({resourceList: resourceList, resources: resources, cycleQuota: cycleQuota, isLoading: false}); + }); + } + + /** + * Cycle option sub-component with cycle object + */ + cycleOptionTemplate(option) { + return ( + <div className="p-clearfix"> + <span style={{fontSize:'1em',float:'right',margin:'1em .5em 0 0'}}>{option.name}</span> + </div> + ); + } + + /** + * Function to set cycle resource allocation + * @param {Array} resources + */ + setCycleQuotaDefaults(resources) { + let cycleQuota = this.state.cycleQuota; + for (const resource of resources) { + // const conversionFactor = this.resourceUnitMap[resource.quantity_value]?this.resourceUnitMap[resource.quantity_value].conversionFactor:1; + // cycleQuota[resource['name']] = this.cycleResourceDefaults[resource.name]/conversionFactor; + cycleQuota[resource['name']] = 0; + } + return cycleQuota; + } + + /** + * Function to add new resource to Cycle + */ + addNewResource(){ + if (this.state.newResource) { + let resourceList = this.state.resourceList; + const newResource = _.remove(resourceList, {'name': this.state.newResource}); + let resources = this.state.resources; + resources.push(newResource[0]); + this.setState({resources: resources, resourceList: resourceList, newResource: null}); + } + } + + /** + * Callback function to be called from ResourceInpulList when a resource is removed from it + * @param {string} name - resource_type_id + */ + removeResource(name) { + let resources = this.state.resources; + let resourceList = this.state.resourceList; + let cycleQuota = this.state.cycleQuota; + const removedResource = _.remove(resources, (resource) => { return resource.name === name }); + resourceList.push(removedResource[0]); + resourceList = _.sortBy(resourceList, 'name'); + delete cycleQuota[name]; + this.setState({resourceList: resourceList, resources: resources, cycleQuota: cycleQuota}); + } + + /** + * Function to call on change and blur events from input components + * @param {string} key + * @param {any} value + */ + setCycleParams(key, value, type) { + let cycle = this.state.cycle; + switch(type) { + case 'NUMBER': { + cycle[key] = value?parseInt(value):0; + break; + } + default: { + cycle[key] = value; + break; + } + + } + this.setState({cycle: cycle, validForm: this.validateForm(key)}); + } + + /** + * Callback Function to call from ResourceInputList on change and blur events + * @param {string} key + * @param {InputEvent} event + */ + setCycleQuotaParams(key, event) { + let cycleQuota = this.state.cycleQuota; + if (event.target.value) { + let resource = _.find(this.state.resources, {'name': key}); + + let newValue = 0; + if (this.resourceUnitMap[resource.quantity_value] && + event.target.value.toString().indexOf(this.resourceUnitMap[resource.quantity_value].display)>=0) { + newValue = event.target.value.replace(this.resourceUnitMap[resource.quantity_value].display,''); + } else { + newValue = event.target.value; + } + cycleQuota[key] = (newValue==="NaN" || isNaN(newValue))?0:newValue; + } else { + let cycleQuota = this.state.cycleQuota; + cycleQuota[key] = 0; + } + this.setState({cycleQuota: cycleQuota}); + } + + /** + * Validation function to validate the form or field based on the form rules. + * If no argument passed for fieldName, validates all fields in the form. + * @param {string} fieldName + */ + validateForm(fieldName) { + let validForm = false; + let errors = this.state.errors; + let validFields = this.state.validFields; + if (fieldName) { + delete errors[fieldName]; + delete validFields[fieldName]; + if (this.formRules[fieldName]) { + const rule = this.formRules[fieldName]; + const fieldValue = this.state.cycle[fieldName]; + if (rule.required) { + if (!fieldValue) { + errors[fieldName] = rule.message?rule.message:`${fieldName} is required`; + } else { + validFields[fieldName] = true; + } + } + } + } else { + errors = {}; + validFields = {}; + for (const fieldName in this.formRules) { + const rule = this.formRules[fieldName]; + const fieldValue = this.state.cycle[fieldName]; + if (rule.required) { + if (!fieldValue) { + errors[fieldName] = rule.message?rule.message:`${fieldName} is required`; + } else { + validFields[fieldName] = true; + } + } + } + } + + this.setState({errors: errors, validFields: validFields}); + if (Object.keys(validFields).length === Object.keys(this.formRules).length) { + validForm = true; + } + + if(this.state.cycle['start'] && this.state.cycle['stop']){ + var isSameOrAfter = moment(this.state.cycle['stop']).isSameOrAfter(this.state.cycle['start']); + if(!isSameOrAfter){ + errors['stop'] = ` Stop date can not be before Start date`; + validForm = false; + }else{ + validForm = true; + } + } + return validForm; + } + + /** + * Function to call when 'Save' button is clicked to save the Cycle. + */ + saveCycle() { + if (this.validateForm) { + let cycleQuota = []; + let cycle = this.state.cycle; + let stoptime = _.replace(this.state.cycle['stop'],'00:00:00', '23:59:59'); + cycle['start'] = moment(cycle['start']).format("YYYY-MM-DDTHH:mm:ss"); + cycle['stop'] = moment(stoptime).format("YYYY-MM-DDTHH:mm:ss"); + this.setState({cycle: cycle}); + for (const resource in this.state.cycleQuota) { + let resourceType = _.find(this.state.resources, {'name': resource}); + if(resourceType){ + let quota = { cycle: this.state.cycle.name, + resource_type: resourceType['url'], + value: this.state.cycleQuota[resource] * (this.resourceUnitMap[resourceType.quantity_value]?this.resourceUnitMap[resourceType.quantity_value].conversionFactor:1)}; + cycleQuota.push(quota); + } + + } + + CycleService.saveCycle(this.state.cycle, this.defaultResourcesEnabled?cycleQuota:[]) + .then(cycle => { + if (cycle.url) { + let dialog = {}; + if (this.defaultResourcesEnabled) { + dialog = {header: 'Success', detail: 'Cycle saved successfully. Do you want to create another Cycle?'}; + } else { + dialog = {header: 'Success', detail: 'Cycle saved successfully with default Resource allocations. Do you want to view and edit them?'}; + } + this.setState({cycle:cycle, dialogVisible: true, dialog: dialog}) + } else { + this.growl.show({severity: 'error', summary: 'Error Occured', detail: 'Unable to save Cycle'}); + this.setState({errors: cycle}); + } + }); + } + } + + /** + * Function to cancel form creation and navigate to other page/component + */ + cancelCreate() { + this.setState({redirect: '/cycle'}); + } + + /** + * Reset function to be called to reset the form fields + */ + reset() { + if (this.defaultResourcesEnabled) { + let prevResources = this.state.resources; + let resourceList = []; + let resources = []; + if (resources) { + const defaultResources = this.defaultResources; + resourceList = _.sortBy(prevResources.concat(this.state.resourceList), "name"); + resources = _.remove(resourceList, function(resource) { return _.find(defaultResources, {'name': resource.name})!=null }); + } + const cycleQuota = this.setCycleQuotaDefaults(resources); + this.setState({ + dialog: { header: '', detail: ''}, + cycle: { + name: '', + description: '', + start: '', + stop: '', + projects: [], + quota: [], + }, + cycleQuota: cycleQuota, + validFields: {}, + validForm: false, + errors: {}, + dialogVisible: false, + resources: resources, + resourceList: resourceList + }); + } else { + this.setState({redirect: `/cycle/edit/${this.state.cycle.name}`}) + } + } + + render() { + if (this.state.redirect) { + return <Redirect to={ {pathname: this.state.redirect} }></Redirect> + } + + return ( + <React.Fragment> + { /*<div className="p-grid"> + <Growl ref={(el) => this.growl = el} /> + + <div className="p-col-10 p-lg-10 p-md-10"> + <h2>Cycle - Add</h2> + </div> + <div className="p-col-2 p-lg-2 p-md-2"> + <Link to={{ pathname: '/cycle'}} tite="Close Edit" style={{float: "right"}}> + <i className="fa fa-window-close" style={{marginTop: "10px"}}></i> + </Link> + </div> + </div> */ } + <AppHeader location={this.props.location} actions={[{name: 'fa-window-close', props:{pathname: '/cycle' }}]}/> + { this.state.isLoading ? <AppLoader /> : + <> + <div> + <div className="p-fluid"> + <div className="p-field p-grid" style={{display: 'none'}}> + <label htmlFor="cycleId" className="col-lg-2 col-md-2 col-sm-12">URL </label> + <div className="col-lg-4 col-md-4 col-sm-12"> + <input id="cycleId" data-testid="cycleId" value={this.state.cycle.url} /> + </div> + </div> + <div className="p-field p-grid"> + <label htmlFor="cycleName" className="col-lg-2 col-md-2 col-sm-12">Name <span style={{color:'red'}}>*</span></label> + <div className="col-lg-3 col-md-3 col-sm-12"> + <InputText className={this.state.errors.name ?'input-error':''} id="cycleName" data-testid="name" + tooltip="Enter name of the cycle" tooltipOptions={this.tooltipOptions} maxLength="128" + value={this.state.cycle.name} + onChange={(e) => this.setCycleParams('name', e.target.value)} + onBlur={(e) => this.setCycleParams('name', e.target.value)}/> + <label className={this.state.errors.name?"error":"info"}> + {this.state.errors.name ? this.state.errors.name : "Max 128 characters"} + </label> + </div> + <div className="col-lg-1 col-md-1 col-sm-12"></div> + <label htmlFor="description" className="col-lg-2 col-md-2 col-sm-12">Description <span style={{color:'red'}}>*</span></label> + <div className="col-lg-3 col-md-3 col-sm-12"> + <InputTextarea className={this.state.errors.description ?'input-error':''} rows={3} cols={30} + tooltip="Short description of the cycle" tooltipOptions={this.tooltipOptions} maxLength="128" + data-testid="description" value={this.state.cycle.description} + onChange={(e) => this.setCycleParams('description', e.target.value)} + onBlur={(e) => this.setCycleParams('description', e.target.value)}/> + <label className={this.state.errors.description ?"error":"info"}> + {this.state.errors.description ? this.state.errors.description : "Max 255 characters"} + </label> + </div> + </div> + + <div className="p-field p-grid"> + <label htmlFor="cycleName" className="col-lg-2 col-md-2 col-sm-12">Start Date <span style={{color:'red'}}>*</span></label> + <div className="col-lg-3 col-md-3 col-sm-12"> + <Calendar + d dateFormat="dd-M-yy" + value= {this.state.cycle.start} + onChange= {e => this.setCycleParams('start',e.value)} + onBlur= {e => this.setCycleParams('start',e.value)} + data-testid="start" + tooltip="Moment at which the cycle starts, that is, when its projects can run." tooltipOptions={this.tooltipOptions} + showIcon={true} + /> + + <label className={this.state.errors.start?"error":"info"}> + {this.state.errors.start ? this.state.errors.start : ""} + </label> + </div> + <div className="col-lg-1 col-md-1 col-sm-12"></div> + <label htmlFor="cycleName" className="col-lg-2 col-md-2 col-sm-12">Stop Date <span style={{color:'red'}}>*</span></label> + <div className="col-lg-3 col-md-3 col-sm-12"> + <Calendar + d dateFormat="dd-M-yy" + value= {this.state.cycle.stop} + onChange= {e => this.setCycleParams('stop', e.value)} + onBlur= {e => this.setCycleParams('stop',e.value)} + data-testid="stop" + tooltip="Moment at which the cycle officially ends." tooltipOptions={this.tooltipOptions} + showIcon={true} + /> + + <label className={this.state.errors.stop?"error":"info"}> + {this.state.errors.stop ? this.state.errors.stop : ""} + </label> + </div> + </div> + + {this.defaultResourcesEnabled && this.state.resourceList && + <div className="p-fluid"> + <div className="p-field p-grid"> + <div className="col-lg-2 col-md-2 col-sm-12"> + <h5 data-testid="resource_alloc">Resource Allocations</h5> + </div> + <div className="col-lg-3 col-md-3 col-sm-10"> + <Dropdown optionLabel="name" optionValue="name" + tooltip="Resources to be allotted for the cycle" + tooltipOptions={this.tooltipOptions} + value={this.state.newResource} + options={this.state.resourceList} + onChange={(e) => {this.setState({'newResource': e.value})}} + placeholder="Add Resources" /> + </div> + <div className="col-lg-2 col-md-2 col-sm-2"> + <Button label="" className="p-button-primary" icon="pi pi-plus" onClick={this.addNewResource} data-testid="add_res_btn" /> + </div> + </div> + <div className="p-field p-grid resource-input-grid"> + <ResourceInputList list={this.state.resources} unitMap={this.resourceUnitMap} + cycleQuota={this.state.cycleQuota} callback={this.setCycleQuotaParams} + removeInputCallback={this.removeResource} /> + </div> + </div> + } + </div> + </div> + <div className="p-grid p-justify-start act-btn-grp"> + <div className="p-col-1"> + <Button label="Save" className="p-button-primary" id="save-btn" data-testid="save-btn" icon="pi pi-check" onClick={this.saveCycle} disabled={!this.state.validForm} /> + </div> + <div className="p-col-1"> + <Button label="Cancel" className="p-button-danger" icon="pi pi-times" onClick={this.cancelCreate} /> + </div> + </div> + </> + } + + {/* Dialog component to show messages and get input */} + <div className="p-grid" data-testid="confirm_dialog"> + <Dialog header={this.state.dialog.header} visible={this.state.dialogVisible} style={{width: '25vw'}} inputId="confirm_dialog" + modal={true} onHide={() => {this.setState({dialogVisible: false})}} + footer={<div> + <Button key="back" onClick={() => {this.setState({dialogVisible: false}); this.cancelCreate();}} label="No" /> + <Button key="submit" type="primary" onClick={this.reset} label="Yes" /> + </div> + } > + <div className="p-grid"> + <div className="col-lg-2 col-md-2 col-sm-2"> + <i className="pi pi-check-circle pi-large pi-success"></i> + </div> + <div className="col-lg-10 col-md-10 col-sm-10"> + <span style={{marginTop:"5px"}}>{this.state.dialog.detail}</span> + </div> + </div> + </Dialog> + </div> + + </React.Fragment> + ); + } +} \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/create.test.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/create.test.js new file mode 100644 index 00000000000..d738a4a909a --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/create.test.js @@ -0,0 +1,346 @@ +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 {CycleCreate} from './create'; +import CycleService from '../../services/cycle.service'; + +import CycleServiceMock from '../../__mocks__/cycle.service.data'; + +let saveCycleSpy, resourcesSpy, cycleResourceDefaultsSpy; + +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 = (() => { + + resourcesSpy = jest.spyOn(CycleService, 'getResources'); + resourcesSpy.mockImplementation(() => { + return Promise.resolve(CycleServiceMock.resources); + }); + + + saveCycleSpy = jest.spyOn(CycleService, 'saveCycle'); + saveCycleSpy.mockImplementation((cycle, cycleQuota) => { + cycle.url = `http://localhost:3000/api/cycle/${cycle.name}`; + return Promise.resolve(cycle) + }); +}); + +const clearMockSpy = (() => { + saveCycleSpy.mockRestore(); +}); + +it("renders without crashing with all back-end data loaded", async () => { + console.log("renders without crashing with all back-end data loaded ------------------------"); + + let content; + await act(async () => { + content = render(<Router><CycleCreate /></Router>); + }); + + expect(content.queryByText('Cycle - Add')).not.toBe(null); // Page loaded successfully + expect(content.queryAllByText('Add Resources').length).toBe(2); // Resource Dropdown loaded successfully + expect(content.queryByText('Support hours')).toBeInTheDocument(); // Resources other than Default Resources listed in dropdown + expect(content.queryByPlaceholderText('Support Hours')).toBe(null); // No resources other than Default Resources listed to get input + expect(content.queryByPlaceholderText('LOFAR Observing Time').value).toBe('1 Hours'); // Default Resource Listed with default value +}); + +it("Save button disabled initially when no data entered", async () => { + console.log("Save button disabled initially when no data entered -----------------------"); + let content; + await act(async () => { + content = render(<Router><CycleCreate /></Router>); + }); + expect(content.queryByTestId('save-btn')).toHaveAttribute("disabled"); +}); + +it("Save button enabled when mandatory data entered", async () => { + console.log("Save button enabled when mandatory data entered -----------------------"); + let content; + await act(async () => { + content = render(<Router><CycleCreate /></Router>); + }); + const nameInput = content.queryByTestId('name'); + const descInput = content.queryByTestId('description'); + const startInput = content.queryByTestId('start'); + const stopInput = content.queryByTestId('stop'); + + fireEvent.change(nameInput, { target: { value: 'Cycle-12' } }); + expect(nameInput.value).toBe("Cycle-12"); + + fireEvent.change(descInput, { target: { value: 'Cycle-12' } }); + expect(descInput.value).toBe("Cycle-12"); + + fireEvent.change(startInput, { target: { value: '2020-07-29 11:12:15' } }); + expect(startInput.value).toBe("2020-07-29 11:12:15"); + + fireEvent.change(stopInput, { target: { value: '2020-07-30 11:12:15' } }); + expect(stopInput.value).toBe("2020-07-30 11:12:15"); + expect(content.queryByTestId('save-btn').hasAttribute("disabled")).toBeFalsy(); +}); + +it("renders Save button enabled when all data entered", async () => { + console.log("renders Save button enabled when all data entered -----------------------"); + let content; + await act(async () => { + content = render(<Router><CycleCreate /></Router>); + }); + + const nameInput = content.queryByTestId('name'); + const descInput = content.queryByTestId('description'); + const startInput = content.queryByTestId('start'); + const stopInput = content.queryByTestId('stop'); + + fireEvent.change(nameInput, { target: { value: 'Cycle-12' } }); + expect(nameInput.value).toBe("Cycle-12"); + + fireEvent.change(descInput, { target: { value: 'Cycle-12' } }); + expect(descInput.value).toBe("Cycle-12"); + + fireEvent.change(startInput, { target: { value: '2020-07-29 11:12:15' } }); + expect(startInput.value).toBe("2020-07-29 11:12:15"); + + fireEvent.change(stopInput, { target: { value: '2020-07-30 11:12:15' } }); + expect(stopInput.value).toBe("2020-07-30 11:12:15"); + + expect(content.queryByTestId('save-btn').hasAttribute("disabled")).toBeFalsy(); + +}); + +it("save cycle with mandatory fields", async () => { + console.log("save cycle -----------------------"); + let content; + await act(async () => { + content = render(<Router><CycleCreate /></Router>); + }); + + const nameInput = content.queryByTestId('name'); + const descInput = content.queryByTestId('description'); + const startInput = content.queryByTestId('start'); + const stopInput = content.queryByTestId('stop'); + + fireEvent.change(nameInput, { target: { value: 'Cycle-12' } }); + expect(nameInput.value).toBe("Cycle-12"); + fireEvent.change(descInput, { target: { value: 'Cycle-12' } }); + expect(descInput.value).toBe("Cycle-12"); + + fireEvent.change(startInput, { target: { value: '2020-07-29 11:12:15' } }); + expect(startInput.value).toBe("2020-07-29 11:12:15"); + + fireEvent.change(stopInput, { target: { value: '2020-07-30 11:12:15' } }); + expect(stopInput.value).toBe("2020-07-30 11:12:15"); + + expect(content.queryByTestId('save-btn').hasAttribute("disabled")).toBeFalsy(); + expect(content.queryByTestId('cycleId').value).toBe(""); + expect(content.queryByText("Success")).toBe(null); + + await act(async () => { + fireEvent.click(content.queryByTestId('save-btn')); + }); + + // After saving cycle, URL should be available and Success dialog should be displayed + expect(content.queryByTestId('cycleId').value).toBe("http://localhost:3000/api/cycle/Cycle-12"); + expect(content.queryByText("Success")).not.toBe(null); +}); + +it("save cycle with default resources", async () => { + console.log("save cycle with default resources -----------------------"); + let content; + await act(async () => { + content = render(<Router><CycleCreate /></Router>); + }); + + const nameInput = content.queryByTestId('name'); + const descInput = content.queryByTestId('description'); + const startInput = content.queryByTestId('start'); + const stopInput = content.queryByTestId('stop'); + + fireEvent.change(nameInput, { target: { value: 'Cycle-12' } }); + expect(nameInput.value).toBe("Cycle-12"); + fireEvent.change(descInput, { target: { value: 'Cycle-12' } }); + expect(descInput.value).toBe("Cycle-12"); + + fireEvent.change(startInput, { target: { value: '2020-07-29 11:12:15' } }); + expect(startInput.value).toBe("2020-07-29 11:12:15"); + + fireEvent.change(stopInput, { target: { value: '2020-07-30 11:12:15' } }); + expect(stopInput.value).toBe("2020-07-30 11:12:15"); + + expect(content.queryByTestId('save-btn').hasAttribute("disabled")).toBeFalsy(); + expect(content.queryByTestId('cycleId').value).toBe(""); + expect(content.queryByText("Success")).toBe(null); + + const lofarObsTimeInput = content.queryByPlaceholderText('LOFAR Observing Time'); + fireEvent.change(lofarObsTimeInput, { target: { value: 10 } }); + expect(lofarObsTimeInput.value).toBe('10'); + + const lofarObsTimeAInput = content.queryByPlaceholderText('LOFAR Observing Time prio A'); + fireEvent.change(lofarObsTimeAInput, { target: { value: 15 } }); + expect(lofarObsTimeAInput.value).toBe('15'); + + const lofarObsTimeBInput = content.queryByPlaceholderText('LOFAR Observing Time prio B'); + fireEvent.change(lofarObsTimeBInput, { target: { value: 20 } }); + expect(lofarObsTimeBInput.value).toBe('20'); + + const cepProcTimeInput = content.queryByPlaceholderText('CEP Processing Time'); + fireEvent.change(cepProcTimeInput, { target: { value: 5 } }); + expect(cepProcTimeInput.value).toBe('5'); + + const ltaStorageInput = content.queryByPlaceholderText('LTA Storage'); + fireEvent.change(ltaStorageInput, { target: { value: 2 } }); + expect(ltaStorageInput.value).toBe('2'); + + const noOfTriggerInput = content.queryByPlaceholderText('Number of triggers'); + fireEvent.change(noOfTriggerInput, { target: { value: 3 } }); + expect(noOfTriggerInput.value).toBe('3'); + + const lofarSupTimeInput = content.queryByPlaceholderText('LOFAR Support Time'); + fireEvent.change(lofarSupTimeInput, { target: { value: 25 } }); + expect(lofarSupTimeInput.value).toBe('25'); + + await act(async () => { + fireEvent.click(content.queryByTestId('save-btn')); + }); + + // After saving cycle, URL should be available and Success dialog should be displayed + expect(content.queryByTestId('cycleId').value).toBe("http://localhost:3000/api/cycle/Cycle-12"); + expect(content.queryByText("Success")).not.toBe(null); +}); + +it("save cycle with added resources", async () => { + console.log("save cycle with added resources -----------------------"); + let content; + await act(async () => { + content = render(<Router><CycleCreate /></Router>); + }); + + const nameInput = content.queryByTestId('name'); + const descInput = content.queryByTestId('description'); + const startInput = content.queryByTestId('start'); + const stopInput = content.queryByTestId('stop'); + + fireEvent.change(nameInput, { target: { value: 'Cycle-12' } }); + expect(nameInput.value).toBe("Cycle-12"); + fireEvent.change(descInput, { target: { value: 'Cycle-12' } }); + expect(descInput.value).toBe("Cycle-12"); + + fireEvent.change(startInput, { target: { value: '2020-07-29 11:12:15' } }); + expect(startInput.value).toBe("2020-07-29 11:12:15"); + + fireEvent.change(stopInput, { target: { value: '2020-07-30 11:12:15' } }); + expect(stopInput.value).toBe("2020-07-30 11:12:15"); + + expect(content.queryByTestId('save-btn').hasAttribute("disabled")).toBeFalsy(); + expect(content.queryByTestId('cycleId').value).toBe(""); + expect(content.queryByText("Success")).toBe(null); + + const lofarObsTimeInput = content.queryByPlaceholderText('LOFAR Observing Time'); + fireEvent.change(lofarObsTimeInput, { target: { value: 10 } }); + expect(lofarObsTimeInput.value).toBe('10'); + + const lofarObsTimeAInput = content.queryByPlaceholderText('LOFAR Observing Time prio A'); + fireEvent.change(lofarObsTimeAInput, { target: { value: 15 } }); + expect(lofarObsTimeAInput.value).toBe('15'); + + const lofarObsTimeBInput = content.queryByPlaceholderText('LOFAR Observing Time prio B'); + fireEvent.change(lofarObsTimeBInput, { target: { value: 20 } }); + expect(lofarObsTimeBInput.value).toBe('20'); + + const cepProcTimeInput = content.queryByPlaceholderText('CEP Processing Time'); + fireEvent.change(cepProcTimeInput, { target: { value: 5 } }); + expect(cepProcTimeInput.value).toBe('5'); + + const ltaStorageInput = content.queryByPlaceholderText('LTA Storage'); + fireEvent.change(ltaStorageInput, { target: { value: 2 } }); + expect(ltaStorageInput.value).toBe('2'); + + const noOfTriggerInput = content.queryByPlaceholderText('Number of triggers'); + fireEvent.change(noOfTriggerInput, { target: { value: 3 } }); + expect(noOfTriggerInput.value).toBe('3'); + + const lofarSupTimeInput = content.queryByPlaceholderText('LOFAR Support Time'); + fireEvent.change(lofarSupTimeInput, { target: { value: 25 } }); + expect(lofarSupTimeInput.value).toBe('25'); + + // Before selecting New Resource + expect(content.queryAllByText('Add Resources').length).toBe(2); + expect(content.queryAllByText('Support hours').length).toBe(1); + expect(content.getAllByRole("listbox")[3].children.length).toBe(2); + expect(content.queryByPlaceholderText('Support hours')).toBe(null); + const addResourceInput = content.getAllByRole("listbox")[3].children[1] ; + fireEvent.click(addResourceInput); + // After selecting New Resource + expect(content.queryAllByText('Add Resources').length).toBe(1); + expect(content.queryAllByText('Support hours').length).toBe(3); + + const addResourceBtn = content.queryByTestId('add_res_btn'); + fireEvent.click(addResourceBtn); + expect(content.queryAllByText('Add Resources').length).toBe(2); + expect(content.queryByPlaceholderText('Support hours')).not.toBe(null); + + const newResourceInput = content.queryByPlaceholderText('Support hours'); + fireEvent.change(newResourceInput, { target: { value: 30 } }); + expect(newResourceInput.value).toBe('30'); + + + await act(async () => { + fireEvent.click(content.queryByTestId('save-btn')); + }); + + // After saving cycle, URL should be available and Success dialog should be displayed + expect(content.queryByTestId('cycleId').value).toBe("http://localhost:3000/api/cycle/Cycle-12"); + expect(content.queryByText("Success")).not.toBe(null); +}); + +it("remove default resource and added resource", async () => { + console.log("remove default resource and added resource -----------------------"); + let content; + await act(async () => { + content = render(<Router><CycleCreate /></Router>); + }); + + // Before selecting New Resource + expect(content.queryAllByText('Add Resources').length).toBe(2); + expect(content.queryAllByText('Support hours').length).toBe(1); + expect(content.getAllByRole("listbox")[3].children.length).toBe(2); + expect(content.queryByPlaceholderText('Support hours')).toBe(null); + const addResourceInput = content.getAllByRole("listbox")[3].children[1] ; + fireEvent.click(addResourceInput); + // After selecting New Resource + expect(content.queryAllByText('Add Resources').length).toBe(1); + expect(content.queryAllByText('Support hours').length).toBe(3); + + const addResourceBtn = content.queryByTestId('add_res_btn'); + fireEvent.click(addResourceBtn); + expect(content.queryAllByText('Add Resources').length).toBe(2); + expect(content.queryByPlaceholderText('Support hours')).not.toBe(null); + + expect(content.queryByPlaceholderText('CEP Processing Time')).not.toBe(null); + expect(content.queryByTestId('CEP Processing Time-btn')).not.toBe(null); + const removeDefResBtn = content.queryByTestId('CEP Processing Time-btn'); + await act(async () => { + fireEvent.click(content.queryByTestId('CEP Processing Time-btn')); + }); + expect(content.queryByPlaceholderText('CEP Processing Time')).toBe(null); + expect(content.queryByTestId('CEP Processing Time-btn')).toBe(null); + + const removeResourceBtn = content.queryByTestId('Support hours-btn'); + fireEvent.click(removeResourceBtn); + expect(content.queryAllByText('Add Resources').length).toBe(2); + expect(content.queryAllByText('Support hours').length).toBe(1); + expect(content.getAllByRole("listbox")[3].children.length).toBe(3); + +}); \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/edit.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/edit.js new file mode 100644 index 00000000000..66e9b157cd3 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/edit.js @@ -0,0 +1,507 @@ +import React, {Component} from 'react'; +import { Link, Redirect } from 'react-router-dom'; +import _ from 'lodash'; +import moment from 'moment' + +import {InputText} from 'primereact/inputtext'; +import {Calendar} from 'primereact/calendar'; +import {InputTextarea} from 'primereact/inputtextarea'; +import {Dropdown} from 'primereact/dropdown'; +import { Button } from 'primereact/button'; +import {Dialog} from 'primereact/components/dialog/Dialog'; +import {Growl} from 'primereact/components/growl/Growl'; + +import {ResourceInputList} from './ResourceInputList'; + +import AppLoader from '../../layout/components/AppLoader'; +import AppHeader from '../../layout/components/AppHeader'; +import CycleService from '../../services/cycle.service'; +import UnitConverter from '../../utils/unit.converter'; +import UIConstants from '../../utils/ui.constants'; + +export class CycleEdit extends Component { + constructor(props) { + super(props); + this.state = { + isLoading: true, + dialog: { header: '', detail: ''}, + cycle: { + projects: [], + quota: [], // Mandatory Field in the back end + }, + cycleQuota: {}, // 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: {}, + resources: [], // Selected resources for the cycle + resourceList: [], // Available resources to select for the cycle + redirect: this.props.match.params.id?"":'/cycle/list' //If no cycle name passed redirect to Cycle list page + } + this.cycleQuota = [] // Holds the old list of cycle_quota saved for the cycle + // Validation Rules + this.formRules = { + name: {required: true, message: "Name can not be empty"}, + description: {required: true, message: "Description can not be empty"}, + start: {required: true, message: "Start Date can not be empty"}, + stop: {required: true, message: "Stop Date can not be empty"}, + }; + this.defaultResources = [ + {name:'LOFAR Observing Time'}, + {name:'LOFAR Observing Time prio A'}, + {name:'LOFAR Observing Time prio B'}, + {name:'LOFAR Processing time '}, + {name:'LOFAR LTA resources'}, + {name:'LOFAR LTA resources SARA'}, + {name:'LOFAR LTA resources Jülich'}, + {name:'LOFAR LTA resources Poznan'}, + {name:'LOFAR Observing time DDT/Commissioning'}, + {name:'LOFAR Support'}]; + this.cycleResourceDefaults = {}; + this.resourceUnitMap = UnitConverter.resourceUnitMap; + this.tooltipOptions = UIConstants.tooltipOptions; + + this.getCycleDetails = this.getCycleDetails.bind(this); + this.cycleOptionTemplate = this.cycleOptionTemplate.bind(this); + this.setCycleQuotaDefaults = this.setCycleQuotaDefaults.bind(this); + this.setCycleParams = this.setCycleParams.bind(this); + this.addNewResource = this.addNewResource.bind(this); + this.removeResource = this.removeResource.bind(this); + this.setCycleQuotaParams = this.setCycleQuotaParams.bind(this); + this.saveCycle = this.saveCycle.bind(this); + this.saveCycleQuota = this.saveCycleQuota.bind(this); + this.cancelEdit = this.cancelEdit.bind(this); + } + + componentDidMount() { + CycleService.getCycle(this.props.match.params.id) + .then(result =>{ + this.setState({ + cycle: result.data, + isLoading : false, + }) + }) + .then(()=>{ + CycleService.getResources() + .then(resourceList => { + this.setState({resourceList: resourceList}); + }) + .then((resourceList, resources) => { + this.getCycleDetails(); + }); + }) + } + + /** + * Function retrieve cycle details and resource allocations(cycle_quota) and assign to appropriate varaibles + */ + async getCycleDetails() { + let cycle = this.state.cycle; + let resourceList = this.state.resourceList; + let cycleQuota = {}; + if (cycle) { + // Get cycle_quota for the cycle and asssign to the component variable + for (const id of cycle.quota_ids) { + let quota = await CycleService.getCycleQuota(id); + let resource = _.find(resourceList, ['name', quota.resource_type_id]); + quota.resource = resource; + this.cycleQuota.push(quota); + const conversionFactor = this.resourceUnitMap[resource.quantity_value]?this.resourceUnitMap[resource.quantity_value].conversionFactor:1; + cycleQuota[quota.resource_type_id] = quota.value / conversionFactor; + }; + // Remove the already assigned resources from the resoureList + const resources = _.remove(resourceList, (resource) => { return _.find(this.cycleQuota, {'resource_type_id': resource.name})!=null }); + this.setState({cycle: cycle, resourceList: resourceList, resources: resources, + cycleQuota: cycleQuota, isLoading: false}); + + // Validate form if all values are as per the form rules and enable Save button + this.validateForm(); + } else { + this.setState({redirect: '../../not-found'}); + } + } + + /** + * Cycle option sub-component with cycle object + */ + cycleOptionTemplate(option) { + return ( + <div className="p-clearfix"> + <span style={{fontSize:'1em',float:'right',margin:'1em .5em 0 0'}}>{option.name}</span> + </div> + ); + } + + /** + * Function to set cycle resource allocation + * @param {Array} resources + */ + setCycleQuotaDefaults(resources) { + let cycleQuota = this.state.cycleQuota; + for (const resource of resources) { + const conversionFactor = this.resourceUnitMap[resource.quantity_value]?this.resourceUnitMap[resource.quantity_value].conversionFactor:1; + cycleQuota[resource['name']] = this.cycleResourceDefaults[resource.name]/conversionFactor; + } + return cycleQuota; + } + + /** + * Function to add new resource to cycle + */ + addNewResource(){ + if (this.state.newResource) { + let resourceList = this.state.resourceList; + const newResource = _.remove(resourceList, {'name': this.state.newResource}); + let resources = this.state.resources?this.state.resources:[]; + resources.push(newResource[0]); + this.setState({resources: resources, resourceList: resourceList, newResource: null}); + } + } + + /** + * Callback function to be called from ResourceInpulList when a resource is removed from it + * @param {string} name - resource_type_id + */ + removeResource(name) { + let resources = this.state.resources; + let resourceList = this.state.resourceList; + let cycleQuota = this.state.cycleQuota; + const removedResource = _.remove(resources, (resource) => { return resource.name === name }); + resourceList.push(removedResource[0]); + delete cycleQuota[name]; + this.setState({resourceList: resourceList, resources: resources, cycleQuota: cycleQuota}); + } + + /** + * Function to call on change and blur events from input components + * @param {string} key + * @param {any} value + */ + setCycleParams(key, value, type) { + let cycle = this.state.cycle; + switch(type) { + case 'NUMBER': { + cycle[key] = value?parseInt(value):0; + break; + } + default: { + cycle[key] = value; + break; + } + } + this.setState({cycle: cycle, validForm: this.validateForm(key)}); + } + + /** + * Callback Function to call from ResourceInputList on change and blur events + * @param {string} key + * @param {InputEvent} event + */ + setCycleQuotaParams(key, event) { + let cycleQuota = this.state.cycleQuota; + if (event.target.value) { + let resource = _.find(this.state.resources, {'name': key}); + let newValue = 0; + if (this.resourceUnitMap[resource.quantity_value] && + event.target.value.toString().indexOf(this.resourceUnitMap[resource.quantity_value].display)>=0) { + newValue = event.target.value.replace(this.resourceUnitMap[resource.quantity_value].display,''); + } else { + newValue = event.target.value; + } + cycleQuota[key] = (newValue==="NaN" || isNaN(newValue))?0:newValue; + } else { + let cycleQuota = this.state.cycleQuota; + cycleQuota[key] = 0; + } + this.setState({cycleQuota: cycleQuota}); + } + + /** + * Validation function to validate the form or field based on the form rules. + * If no argument passed for fieldName, validates all fields in the form. + * @param {string} fieldName + */ + validateForm(fieldName) { + let validForm = false; + let errors = this.state.errors; + let validFields = this.state.validFields; + if (fieldName) { + delete errors[fieldName]; + delete validFields[fieldName]; + if (this.formRules[fieldName]) { + const rule = this.formRules[fieldName]; + const fieldValue = this.state.cycle[fieldName]; + if (rule.required) { + if (!fieldValue) { + errors[fieldName] = rule.message?rule.message:`${fieldName} is required`; + } else { + validFields[fieldName] = true; + } + } + } + } else { + errors = {}; + validFields = {}; + for (const fieldName in this.formRules) { + const rule = this.formRules[fieldName]; + const fieldValue = this.state.cycle[fieldName]; + if (rule.required) { + if (!fieldValue) { + errors[fieldName] = rule.message?rule.message:`${fieldName} is required`; + } else { + validFields[fieldName] = true; + } + } + } + } + + if (Object.keys(validFields).length === Object.keys(this.formRules).length) { + validForm = true; + } + + if(this.state.cycle['start'] && this.state.cycle['stop']){ + var isSameOrAfter = moment(this.state.cycle['stop']).isSameOrAfter(this.state.cycle['start']); + if(!isSameOrAfter){ + errors['stop'] = ` Stop date should be after Start date`; + validForm = false; + }else{ + validForm = true; + } + } + + this.setState({errors: errors, validFields: validFields, validForm: validForm}); + return validForm; + } + + /** + * Function to call when 'Save' button is clicked to update the cycle. + */ + saveCycle() { + if (this.validateForm) { + let cycle = this.state.cycle; + let stoptime = _.replace(this.state.cycle['stop'],'00:00:00', '23:59:59'); + cycle['start'] = moment(this.state.cycle['start']).format("YYYY-MM-DDTHH:mm:ss"); + cycle['stop'] = moment(stoptime).format("YYYY-MM-DDTHH:mm:ss"); + this.setState({cycle: cycle}); + CycleService.updateCycle(this.props.match.params.id, this.state.cycle) + .then(async (cycle) => { + if (cycle && this.state.cycle.updated_at !== cycle.updated_at) { + this.saveCycleQuota(cycle); + } else { + this.growl.show({severity: 'error', summary: 'Error Occured', detail: 'Unable to update Cycle'}); + this.setState({errors: cycle}); + } + }); + } + } + + /** + * Function to Create, Update & Delete cycle_quota for the cycle + */ + async saveCycleQuota(cycle) { + let dialog = {}; + let quotaError = {}; + let updatingCycleQuota = []; + let newCycleQuota = []; + let deletingCycleQuota = []; + for (const resource in this.state.cycleQuota) { + 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.cycleQuota[resource] * conversionFactor; + let existingQuota = _.find(this.cycleQuota, {'resource_type_id': resource}); + if (!existingQuota) { + let quota = { cycle: cycle.url, + resource_type: resourceType['url'], + value: quotaValue }; + newCycleQuota.push(quota); + } else if (existingQuota && existingQuota.value !== quotaValue) { + existingQuota.cycle = cycle.url; + existingQuota.value = quotaValue; + updatingCycleQuota.push(existingQuota); + } + } + let cycleQuota = this.state.cycleQuota; + deletingCycleQuota = _.filter(this.cycleQuota, function(quota) { return !cycleQuota[quota.resource_type_id]}); + + for (const cycleQuota of deletingCycleQuota) { + const deletedCycleQuota = await CycleService.deleteCycleQuota(cycleQuota); + if (!deletedCycleQuota) { + quotaError[cycleQuota.resource_type_id] = true; + } + } + for (const cycleQuota of updatingCycleQuota) { + const updatedCycleQuota = await CycleService.updateCycleQuota(cycleQuota); + if (!updatedCycleQuota) { + quotaError[cycleQuota.resource_type_id] = true; + } + } + for (const cycleQuota of newCycleQuota) { + const createdCycleQuota = await CycleService.saveCycleQuota(cycleQuota); + if (!createdCycleQuota) { + quotaError[cycleQuota.resource_type_id] = true; + } + } + if (_.keys(quotaError).length === 0) { + dialog = {header: 'Success', detail: 'Cycle updated successfully.'}; + } else { + dialog = {header: 'Error', detail: 'Cycle updated successfully but resource allocation not updated properly. Try again!'}; + } + this.setState({dialogVisible: true, dialog: dialog}); + } + + /** + * Cancel edit and redirect to Cycle View page + */ + cancelEdit() { + this.setState({redirect: `/cycle/view/${this.state.cycle.name}`}); + } + + render() { + if (this.state.redirect) { + return <Redirect to={ {pathname: this.state.redirect} }></Redirect> + } + return ( + <React.Fragment> + {/*} <div className="p-grid"> + <Growl ref={(el) => this.growl = el} /> + + <div className="p-col-10 p-lg-10 p-md-10"> + <h2>Cycle - Edit</h2> + </div> + <div className="p-col-2 p-lg-2 p-md-2"> + <Link to={{ pathname: `/cycle/view/${this.state.cycle.name}`}} title="Close Edit" style={{float: "right"}}> + <i className="fa fa-window-close" style={{marginTop: "10px"}}></i> + </Link> + </div> + </div> */} + <AppHeader location={this.props.location} actions={[{name: 'fa-window-close', props:{ pathname: `/cycle/view/${this.state.cycle.name}`}}]}/> + + { this.state.isLoading ? <AppLoader/> : + <> + <div> + <div className="p-fluid"> + <div className="p-field p-grid"> + <label htmlFor="cycleName" className="col-lg-2 col-md-2 col-sm-12">Name <span style={{color:'red'}}>*</span></label> + <div className="col-lg-3 col-md-3 col-sm-12"> + <InputText className={this.state.errors.name ?'input-error':''} id="cycleName" data-testid="name" + tooltip="Enter name of the cycle" tooltipOptions={this.tooltipOptions} maxLength="128" + value={this.state.cycle.name} + onChange={(e) => this.setCycleParams('name', e.target.value)} + onBlur={(e) => this.setCycleParams('name', e.target.value)}/> + <label className={this.state.errors.name?"error":"info"}> + {this.state.errors.name ? this.state.errors.name : "Max 128 characters"} + </label> + </div> + <div className="col-lg-1 col-md-1 col-sm-12"></div> + <label htmlFor="description" className="col-lg-2 col-md-2 col-sm-12">Description <span style={{color:'red'}}>*</span></label> + <div className="col-lg-3 col-md-3 col-sm-12"> + <InputTextarea className={this.state.errors.description ?'input-error':''} rows={3} cols={30} + tooltip="Short description of the cycle" tooltipOptions={this.tooltipOptions} maxLength="128" + data-testid="description" value={this.state.cycle.description} + onChange={(e) => this.setCycleParams('description', e.target.value)} + onBlur={(e) => this.setCycleParams('description', e.target.value)}/> + <label className={this.state.errors.description ?"error":"info"}> + {this.state.errors.description ? this.state.errors.description : "Max 255 characters"} + </label> + </div> + </div> + + <div className="p-field p-grid"> + <label htmlFor="cycleName" className="col-lg-2 col-md-2 col-sm-12">Start Date <span style={{color:'red'}}>*</span></label> + <div className="col-lg-3 col-md-3 col-sm-12"> + <Calendar + d dateFormat="dd-M-yy" + inputId="start" + value= {new Date(this.state.cycle.start)} + onChange= {e => this.setCycleParams('start',e.value)} + onBlur= {e => this.setCycleParams('start',e.value)} + data-testid="start" + tooltip="Moment at which the cycle starts, that is, when its projects can run." tooltipOptions={this.tooltipOptions} + showIcon={true} + /> + <label className={this.state.errors.start?"error":"info"}> + {this.state.errors.start ? this.state.errors.start : ""} + </label> + </div> + <div className="col-lg-1 col-md-1 col-sm-12"></div> + <label htmlFor="cycleName" className="col-lg-2 col-md-2 col-sm-12">Stop Date <span style={{color:'red'}}>*</span></label> + <div className="col-lg-3 col-md-3 col-sm-12"> + <Calendar + d dateFormat="dd-M-yy" + value= {new Date(this.state.cycle.stop)} + onChange= {e => this.setCycleParams('stop', e.value)} + onBlur= {e => this.setCycleParams('stop',e.value)} + inputId="stop" + data-testid="stop" + tooltip="Moment at which the cycle officially ends." tooltipOptions={this.tooltipOptions} + showIcon={true} + /> + <label className={this.state.errors.stop?"error":"info"}> + {this.state.errors.stop ? this.state.errors.stop : ""} + </label> + </div> + </div> + + {this.state.resourceList && + <div className="p-fluid"> + <div className="p-field p-grid"> + <div className="col-lg-2 col-md-2 col-sm-12"> + <h5>Resource Allocations:</h5> + </div> + <div className="col-lg-3 col-md-3 col-sm-10"> + <Dropdown optionLabel="name" optionValue="name" + tooltip="Resources to be allotted for the cycle" + tooltipOptions={this.tooltipOptions} + value={this.state.newResource} + options={_.sortBy(this.state.resourceList, ['name'])} + onChange={(e) => {this.setState({'newResource': e.value})}} + placeholder="Add Resources" /> + </div> + <div className="col-lg-2 col-md-2 col-sm-2"> + <Button label="" className="p-button-primary" icon="pi pi-plus" onClick={this.addNewResource} disabled={!this.state.newResource} data-testid="add_res_btn" /> + </div> + </div> + {/* {_.keys(this.state.cycleQuota).length>0 && */} + <div className="p-field p-grid resource-input-grid"> + <ResourceInputList list={this.state.resources} unitMap={this.resourceUnitMap} + cycleQuota={this.state.cycleQuota} callback={this.setCycleQuotaParams} + removeInputCallback={this.removeResource} /> + </div> + {/* } */} + </div> + } + </div> + </div> + <div className="p-grid p-justify-start act-btn-grp"> + <div className="p-col-1"> + <Button label="Save" className="p-button-primary" id="save-btn" data-testid="save-btn" icon="pi pi-check" onClick={this.saveCycle} disabled={!this.state.validForm} /> + </div> + <div className="p-col-1"> + <Button label="Cancel" className="p-button-danger" icon="pi pi-times" onClick={this.cancelEdit} /> + </div> + </div> + + </> + } + {/* Dialog component to show messages and get input */} + <div className="p-grid" data-testid="confirm_dialog"> + <Dialog header={this.state.dialog.header} visible={this.state.dialogVisible} style={{width: '30vw'}} inputId="confirm_dialog" + modal={true} onHide={() => {this.setState({dialogVisible: false})}} + footer={<div> + <Button key="back" onClick={() => {this.setState({dialogVisible: false}); this.cancelEdit();}} label="Ok" /> + {/* <Button key="submit" type="primary" onClick={this.reset} label="Yes" /> */} + </div> + } > + <div className="p-grid"> + <div className="col-lg-2 col-md-2 col-sm-2"> + <i className="pi pi-check-circle pi-large pi-success"></i> + </div> + <div className="col-lg-10 col-md-10 col-sm-10"> + <span style={{marginTop:"5px"}}>{this.state.dialog.detail}</span> + </div> + </div> + </Dialog> + </div> + </React.Fragment> + ); + } +} \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/index.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/index.js new file mode 100644 index 00000000000..a52436177a7 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/index.js @@ -0,0 +1,6 @@ +import CycleList from './list'; +import {CycleCreate} from './create'; +import {CycleView} from './view'; +import {CycleEdit} from './edit'; + +export {CycleList, CycleCreate, CycleView, CycleEdit}; diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/list.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/list.js new file mode 100644 index 00000000000..71705d8f70c --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/list.js @@ -0,0 +1,160 @@ +import React, { Component } from 'react' +import 'primeflex/primeflex.css'; +import { Link } from 'react-router-dom/cjs/react-router-dom.min'; +import _ from 'lodash'; + +import ViewTable from '../../components/ViewTable'; +import CycleService from '../../services/cycle.service'; +import UnitConversion from '../../utils/unit.converter'; +import AppLoader from '../../layout/components/AppLoader'; +import AppHeader from '../../layout/components/AppHeader'; + +class CycleList extends Component{ + constructor(props){ + super(props) + this.state = { + cyclelist: [], + paths: [{ + "View": "/cycle/view", + }], + isLoading: true + } + this.projectCategory = ['regular', 'user_shared_support']; + this.periodCategory = ['long_term']; + this.defaultcolumns = [ { id:"Cycle Code", + start:"Start Date", + stop: "End Date", + duration: "Duration (Days)", + totalProjects: 'No.of Projects', + observingTime: 'Lofar Observing Time (Hrs)', + processingTime: 'Lofar Processing Time (Hrs)', + ltaResources: 'Lofar LTA Resources(TB)', + support: 'Lofar Support (Hrs)', + longterm : 'Long Term Projects' } ]; + this.optionalcolumns = [{ regularProjects: 'No.of Regular Projects', + observingTimeDDT: 'Lofar Observing Time Commissioning (Hrs)', + observingTimePrioA: 'Lofar Observing Time Prio A (Hrs)', + observingTimePrioB: 'Lofar Observing Time Prio B (Hrs)', + actionpath: "actionpath", }]; + + this.columnclassname = [{ "Cycle Code":"filter-input-75", + "Duration (Days)" : "filter-input-50", + "No.of Projects" : "filter-input-50", + "Lofar Observing Time (Hrs)" : "filter-input-75", + "Lofar Processing Time (Hrs)" : "filter-input-75", + "Lofar LTA Resources(TB)" : "filter-input-75", + "Lofar Support (Hrs)" : "filter-input-50", + "Long Term Projects" : "filter-input-50", + "No.of Regular Projects" : "filter-input-50", + "Lofar Observing Time Commissioning (Hrs)" : "filter-input-75", + "Lofar Observing Time Prio A (Hrs)" : "filter-input-75", + "Lofar Observing Time Prio B (Hrs)" : "filter-input-75" }]; + } + + getUnitConvertedQuotaValue(cycle, cycleQuota, resourceName) { + const quota = _.find(cycleQuota, {'cycle_id': cycle.name, 'resource_type_id': resourceName}); + let ref = this.state.resources.find(i => i.name === resourceName) + const unitQuantity =(ref)?ref.quantity_value:0 + return UnitConversion.getUIResourceUnit(unitQuantity, quota?quota.value:0); + } + + getCycles(cycles = [], cycleQuota) { + const promises = []; + cycles.map(cycle => promises.push(CycleService.getCycleById(cycle.name))); + Promise.all(promises).then(responses => { + const results = cycles; + results.map(async (cycle, index) => { + const projects = responses[index]; + const regularProjects = projects.filter(project => this.projectCategory.includes(project.project_category_value)); + const longterm = projects.filter(project => this.periodCategory.includes(project.period_category_value)); + cycle.duration = UnitConversion.getUIResourceUnit('days', cycle.duration); + cycle.totalProjects = cycle.projects ? cycle.projects.length : 0; + cycle.id = cycle.name ; + cycle.regularProjects = regularProjects.length; + cycle.longterm = longterm.length; + // cycle.observingTime = this.getUnitConvertedQuotaValue(cycle, cycleQuota, 'observing_time'); + // cycle.processingTime = this.getUnitConvertedQuotaValue(cycle, cycleQuota, 'cep_processing_time'); + // cycle.ltaResources = this.getUnitConvertedQuotaValue(cycle, cycleQuota, 'lta_storage'); + // cycle.support = this.getUnitConvertedQuotaValue(cycle, cycleQuota, 'support_time'); + // cycle.observingTimeDDT = this.getUnitConvertedQuotaValue(cycle, cycleQuota, 'observing_time_commissioning'); + // cycle.observingTimePrioA = this.getUnitConvertedQuotaValue(cycle, cycleQuota, 'observing_time_prio_a'); + // cycle.observingTimePrioB = this.getUnitConvertedQuotaValue(cycle, cycleQuota, 'observing_time_prio_b'); + cycle.observingTime = this.getUnitConvertedQuotaValue(cycle, cycleQuota, 'LOFAR Observing Time'); + cycle.processingTime = this.getUnitConvertedQuotaValue(cycle, cycleQuota, 'CEP Processing Time'); + cycle.ltaResources = this.getUnitConvertedQuotaValue(cycle, cycleQuota, 'LTA Storage'); + cycle.support = this.getUnitConvertedQuotaValue(cycle, cycleQuota, 'LOFAR Support Time'); + cycle.observingTimeDDT = this.getUnitConvertedQuotaValue(cycle, cycleQuota, 'LOFAR Observing Time Commissioning'); + cycle.observingTimePrioA = this.getUnitConvertedQuotaValue(cycle, cycleQuota, 'LOFAR Observing Time prio A'); + cycle.observingTimePrioB = this.getUnitConvertedQuotaValue(cycle, cycleQuota, 'LOFAR Observing Time prio B'); + + cycle['actionpath'] = `/cycle/view/${cycle.id}`; + return cycle; + }); + this.setState({ + cyclelist : results, + isLoading: false + }); + }); + } + + componentDidMount(){ + const promises = [CycleService.getAllCycleQuotas(), CycleService.getResources()] + Promise.all(promises).then(responses => { + const cycleQuota = responses[0]; + this.setState({ resources: responses[1] }); + CycleService.getAllCycles().then(cyclelist => { + this.getCycles(cyclelist, cycleQuota) + }); + }); + } + + 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/create'}} 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 + defaultcolumns - This colum will be populate by default in table with header mentioned + 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 + */} + <AppHeader location={this.props.location} actions={[{name: 'fa-plus-square', props:{ pathname: '/cycle/create'}}]}/> + {/* + * 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 + 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.isLoading? <AppLoader /> : (this.state.cyclelist && this.state.cyclelist.length) ? + + <ViewTable + data={this.state.cyclelist} + defaultcolumns={this.defaultcolumns} + optionalcolumns={this.optionalcolumns} + columnclassname = {this.columnclassname} + showaction="true" + paths={this.state.paths} + /> : <></> + } + + + + </> + ) + } +} + +export default CycleList + 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 00000000000..36858e8cacf --- /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 CycleServiceMock from '../../__mocks__/cycle.service.data'; + +jest.mock('../../services/cycle.service', () => { + return { + getProjects: () => Promise.resolve({ data: CycleServiceMock.getProjects }), + getCycleQuota: () => Promise.resolve({ data: CycleServiceMock.getCycleQuota }), + getAllCycles: () => Promise.resolve(CycleServiceMock.getAllCycle.results ), + getResources: () => Promise.resolve({ data: CycleServiceMock.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(CycleServiceMock.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(CycleServiceMock.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/Cycle/view.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/view.js new file mode 100644 index 00000000000..ef1c35cd5cd --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/view.js @@ -0,0 +1,143 @@ +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 AppHeader from '../../layout/components/AppHeader'; +import CycleService from '../../services/cycle.service'; +import UnitConverter from '../../utils/unit.converter'; + +/** + * Component to view the details of a cycle + */ +export class CycleView extends Component { + DATE_FORMAT = 'YYYY-MMM-DD HH:mm:ss'; + constructor(props) { + super(props); + this.state = { + isLoading: true, + cycle:'', + }; + if (this.props.match.params.id) { + this.state.cycleId = this.props.match.params.id; + } else if (this.props.location.state && this.props.location.state.id) { + this.state.cycleId = this.props.location.state.id; + } + this.state.redirect = this.state.cycleId?"":'/cycle' // If no cycle id is passed, redirect to cycle list page + this.resourceUnitMap = UnitConverter.resourceUnitMap; // Resource unit conversion factor and constraints + } + + componentDidMount() { + const cycleId = this.state.cycleId; + if (cycleId) { + this.getCycleDetails(); + } else { + this.setState({redirect: "/not-found"}); + } + } + + /** + * To get the cycle details from the backend using the service + * + */ + async getCycleDetails() { + let cycle = await CycleService.getCycleDetails(this.state.cycleId); + let cycleQuota = []; + let resources = []; + + if (cycle) { + // If resources are allocated for the cycle quota fetch the resources master from the API + if (cycle.quota) { + resources = await CycleService.getResources(); + } + + // For every cycle quota, get the resource type & assign to the resource variable of the quota object + for (const id of cycle.quota_ids) { + let quota = await CycleService.getCycleQuota(id); + let resource = _.find(resources, ['name', quota.resource_type_id]); + quota.resource = resource; + cycleQuota.push(quota); + }; + this.setState({cycle: cycle, cycleQuota: cycleQuota, 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>Cycle - Details </h2> + </div> + { this.state.cycle && + <div className="p-col-2 p-lg-2 p-md-2"> + <Link to={{ pathname: `/cycle`}} title="Close View" style={{float: "right"}}> + <i className="fa fa-times" style={{marginTop: "10px", marginLeft: "5px"}}></i> + </Link> + <Link to={{ pathname: `/cycle/edit/${this.state.cycle.name}`, state: {id: this.state.cycle?this.state.cycle.name:''}}} title="Edit Cycle" + style={{float: "right"}}> + <i className="fa fa-edit" style={{marginTop: "10px"}}></i> + </Link> + </div> + } + </div> */ } + <AppHeader location={this.props.location} actions={[{name: 'fa-edit', props:{ pathname: `/cycle/edit/${this.state.cycle.name}`, state: {id: this.state.cycle?this.state.cycle.name:''}}},{name: 'fa-times',props:{ pathname: `/cycle`}}]}/> + { this.state.isLoading && <AppLoader /> } + { this.state.cycle && + <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.cycle.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.cycle.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.cycle.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.cycle.updated_at).format(this.DATE_FORMAT)}</span> + </div> + + <div className="p-grid"> + <label className="col-lg-2 col-md-2 col-sm-12">Projects</label> + <Chips className="col-lg-4 col-md-4 col-sm-12 chips-readonly" disabled value={this.state.cycle.projects_ids}></Chips> + </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.cycleQuota.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: `/cycle/edit/${this.state.cycle.name}`, state: {id: this.state.cycle?this.state.cycle.name:''}}} title="Edit Cycle" > Click</Link> to add. + </span> + </div> + </div> + } + <div className="p-field p-grid resource-input-grid"> + <ResourceDisplayList cycleQuota={this.state.cycleQuota} 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/create.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/create.js index e400bf2f3d5..bfa99e4d328 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/create.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/create.js @@ -15,6 +15,7 @@ import {Growl} from 'primereact/components/growl/Growl'; import {ResourceInputList} from './ResourceInputList'; import AppLoader from '../../layout/components/AppLoader'; +import AppHeader from '../../layout/components/AppHeader'; import CycleService from '../../services/cycle.service'; import ProjectService from '../../services/project.service'; import UnitConverter from '../../utils/unit.converter'; @@ -255,10 +256,12 @@ export class ProjectCreate extends Component { let projectQuota = []; for (const resource in this.state.projectQuota) { let resourceType = _.find(this.state.resources, {'name': resource}); - let quota = { project: this.state.project.name, - resource_type: resourceType['url'], - value: this.state.projectQuota[resource] * (this.resourceUnitMap[resourceType.quantity_value]?this.resourceUnitMap[resourceType.quantity_value].conversionFactor:1)}; - projectQuota.push(quota); + if(resourceType){ + let quota = { project: this.state.project.name, + resource_type: resourceType['url'], + value: this.state.projectQuota[resource] * (this.resourceUnitMap[resourceType.quantity_value]?this.resourceUnitMap[resourceType.quantity_value].conversionFactor:1)}; + projectQuota.push(quota); + } } ProjectService.saveProject(this.state.project, this.defaultResourcesEnabled?projectQuota:[]) .then(project => { @@ -306,11 +309,12 @@ export class ProjectCreate extends Component { this.setState({ dialog: { header: '', detail: ''}, project: { + url: '', name: '', description: '', trigger_priority: 1000, priority_rank: null, - project_quota: [] + quota: [] }, projectQuota: projectQuota, validFields: {}, @@ -332,14 +336,19 @@ export class ProjectCreate extends Component { return ( <React.Fragment> - <div className="p-grid"> + { /* <div className="p-grid"> <Growl ref={(el) => this.growl = el} /> - <div className="p-col-2 p-lg-2 p-md-2"> + + <div className="p-col-10 p-lg-10 p-md-10"> + <h2>Project - Add</h2> + </div> + <div className="p-col-2 p-lg-2 p-md-2"> <Link to={{ pathname: '/project'}} tite="Close Edit" style={{float: "right"}}> - <i className="fa fa-window-close" style={{position:"absolute",top: "105px",marginLeft:"863px"}}></i> + <i className="fa fa-window-close" style={{marginTop: "10px"}}></i> </Link> </div> - </div> + </div> */ } + <AppHeader location={this.props.location} actions={[{name: 'fa-window-close', props:{ pathname: '/project'}}]}/> { this.state.isLoading ? <AppLoader /> : <> <div> 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 0136988f137..80ae162cb4b 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/edit.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/edit.js @@ -15,6 +15,7 @@ import {Growl} from 'primereact/components/growl/Growl'; import {ResourceInputList} from './ResourceInputList'; import AppLoader from '../../layout/components/AppLoader'; +import AppHeader from '../../layout/components/AppHeader'; import CycleService from '../../services/cycle.service'; import ProjectService from '../../services/project.service'; import UnitConverter from '../../utils/unit.converter'; @@ -356,14 +357,19 @@ export class ProjectEdit extends Component { return ( <React.Fragment> - <div className="p-grid"> + {/*} <div className="p-grid"> <Growl ref={(el) => this.growl = el} /> + + <div className="p-col-10 p-lg-10 p-md-10"> + <h2>Project - Edit</h2> + </div> <div className="p-col-2 p-lg-2 p-md-2"> <Link to={{ pathname: `/project/view/${this.state.project.name}`}} title="Close Edit" style={{float: "right"}}> - <i className="fa fa-window-close" style={{position: "absolute",top: "105px",marginLeft: "875px"}}></i> + <i className="fa fa-window-close" style={{marginTop: "10px"}}></i> </Link> </div> - </div> + </div> */} + <AppHeader location={this.props.location} actions={[{name: 'fa-window-close', props : { pathname: `/project/view/${this.state.project.name}`}}]}/> { this.state.isLoading ? <AppLoader/> : <> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/list.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/list.js index f47fc36beca..8a1b11ac569 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/list.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/list.js @@ -3,6 +3,7 @@ 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'; +import AppHeader from '../../layout/components/AppHeader'; export class ProjectList extends Component{ constructor(props){ @@ -70,13 +71,17 @@ export class ProjectList extends Component{ render(){ return( <> - <div className="p-grid"> + {/*<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={{top: "105px",position: "absolute",marginLeft: "865px"}}></i> + <i className="fa fa-plus-square" style={{marginTop: "10px"}}></i> </Link> </div> - </div> + </div> */} + <AppHeader location={this.props.location} actions={[{name: 'fa-plus-square', props:{pathname: '/project/create' }}]}/> {this.state.isLoading? <AppLoader /> : this.state.isprocessed && <ViewTable data={this.state.projectlist} diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/view.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/view.js index eca8951d2d9..6fe352aee92 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/view.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/view.js @@ -8,6 +8,7 @@ import { Chips } from 'primereact/chips'; import ResourceDisplayList from './ResourceDisplayList'; import AppLoader from '../../layout/components/AppLoader'; +import AppHeader from '../../layout/components/AppHeader'; import ProjectService from '../../services/project.service'; import UnitConverter from '../../utils/unit.converter'; @@ -20,23 +21,20 @@ export class ProjectView extends Component { super(props); this.state = { isLoading: true, + project:'', }; - 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"}); @@ -79,19 +77,23 @@ export class ProjectView extends Component { return ( <React.Fragment> - <div className="p-grid"> + { /*} <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={{position:"absolute",top:"105px",marginLeft:"870px"}}></i> + <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={{position:"absolute",top:"105px",marginLeft:"845px"}}></i> + <i className="fa fa-edit" style={{marginTop: "10px"}}></i> </Link> </div> } - </div> + </div> */} + <AppHeader location={this.props.location} actions={[{name: 'fa-edit', props : { pathname: `/project/edit/${this.state.project.name}`, state: {id: this.state.project?this.state.project.name:''&& this.state.project}}},{name: 'fa-times', props : { pathname: `/project`}}]}/> { this.state.isLoading && <AppLoader /> } { this.state.project && <React.Fragment> @@ -112,7 +114,7 @@ export class ProjectView extends Component { <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> + <span className="col-lg-4 col-md-4 col-sm-12"><i className={this.state.project.can_trigger?'fa fa-check-circle':'fa fa-times-circle'}></i></span> </div> <div className="p-grid"> <label className="col-lg-2 col-md-2 col-sm-12">Project Category</label> 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 4ef6b176343..a3ec244ea83 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/SchedulingUnitList.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/SchedulingUnitList.js @@ -1,8 +1,8 @@ import React, { Component } from 'react' import 'primeflex/primeflex.css'; - -import AppLoader from "../../layout/components/AppLoader"; -import ViewTable from '../../components/ViewTable'; +import moment from 'moment'; +import AppLoader from "./../../layout/components/AppLoader"; +import ViewTable from './../../components/ViewTable'; import ScheduleService from '../../services/schedule.service'; @@ -18,6 +18,7 @@ class SchedulingUnitList extends Component{ }], isLoading: true, defaultcolumns: [ { + "type":"Type", "name":"Name", "description":"Description", "created_at":"Created Date", @@ -25,7 +26,7 @@ class SchedulingUnitList extends Component{ "requirements_template_id": "Template", "start_time":"Start Time", "stop_time":"End time", - "duration":"Duration" + "duration":"Duration (HH:mm:ss)" }], optionalcolumns: [{ "actionpath":"actionpath", @@ -33,23 +34,41 @@ class SchedulingUnitList extends Component{ columnclassname: [{ "Template":"filter-input-50", "Duration":"filter-input-50", + "Type": "filter-input-75" }] } } + + - componentDidMount(){ + async getSchedulingUnitList () { + const bluePrint = await ScheduleService.getSchedulingUnitBlueprint(); ScheduleService.getSchedulingUnitDraft().then(scheduleunit =>{ - console.log(scheduleunit) + const output = []; var scheduleunits = scheduleunit.data.results; - for( const scheduleunit of scheduleunits){ - scheduleunit['actionpath']='/schedulingunit/view' + const blueprintdata = bluePrint.data.results.filter(i => i.draft_id === scheduleunit.id); + blueprintdata.map(blueP => { + blueP.duration = moment.utc(blueP.duration*1000).format('HH:mm:ss'); + blueP.type="Blueprint"; + blueP['actionpath'] = '/task/view/type/id'; + return blueP; + }); + output.push(...blueprintdata); + scheduleunit['actionpath']='/schedulingunit/view'; + scheduleunit['type'] = 'Draft'; + scheduleunit['duration'] = moment.utc(scheduleunit.duration*1000).format('HH:mm:ss'); + output.push(scheduleunit); } this.setState({ - scheduleunit : scheduleunit.data ,isLoading: false + scheduleunit: output, isLoading:false }); }) } + + componentDidMount(){ + this.getSchedulingUnitList(); + } render(){ if (this.state.isLoading) { @@ -57,9 +76,7 @@ class SchedulingUnitList extends Component{ } return( <> - - { - + { /* * Call View table to show table data, the parameters are, data - Pass API data @@ -70,9 +87,9 @@ class SchedulingUnitList extends Component{ paths - specify the path for navigation - Table will set "id" value for each row in action button */} - {this.state.scheduleunit.results && + {this.state.scheduleunit && <ViewTable - data={this.state.scheduleunit.results} + data={this.state.scheduleunit} defaultcolumns={this.state.defaultcolumns} optionalcolumns={this.state.optionalcolumns} columnclassname={this.state.columnclassname} 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 eaececbd9a8..68974a98919 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/ViewSchedulingUnit.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/ViewSchedulingUnit.js @@ -4,6 +4,7 @@ import 'primeflex/primeflex.css'; import { Chips } from 'primereact/chips'; import AppLoader from "./../../layout/components/AppLoader"; +import AppHeader from '../../layout/components/AppHeader'; import ViewTable from './../../components/ViewTable'; import ScheduleService from '../../services/schedule.service'; @@ -21,7 +22,7 @@ class ViewSchedulingUnit extends Component{ }], defaultcolumns: [ { - "tasktype":"Task Type", + "tasktype":"Type", "id":"ID", "name":"Name", "description":"Description", @@ -30,11 +31,11 @@ class ViewSchedulingUnit extends Component{ "do_cancel":"Cancelled", "start_time":"Start Time", "stop_time":"End Time", - "duration":"Duration", + "duration":"Duration (HH:mm:ss)", }], optionalcolumns: [{ - "relative_start_time":"Relative Start Time", - "relative_stop_time":"Relative End Time", + "relative_start_time":"Relative Start Time (HH:mm:ss)", + "relative_stop_time":"Relative End Time (HH:mm:ss)", "tags":"Tags", "blueprint_draft":"BluePrint / Task Draft link", "url":"URL", @@ -42,12 +43,14 @@ class ViewSchedulingUnit extends Component{ }], columnclassname: [{ - "Task Type":"filter-input-100", + "Type":"filter-input-75", "ID":"filter-input-50", "Cancelled":"filter-input-50", - "Duration":"filter-input-50", + "Duration (HH:mm:ss)":"filter-input-75", "Template ID":"filter-input-50", - "BluePrint / Task Draft link":"filter-input-100", + "BluePrint / Task Draft link": "filter-input-100", + "Relative Start Time (HH:mm:ss)": "filter-input-75", + "Relative End Time (HH:mm:ss)": "filter-input-75", }] } } @@ -59,6 +62,12 @@ class ViewSchedulingUnit extends Component{ .then(scheduleunit =>{ ScheduleService.getScheduleTasksBySchedulingUnitId(scheduleunit.data.id) .then(tasks =>{ + tasks.map(task => { + task.duration = moment.utc(task.duration*1000).format('HH:mm:ss'); + task.relative_start_time = moment.utc(task.relative_start_time*1000).format('HH:mm:ss'); + task.relative_stop_time = moment.utc(task.relative_stop_time*1000).format('HH:mm:ss'); + return task; + }); this.setState({ scheduleunit : scheduleunit.data, schedule_unit_task : tasks, @@ -72,50 +81,54 @@ class ViewSchedulingUnit extends Component{ render(){ return( <> - <div className="p-grid"> - <div className="p-col-2"> + {/*} <div className="p-grid"> + <div className="p-col-10"> + <h2>Scheduling Unit - Details </h2> + </div> + <div className="p-col-2"> <Link to={{ pathname: '/schedulingunit'}} title="Close" style={{float:'right'}}> - <i className="fa fa-times" style={{position:"absolute",top:"105px",marginLeft:"870px"}}></i> + <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" + <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={{position:"absolute",top:"105px",marginLeft:"845px"}}></i> - </Link> - </div> + <i className="fa fa-edit" style={{marginTop: "10px"}}></i> + </Link> </div> + </div> */} + <AppHeader location={this.props.location} actions={[{name: 'fa-times', props : { pathname: '/schedulingunit'}}]}/> { this.state.isLoading ? <AppLoader/> :this.state.scheduleunit && <> <div className="p-grid"> - <label className="p-col-2">Name</label> - <span className="p-col-4">{this.state.scheduleunit.name}</span> - <label className="p-col-2">Description</label> - <span className="p-col-4">{this.state.scheduleunit.description}</span> + <label className="col-lg-2 col-md-2 col-sm-12">Name</label> + <span className="p-col-lg-4 col-md-4 col-sm-12">{this.state.scheduleunit.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.scheduleunit.description}</span> </div> <div className="p-grid"> - <label className="p-col-2">Created At</label> - <span className="p-col-4">{moment(this.state.scheduleunit.created_at).format("YYYY-MMM-DD HH:mm:SS")}</span> - <label className="p-col-2">Updated At</label> - <span className="p-col-4">{moment(this.state.scheduleunit.updated_at).format("YYYY-MMM-DD HH:mm:SS")}</span> + <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(this.state.scheduleunit.created_at).format("YYYY-MMM-DD HH:mm:SS")}</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(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> + <label className="col-lg-2 col-md-2 col-sm-12">Start Time</label> + <span className="col-lg-4 col-md-4 col-sm-12">{this.state.scheduleunit.start_time && moment(this.state.scheduleunit.start_time).format("YYYY-MMM-DD HH:mm:SS")}</span> + <label className="col-lg-2 col-md-2 col-sm-12">End Time</label> + <span className="col-lg-4 col-md-4 col-sm-12">{this.state.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> + <label className="col-lg-2 col-md-2 col-sm-12">Template ID</label> + <span className="col-lg-4 col-md-4 col-sm-12">{this.state.scheduleunit.requirements_template_id}</span> + <label className="col-lg-2 col-md-2 col-sm-12">Scheduling set</label> + <span className="col-lg-4 col-md-4 col-sm-12">{this.state.scheduleunit.scheduling_set_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> + <label className="col-lg-2 col-md-2 col-sm-12">Duration (HH:mm:ss)</label> + <span className="col-lg-4 col-md-4 col-sm-12">{moment.utc(this.state.scheduleunit.duration*1000).format('HH:mm:ss')}</span> + <label className="col-lg-2 col-md-2 col-sm-12">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> + <span className="col-lg-4 col-md-4 col-sm-12">{this.state.scheduleunit.tags}</span> </div> </> 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 81a439da72d..e4c8e9e8f8d 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/index.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/index.js @@ -1,5 +1,6 @@ import React, {Component} from 'react'; import SchedulingUnitList from './SchedulingUnitList'; +import AppHeader from '../../layout/components/AppHeader'; export class Scheduling extends Component { constructor(props){ @@ -14,7 +15,7 @@ export class Scheduling extends Component { render() { return ( <> - + <AppHeader location={this.props.location} /> {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 f110d4ac421..6d40b19874e 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/edit.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/edit.js @@ -12,6 +12,7 @@ import Jeditor from '../../components/JSONEditor/JEditor'; import TaskService from '../../services/task.service'; import AppLoader from "./../../layout/components/AppLoader"; +import AppHeader from '../../layout/components/AppHeader'; export class TaskEdit extends Component { @@ -187,15 +188,18 @@ export class TaskEdit extends Component { return ( <React.Fragment> - <div className="p-grid"> + {/*} <div className="p-grid"> + <div className="p-col-10 p-lg-10 p-md-10"> + <h2>Task - Edit</h2> + </div> <div className="p-col-2 p-lg-2 p-md-2"> <Link to={{ pathname: `/task/view/draft/${this.state.task?this.state.task.id:''}`}} title="Close Edit" style={{float: "right"}} > - <i className="fa fa-window-close" style={{position:"absolute",top:"105px",marginLeft: "860px"}}></i> + <i className="fa fa-window-close" style={{marginTop: "10px"}}></i> </Link> </div> - </div> - + </div> */} + <AppHeader location={this.props.location} actions={[{name: 'fa-window-close', props : { pathname: `/task/view/draft/${this.state.task?this.state.task.id:''}`}}]}/> {isLoading ? <AppLoader/> : <div> <div className="p-fluid"> 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 812f2cc0d80..13c77c508a0 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/view.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/view.js @@ -7,6 +7,7 @@ import Jeditor from '../../components/JSONEditor/JEditor'; import TaskService from '../../services/task.service'; import { Chips } from 'primereact/chips'; import AppLoader from '../../layout/components/AppLoader'; +import AppHeader from '../../layout/components/AppHeader'; export class TaskView extends Component { DATE_FORMAT = 'YYYY-MMM-DD HH:mm:ss'; @@ -126,25 +127,29 @@ export class TaskView extends Component { ); return ( <React.Fragment> - <div className="p-grid"> + {/* <div className="p-grid"> + <div className="p-col-10 p-lg-10 p-md-10"> + <h2>Task - Details </h2> + </div> <div className="p-col-2 p-lg-2 p-md-2"> {this.state.taskType === 'draft' && <div> <Link to={{ pathname: '/task'}} tooltip="Edit Task" style={{float: 'right'}}> - <i className="fa fa-times" style={{position:"absolute",top:"105px",marginLeft:"865px"}}></i> + <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={{position:"absolute",top:"105px",marginLeft:"839px"}}></i> + <i className="fa fa-edit" style={{marginTop: "10px"}}></i> </Link> </div> } {this.state.taskType === 'blueprint' && - <i className="fa fa-lock" style={{position: "absolute",top: "105px",marginLeft: "1025px"}}></i> + <i className="fa fa-lock" style={{float:"right", marginTop: "10px"}}></i> } </div> - </div> + </div> */} + <AppHeader location={this.props.location} actions={[{name: 'fa-edit', props : { pathname:'/task/edit',state: {taskId: this.state.task?this.state.task.id:''} } },{name: 'fa-times', props : { pathname:'/task' }}]}/> { this.state.isLoading? <AppLoader /> : this.state.task && <React.Fragment> <div className="main-content"> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/index.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/index.js index 8efb26f988e..7b48b397c17 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/index.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/index.js @@ -11,6 +11,7 @@ import {Dashboard} from './Dashboard'; import {Scheduling} from './Scheduling'; import {TaskEdit, TaskView} from './Task'; import ViewSchedulingUnit from './Scheduling/ViewSchedulingUnit' +import { CycleList, CycleCreate, CycleView, CycleEdit } from './Cycle'; export const routes = [ { @@ -72,18 +73,45 @@ export const routes = [ component: ProjectView, name: 'Project View', pageTitle: 'Project - View' - },{ + }, + { path: "/project/edit/:id", component: ProjectEdit, name: 'Project Edit', - pageTitle: 'Project - Edit' + pageTitle: 'Project Edit' + }, + { + path: "/cycle/edit/:id", + component: CycleEdit, + name: 'Cycle Edit', + pageTitle:'Cycle-Edit' + },{ + path: "/cycle/view", + component: CycleView, + name: 'Cycle View', + pageTitle:'Cycle-View' + },{ + path: "/cycle/view/:id", + component: CycleView, + name: 'Cycle View', + pageTitle:'Cycle-View' + }, { + path: "/cycle/create", + component: CycleCreate, + name: 'Cycle Add', + pageTitle:'Cycle-Add' + }, + { + path: "/cycle", + component: CycleList, + name: 'Cycle List', + pageTitle:'Cycle-List' }, ]; export const RoutedContent = () => { return ( - - <Switch> + <Switch> <Redirect from="/" to="/" exact /> {routes.map(routeProps => <Route {...routeProps} exact key={routeProps.path} />)} </Switch> 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 e9ab9620b3a..f758c543f5a 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/services/cycle.service.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/services/cycle.service.js @@ -4,25 +4,46 @@ const axios = require('axios'); axios.defaults.headers.common['Authorization'] = 'Basic dGVzdDp0ZXN0'; const CycleService = { - getAllCycles: async function() { + getAllCycles: async function () { try { - const url = `/api/cycle`; - const response = await axios.get(url); - return response.data.results; + const url = `/api/cycle`; + const response = await axios.get(url); + return response.data.results; } catch (error) { - console.error(error); + console.error(error); } - }, + }, + // Duplicate + getCycleById: async function (id) { + try { + const url = `/api/cycle/${id}/project`; + const response = await axios.get(url); + return response.data.results; + } catch (error) { + console.error(error); + } + }, + getAllCycleQuotas: async function () { + let res = []; + // To be changed once the cycle_quota for cycle is available. + await axios.get('/api/cycle_quota/?limit=1000&offset=0') + .then(response => { + res = response.data.results; + }).catch(function (error) { + console.error('[cycle.services.cycle_quota]', error); + }); + return res; + }, + // Duplicate getCycle: async function(id) { try { - const url = `/api/cycle/${id}`; - const response = await axios.get(url); - return response.data.results; + const response = await axios.get((`/api/cycle/${id}`)); + return response; } catch (error) { console.error(error); } }, - // To be rmoved + // To be removed getAllCycle: async function (){ let res = []; await axios.get('/api/cycle/') @@ -34,7 +55,85 @@ const CycleService = { return res; }, - + //Duplicate + getResources: async function() { + try { + const url = `/api/resource_type`; + const response = await axios.get(url); + return response.data.results; + } catch (error) { + console.error('[cycle.services.getResources]',error); + } + }, + getCycleQuota: async function(id) { + try { + const response = await axios.get((`/api/cycle_quota/${id}`)); + return response.data; + } catch (error) { + console.error(error); + } + }, + saveCycle: async function(cycle, cycleQuota) { + try { + const response = await axios.post(('/api/cycle/'), cycle); + cycle = response.data + for (let quota of cycleQuota) { + quota.cycle = cycle.url; + this.saveCycleQuota(quota); + } + return response.data; + } catch (error) { + console.log(error.response.data); + return error.response.data; + } + }, + saveCycleQuota: async function(cycleQuota) { + try { + const response = await axios.post(('/api/cycle_quota/'),cycleQuota); + return response.data; + } catch (error) { + console.error(error); + return null; + } + }, + updateCycle: async function(id, cycle) { + try { + const response = await axios.put((`/api/cycle/${id}/`), cycle); + return response.data; + } catch (error) { + console.log(error.response.data); + return error.response.data; + } + }, + deleteCycleQuota: async function(cycleQuota) { + try { + const response = await axios.delete(`/api/cycle_quota/${cycleQuota.id}/`); + return response.status===204?{message: 'deleted'}:null; + } catch (error) { + console.error(error); + return null; + } + }, + updateCycleQuota: async function(cycleQuota) { + try { + const response = await axios.put(`/api/cycle_quota/${cycleQuota.id}/`, cycleQuota); + return response.data; + } catch (error) { + console.error(error); + return null; + } + }, + //Duplicate + getCycleDetails: async function(id) { + try { + const response = await axios.get((`/api/cycle/${id}`)); + let cycle = response.data; + return cycle; + } catch(error) { + console.error(error); + return null; + } + }, } -export default CycleService; \ No newline at end of file +export default CycleService; diff --git a/SAS/TMSS/frontend/tmss_webapp/src/services/schedule.service.js b/SAS/TMSS/frontend/tmss_webapp/src/services/schedule.service.js index cda5342a5c6..f09e1da9e87 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/services/schedule.service.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/services/schedule.service.js @@ -14,6 +14,16 @@ const ScheduleService = { }); return res; }, + 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; + }, getSchedulingUnitDraftById: async function (id){ let res = []; await axios.get('/api/scheduling_unit_draft/'+id) @@ -36,7 +46,7 @@ const ScheduleService = { .then(response =>{ for(const task of response.data.results){ let scheduletask = []; - scheduletask['tasktype'] = 'Task Draft'; + scheduletask['tasktype'] = 'Draft'; scheduletask['actionpath'] = '/task/view/draft/'+task['id']; scheduletask['blueprint_draft'] = task['task_blueprints']; -- GitLab