From c4ffdc44bea54f1a732abc73b02668d1047f6680 Mon Sep 17 00:00:00 2001 From: Ramesh Kumar <r.kumar@redkarma.eu> Date: Sat, 25 Jul 2020 23:21:04 +0530 Subject: [PATCH] TMS-275: Add new Project functionality developed with initial requirements. --- SAS/TMSS/frontend/tmss_webapp/package.json | 6 +- SAS/TMSS/frontend/tmss_webapp/src/App.css | 44 ++ SAS/TMSS/frontend/tmss_webapp/src/index.js | 1 + .../src/routes/Project/ResourceInputList.js | 47 ++ .../tmss_webapp/src/routes/Project/create.js | 450 ++++++++++++++++++ .../src/routes/Project/create.test.js | 186 ++++++++ .../tmss_webapp/src/routes/Project/edit.js | 422 ++++++++++++++++ .../tmss_webapp/src/routes/Project/index.js | 4 + .../frontend/tmss_webapp/src/routes/index.js | 5 + .../tmss_webapp/src/services/cycle.service.js | 19 + .../src/services/project.service.js | 140 ++++++ .../tmss_webapp/src/utils/unit.converter.js | 14 + 12 files changed, 1337 insertions(+), 1 deletion(-) create mode 100644 SAS/TMSS/frontend/tmss_webapp/src/routes/Project/ResourceInputList.js create mode 100644 SAS/TMSS/frontend/tmss_webapp/src/routes/Project/create.js create mode 100644 SAS/TMSS/frontend/tmss_webapp/src/routes/Project/create.test.js create mode 100644 SAS/TMSS/frontend/tmss_webapp/src/routes/Project/edit.js create mode 100644 SAS/TMSS/frontend/tmss_webapp/src/routes/Project/index.js create mode 100644 SAS/TMSS/frontend/tmss_webapp/src/services/cycle.service.js create mode 100644 SAS/TMSS/frontend/tmss_webapp/src/services/project.service.js create mode 100644 SAS/TMSS/frontend/tmss_webapp/src/utils/unit.converter.js diff --git a/SAS/TMSS/frontend/tmss_webapp/package.json b/SAS/TMSS/frontend/tmss_webapp/package.json index e312fe079c2..9ca53b5170a 100644 --- a/SAS/TMSS/frontend/tmss_webapp/package.json +++ b/SAS/TMSS/frontend/tmss_webapp/package.json @@ -32,7 +32,7 @@ "react-router-dom": "^5.2.0", "react-scripts": "3.4.1", "react-table": "^7.2.1", - "react-transition-group": "^1.2.1", + "react-transition-group": "^2.5.1", "reactstrap": "^8.5.1", "styled-components": "^5.1.1", "typescript": "^3.9.5", @@ -59,5 +59,9 @@ "last 1 firefox version", "last 1 safari version" ] + }, + "devDependencies": { + "customize-cra": "^0.9.1", + "react-app-rewired": "^1.6.2" } } diff --git a/SAS/TMSS/frontend/tmss_webapp/src/App.css b/SAS/TMSS/frontend/tmss_webapp/src/App.css index 8fb8be12caf..c2f89a4b90f 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/App.css +++ b/SAS/TMSS/frontend/tmss_webapp/src/App.css @@ -94,6 +94,14 @@ p { color: #28289b; } +.p-multiselect-label { + margin-bottom: 0px; +} + +.resource-input-grid div { + margin-bottom: 1rem; +} + .fa { color: #005b9f; } @@ -112,6 +120,42 @@ thead { border-color: #dc3545 !important; } +.pi-primary { + color: #007ad9; +} + +.pi-warning { + color: #ffba01; +} + +.pi-success { + color: #34A835; +} + +.pi-info { + color: #008fba; +} + +.pi-error { + color: #e91224; +} + +.pi-small { + font-size: rem !important; +} + +.pi-medium { + font-size: 1.5rem !important; +} + +.pi-large { + font-size: 2rem !important; +} + +.pi-x-large { + font-size: 3rem !important; +} + @keyframes App-logo-spin { from { transform: rotate(0deg); diff --git a/SAS/TMSS/frontend/tmss_webapp/src/index.js b/SAS/TMSS/frontend/tmss_webapp/src/index.js index 3877566b95c..f24975207fd 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/index.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/index.js @@ -1,3 +1,4 @@ +import 'react-app-polyfill/ie11'; import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/ResourceInputList.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/ResourceInputList.js new file mode 100644 index 00000000000..cb74742285a --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/ResourceInputList.js @@ -0,0 +1,47 @@ +import React, {Component} from 'react'; +import {InputNumber} from 'primereact/inputnumber'; + +/** + * Component to get input for Resource allocation while creating and editing Project + */ +export class ResourceInputList extends Component { + constructor(props) { + super(props); + this.state = { + list: props.list, + projectQuota: props.projectQuota + } + this.updateEnabled = this.props.list.length===0?true:false; + this.onInputChange = this.onInputChange.bind(this); + } + + shouldComponentUpdate() { + return this.updateEnabled; + } + + onInputChange(field, event) { + if (this.props.callback) { + this.props.callback(field, event); + } + } + + 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-3 col-md-3 col-sm-12">{item.name}</label> + <div key={'div1-'+ index} className="col-lg-3 col-md-3 col-sm-12"> + <InputNumber key={'item1-'+ index} id={'item1-'+ index} name={'item1-'+ index} + suffix={` ${this.props.unitMap[item.resourceUnit.name].display}`} + placeholder={` ${this.props.unitMap[item.resourceUnit.name].display}`} + value={this.state.projectQuota[item.name]} + onBlur={(e) => this.onInputChange(item.name, e)} + /> + </div> + </React.Fragment> + ))} + </> + ); + } +} \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/create.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/create.js new file mode 100644 index 00000000000..2a15bd02887 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/create.js @@ -0,0 +1,450 @@ +import React, {Component} from 'react'; +import { Link, Redirect } from 'react-router-dom'; +import _ from 'lodash'; + +import {InputText} from 'primereact/inputtext'; +import {InputNumber} from 'primereact/inputnumber'; +import {InputTextarea} from 'primereact/inputtextarea'; +import {Checkbox} from 'primereact/checkbox'; +import {Dropdown} from 'primereact/dropdown'; +import {MultiSelect} from 'primereact/multiselect'; +import { Button } from 'primereact/button'; +import {Dialog} from 'primereact/components/dialog/Dialog'; +import {Growl} from 'primereact/components/growl/Growl'; + +import {ResourceInputList} from './ResourceInputList'; + +import CycleService from '../../services/cycle.service'; +import ProjectService from '../../services/project.service'; +import UnitConverter from '../../utils/unit.converter'; + +/** + * Component to create a new Project + */ +export class ProjectCreate extends Component { + constructor(props) { + super(props); + this.state = { + dialog: { header: '', detail: ''}, + project: { + trigger_priority: 1000, + priority_rank: null, + project_quota: [], // Mandatory Field in the back end, so an empty array is passed + can_trigger: false + }, + projectQuota: {}, // Resource Allocations + validFields: {}, // For Validation + validForm: false, // To enable Save Button + errors: {}, // Validation Errors + periodCategories: [], + projectCategories: [], + resources: [], // Selected Resources for Allocation + resourceList: [], // Available Resources for Allocation + cycles: [] + } + // Validateion Rules + this.formRules = { + name: {required: true, message: "Name can not be empty"}, + description: {required: true, message: "Description can not be empty"}, + priority_rank: {required: true, message: "Enter Project Rank"} + }; + this.defaultResourcesEnabled = false; // 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:'LOFAR Processing Time'}, + {name:'Allocation storage'}, + {name:'Number of triggers'}, + {name:'LOFAR Support hours'} ]; + this.projectResourceDefaults = {}; // Default values for default resources + this.resourceUnitMap = UnitConverter.resourceUnitMap; // Resource unit conversion factor and constraints + this.cycleOptionTemplate = this.cycleOptionTemplate.bind(this); // Template for cycle multiselect + + this.setProjectQuotaDefaults = this.setProjectQuotaDefaults.bind(this); + this.setProjectParams = this.setProjectParams.bind(this); + this.addNewResource = this.addNewResource.bind(this); + this.setProjectQuotaParams = this.setProjectQuotaParams.bind(this); + this.saveProject = this.saveProject.bind(this); + this.cancelCreate = this.cancelCreate.bind(this); + this.reset = this.reset.bind(this); + } + + componentDidMount() { + ProjectService.getDefaultProjectResources() + .then(defaults => { + this.projectResourceDefaults = defaults; + }); + CycleService.getAllCycles() + .then(cycles => { + this.setState({cycles: cycles}); + }); + ProjectService.getProjectCategories() + .then(categories => { + this.setState({projectCategories: categories}); + }); + ProjectService.getPeriodCategories() + .then(categories => { + this.setState({periodCategories: categories}); + }); + ProjectService.getResources() + .then(resourceList => { + const defaultResources = this.defaultResources; + const resources = _.remove(resourceList, function(resource) { return _.find(defaultResources, {'name': resource.name})!=null }); + const projectQuota = this.setProjectQuotaDefaults(resources); + this.setState({resourceList: resourceList, resources: resources, projectQuota: projectQuota}); + }); + // ProjectService.getProjects().then(projects => { + // console.log(projects); + // }); + } + + /** + * Cycle option sub-component with cycle object + */ + cycleOptionTemplate(option) { + return ( + <div className="p-clearfix"> + <span style={{fontSize:'1em',float:'right',margin:'1em .5em 0 0'}}>{option.name}</span> + </div> + ); + } + + /** + * Function to set project resource allocation + * @param {Array} resources + */ + setProjectQuotaDefaults(resources) { + let projectQuota = this.state.projectQuota; + for (const resource of resources) { + projectQuota[resource['name']] = this.projectResourceDefaults[resource.name]/this.resourceUnitMap[resource.resourceUnit.name].conversionFactor; + } + return projectQuota; + } + + /** + * Function to add new resource to project + */ + addNewResource(){ + if (this.state.newResource) { + let resourceList = this.state.resourceList; + const newResource = _.remove(resourceList, {'name': this.state.newResource}); + let resources = this.state.resources; + resources.push(newResource[0]); + this.setState({resources: resources, resourceList: resourceList, newResource: null}); + } + } + + /** + * Function to call on change and blur events from input components + * @param {string} key + * @param {any} value + */ + setProjectParams(key, value) { + let project = this.state.project; + project[key] = value; + this.setState({project: project, validForm: this.validateForm(key)}); + } + + /** + * Callback Function to call from ResourceInputList on change and blur events + * @param {string} key + * @param {InputEvent} event + */ + setProjectQuotaParams(key, event) { + if (event.target.value) { + let projectQuota = this.state.projectQuota; + let resource = _.find(this.state.resources, {'name': key}); + const resourceUnit = resource?resource.resourceUnit:null; + // console.log(resourceUnit); + if (resourceUnit) { + projectQuota[key] = event.target.value.replace(this.resourceUnitMap[resourceUnit.name].display,''); + } else { + projectQuota[key] = event.target.value; + } + // console.log(`${key} - ${event.target.value}`); + this.setState({projectQuota: projectQuota}); + } + } + + /** + * Validation function to validate the form or field based on the form rules. + * If no argument passed for fieldName, validates all fields in the form. + * @param {string} fieldName + */ + validateForm(fieldName) { + let validForm = false; + let errors = this.state.errors; + let validFields = this.state.validFields; + if (fieldName) { + delete errors[fieldName]; + delete validFields[fieldName]; + if (this.formRules[fieldName]) { + const rule = this.formRules[fieldName]; + const fieldValue = this.state.project[fieldName]; + if (rule.required) { + if (!fieldValue) { + errors[fieldName] = rule.message?rule.message:`${fieldName} is required`; + } else { + validFields[fieldName] = true; + } + } + } + } else { + errors = {}; + validFields = {}; + for (const fieldName in this.formRules) { + const rule = this.formRules[fieldName]; + const fieldValue = this.state.project[fieldName]; + if (rule.required) { + if (!fieldValue) { + errors[fieldName] = rule.message?rule.message:`${fieldName} is required`; + } else { + validFields[fieldName] = true; + } + } + } + } + this.setState({errors: errors, validFields: validFields}); + if (Object.keys(validFields).length === Object.keys(this.formRules).length) { + validForm = true; + } + return validForm; + } + + /** + * Function to call when 'Save' button is clicked to save the project. + */ + saveProject() { + if (this.validateForm) { + let projectQuota = []; + for (const resource in this.state.projectQuota) { + let resourceType = _.find(this.state.resources, {'name': resource}); + let quota = { project: this.state.project.name, + resource_type: resourceType['url'], + value: this.state.projectQuota[resource] * this.resourceUnitMap[resourceType.resourceUnit.name].conversionFactor}; + projectQuota.push(quota); + } + ProjectService.saveProject(this.state.project, this.defaultResourcesEnabled?projectQuota:[]) + .then(project => { + if (project.url) { + let dialog = {}; + if (this.defaultResourcesEnabled) { + dialog = {header: 'Success', detail: 'Project saved successfully. Do you want to create another project?'}; + } else { + dialog = {header: 'Success', detail: 'Project saved successfully with default Resource allocations. Do you want to view and edit them?'}; + } + this.setState({project:project, dialogVisible: true, dialog: dialog}) + } else { + this.growl.show({severity: 'error', summary: 'Error Occured', detail: 'Unable to save Project'}); + this.setState({errors: project}); + } + }); + } + } + + /** + * Function to cancel form creation and navigate to other page/component + */ + cancelCreate() { + this.setState({redirect: '/project/list'}); + } + + /** + * Reset function to be called to reset the form fields + */ + reset() { + if (this.defaultResourcesEnabled) { + let resources = this.state.resources; + let resourceList = []; + const defaultResources = this.defaultResources; + if (resources) { + const nonDefaultResources = _.remove(resources, function(resource) { return _.find(defaultResources, {'name': resource.name})==null }); + resourceList = nonDefaultResources.concat(this.state.resourceList); + } + const projectQuota = this.setProjectQuotaDefaults(resources); + this.setState({ + dialog: { header: '', detail: ''}, + project: { + name: '', + description: '', + trigger_priority: 1000, + priority_rank: null, + project_quota: [] + }, + projectQuota: projectQuota, + validFields: {}, + validForm: false, + errors: {}, + dialogVisible: false, + resources: resources, + resourceList: resourceList + }); + } else { + this.setState({redirect: `/project/edit/${this.state.project.name}`}) + } + } + + render() { + if (this.state.redirect) { + return <Redirect to={ {pathname: this.state.redirect} }></Redirect> + } + + return ( + <React.Fragment> + <div className="p-grid"> + <Growl ref={(el) => this.growl = el} /> + + <div className="p-col-10 p-lg-3 p-md-4"> + <h2>Project - Add</h2> + </div> + <div className="p-col-2 p-lg-3 p-md-4"> + <Link to={{ pathname: '/project'}} tooltip="Close Edit" > + <i className="fa fa-window-close" style={{marginTop: "10px"}}></i> + </Link> + </div> + </div> + <div> + <div className="p-fluid"> + <div className="p-field p-grid" style={{display: 'none'}}> + <label htmlFor="projectId" className="col-lg-2 col-md-2 col-sm-12">URL </label> + <div className="col-lg-4 col-md-4 col-sm-12"> + <input id="projectId" data-testid="projectId" value={this.state.project.url} /> + </div> + </div> + <div className="p-field p-grid"> + <label htmlFor="projectName" className="col-lg-2 col-md-2 col-sm-12">Name <span style={{color:'red'}}>*</span></label> + <div className="col-lg-4 col-md-4 col-sm-12"> + <InputText className={this.state.errors.name ?'input-error':''} id="projectName" data-testid="name" + value={this.state.project.name} + onChange={(e) => this.setProjectParams('name', e.target.value)} + onBlur={(e) => this.setProjectParams('name', e.target.value)}/> + <label className="error"> + {this.state.errors.name ? this.state.errors.name : ""} + </label> + </div> + <label htmlFor="description" className="col-lg-2 col-md-2 col-sm-12">Description <span style={{color:'red'}}>*</span></label> + <div className="col-lg-4 col-md-4 col-sm-12"> + <InputTextarea className={this.state.errors.description ?'input-error':''} rows={3} cols={30} + data-testid="description" value={this.state.project.description} + onChange={(e) => this.setProjectParams('description', e.target.value)} + onBlur={(e) => this.setProjectParams('description', e.target.value)}/> + <label className="error"> + {this.state.errors.description ? this.state.errors.description : ""} + </label> + </div> + </div> + <div className="p-field p-grid"> + <label htmlFor="triggerPriority" className="col-lg-2 col-md-2 col-sm-12">Trigger Priority </label> + <div className="col-lg-4 col-md-4 col-sm-12" data-testid="trig_prio"> + <InputNumber inputId="trig_prio" name="trig_prio" value={this.state.project.trigger_priority} + mode="decimal" showButtons min={0} max={1001} step={10} useGrouping={false} + onChange={(e) => this.setProjectParams('trigger_priority', e.target.value)} + onBlur={(e) => this.setProjectParams('trigger_priority', e.target.value)} /> + + <label className="error"> + {this.state.errors.trigger_priority ? this.state.errors.trigger_priority : ""} + </label> + </div> + <label htmlFor="trigger" className="col-lg-2 col-md-2 col-sm-12">Allows Trigger Submission</label> + <div className="col-lg-4 col-md-4 col-sm-12" data-testid="trigger"> + <Checkbox inputId="trigger" role="trigger" checked={this.state.project.can_trigger} onChange={e => this.setProjectParams('can_trigger', e.target.checked)}></Checkbox> + </div> + </div> + <div className="p-field p-grid"> + <label htmlFor="projCat" className="col-lg-2 col-md-2 col-sm-12">Project Category </label> + <div className="col-lg-4 col-md-4 col-sm-12" data-testid="projCat" > + <Dropdown inputId="projCat" optionLabel="name" optionValue="id" + value={this.state.project.project_category} + options={this.state.projectCategories} + onChange={(e) => {this.setProjectParams('project_category', e.value)}} + placeholder="Select Project Category" /> + </div> + <label htmlFor="periodCategory" className="col-lg-2 col-md-2 col-sm-12">Period Category</label> + <div className="col-lg-4 col-md-4 col-sm-12"> + <Dropdown data-testid="period-cat" id="period-cat" optionLabel="name" optionValue="id" + value={this.state.project.period_category} + options={this.state.periodCategories} + onChange={(e) => {this.setProjectParams('period_category',e.value)}} + placeholder="Select Period Category" /> + </div> + </div> + <div className="p-field p-grid"> + <label htmlFor="triggerPriority" className="col-lg-2 col-md-2 col-sm-12">Cycle(s)</label> + <div className="col-lg-4 col-md-4 col-sm-12"> + <MultiSelect data-testid="cycle" id="cycle" optionLabel="name" optionValue="url" filter={true} + value={this.state.project.cycles} + options={this.state.cycles} + onChange={(e) => {this.setProjectParams('cycles',e.value)}} + + /> + </div> + <label htmlFor="projRank" className="col-lg-2 col-md-2 col-sm-12">Project Rank <span style={{color:'red'}}>*</span></label> + <div className="col-lg-4 col-md-4 col-sm-12" data-testid="proj-rank" > + <InputNumber inputId="proj-rank" name="rank" data-testid="rank" value={this.state.project.priority_rank} + mode="decimal" showButtons min={0} max={100} + onChange={(e) => this.setProjectParams('priority_rank', e.target.value)} + onBlur={(e) => this.setProjectParams('priority_rank', e.target.value)} /> + <label className="error"> + {this.state.errors.priority_rank ? this.state.errors.priority_rank : ""} + </label> + </div> + </div> + + {this.defaultResourcesEnabled && this.state.resourceList && + <div className="p-fluid"> + <div className="p-field p-grid"> + <div className="col-lg-3 col-md-3 col-sm-112"> + <span data-testid="resource_alloc">{this.state.resourceList[0]?this.state.resourceList[0].name: 'Resource Allocations'}</span> + </div> + <div className="col-lg-3 col-md-3 col-sm-10"> + <Dropdown optionLabel="name" optionValue="name" + 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} /> + </div> + </div> + <div className="p-field p-grid resource-input-grid"> + <ResourceInputList list={this.state.resources} unitMap={this.resourceUnitMap} + projectQuota={this.state.projectQuota} callback={this.setProjectQuotaParams} /> + </div> + </div> + } + </div> + </div> + <div className="p-grid p-justify-start"> + <div className="p-col-1"> + <Button label="Save" className="p-button-primary" id="save-btn" data-testid="save-btn" icon="pi pi-check" onClick={this.saveProject} disabled={!this.state.validForm} /> + </div> + <div className="p-col-1"> + <Button label="Cancel" className="p-button-danger" icon="pi pi-times" onClick={this.cancelCreate} /> + </div> + </div> + + {/* Dialog component to show messages and get input */} + <div className="p-grid" data-testid="confirm_dialog"> + <Dialog header={this.state.dialog.header} visible={this.state.dialogVisible} style={{width: '50vw'}} inputId="confirm_dialog" + 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-1 col-md-1 col-sm-2"> + <i className="pi pi-check-circle pi-large pi-success"></i> + </div> + <div className="col-lg-10 col-md-10 col-sm-10"> + {this.state.dialog.detail} + </div> + </div> + </Dialog> + </div> + + </React.Fragment> + ); + } +} \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/create.test.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/create.test.js new file mode 100644 index 00000000000..1e9cdd6c148 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/create.test.js @@ -0,0 +1,186 @@ +import React from 'react'; +import { BrowserRouter as Router } from 'react-router-dom'; +import { act } from "react-dom/test-utils"; +import { render, cleanup, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; + +import {ProjectCreate} from './create'; +import ProjectService from '../../services/project.service'; +import CycleService from '../../services/cycle.service'; + +let projectCategoriesSpy, allCycleSpy, periodCategoriesSpy, saveProjectSpy; + +beforeEach(() => { + setMockSpy(); +}); + +afterEach(() => { + // cleanup on exiting + clearMockSpy(); + cleanup(); +}); + +/** + * To set mock spy for Services that have API calls to the back end to fetch data + */ +const setMockSpy = (() => { + projectCategoriesSpy = jest.spyOn(ProjectService, 'getProjectCategories'); + projectCategoriesSpy.mockImplementation(() => { return Promise.resolve([{id: 1, name: 'Regular'}])}); + periodCategoriesSpy = jest.spyOn(ProjectService, 'getPeriodCategories'); + periodCategoriesSpy.mockImplementation(() => { return Promise.resolve([{id: 1, name: 'Single Cycle'}])}); + allCycleSpy = jest.spyOn(CycleService, 'getAllCycles'); + allCycleSpy.mockImplementation(() => { + return Promise.resolve([{url: "http://localhost:3000/api/cycle/Cycle-0", name: 'Cycle-0'}, + {url: "http://localhost:3000/api/cycle/Cycle-1", name: 'Cycle-1'}]); + }); + saveProjectSpy = jest.spyOn(ProjectService, 'saveProject'); + saveProjectSpy.mockImplementation((project, projectQuota) => { + project.url = `http://localhost:3000/api/project/${project.name}`; + return Promise.resolve(project) + }); +}); + +const clearMockSpy = (() => { + projectCategoriesSpy.mockRestore(); + periodCategoriesSpy.mockRestore(); + allCycleSpy.mockRestore(); + saveProjectSpy.mockRestore(); +}); + +it("renders without crashing with all back-end data loaded", async () => { + console.log("renders without crashing with all back-end data loaded ------------------------"); + + let content; + await act(async () => { + content = render(<Router><ProjectCreate /></Router>); + }); + + expect(content.queryByText('Project - Add')).not.toBe(null); // Page loaded successfully + expect(projectCategoriesSpy).toHaveBeenCalled(); // Mock Spy called successfully + expect(content.queryByText('Regular')).toBeInTheDocument(); // Project Category Dropdown loaded successfully + expect(content.queryByText('Single Cycle')).toBeInTheDocument(); // Period Category Dropdown loaded successfully + expect(content.queryByText('Cycle-0')).toBeInTheDocument(); // Cycle multi-select loaded successfully +}); + +it("Save button disabled initially when no data entered", async () => { + console.log("Save button disabled initially when no data entered -----------------------"); + let content; + await act(async () => { + content = render(<Router><ProjectCreate /></Router>); + }); + expect(content.queryByTestId('save-btn')).toHaveAttribute("disabled"); +}); + +it("Save button enabled when mandatory data entered", async () => { + console.log("Save button enabled when mandatory data entered -----------------------"); + let content; + await act(async () => { + content = render(<Router><ProjectCreate /></Router>); + }); + const nameInput = content.queryByTestId('name'); + const descInput = content.queryByTestId('description'); + const spinButtons = content.queryAllByRole("spinbutton"); + const rankInput = spinButtons.filter(function(element) { return element.id==="proj-rank"})[0]; + + // Set values for all mandatory input and test if save button is enabled + fireEvent.change(nameInput, { target: { value: 'OSR' } }); + expect(nameInput.value).toBe("OSR"); + fireEvent.change(descInput, { target: { value: 'OSR' } }); + expect(descInput.value).toBe("OSR"); + fireEvent.blur(rankInput, { target: { value: 1 } }); + expect(rankInput.value).toBe("1"); + expect(content.queryByTestId('save-btn').hasAttribute("disabled")).toBeFalsy(); +}); + +it("renders Save button enabled when all data entered", async () => { + console.log("renders Save button enabled when all data entered -----------------------"); + let content; + await act(async () => { + content = render(<Router><ProjectCreate /></Router>); + }); + + const nameInput = content.queryByTestId('name'); + const descInput = content.queryByTestId('description'); + const spinButtons = content.queryAllByRole("spinbutton"); + const rankInput = spinButtons.filter(function(element) { return element.id==="proj-rank"})[0]; + const trigPrioInput = spinButtons.filter(function(element) { return element.id==="trig_prio"})[0]; + const trigger = content.getByLabelText(/trigger/i); + const projCatInput = content.getAllByRole("listbox")[0].children[0] ; + const projPeriodInput = content.getAllByRole("listbox")[1].children[0] ; + const cycleInput = content.getAllByRole("listbox")[2].children[0] ; + + fireEvent.change(nameInput, { target: { value: 'OSR' } }); + expect(nameInput.value).toBe("OSR"); + + fireEvent.change(descInput, { target: { value: 'OSR' } }); + expect(descInput.value).toBe("OSR"); + + fireEvent.blur(rankInput, { target: { value: 1 } }); + expect(rankInput.value).toBe("1"); + + expect(trigPrioInput.value).toBe("1000"); // Check for default value + fireEvent.blur(trigPrioInput, { target: { value: 100 } }); + expect(trigPrioInput.value).toBe("100"); // Check for new value + + fireEvent.click(trigger); + expect(trigger.hasAttribute("checked")).toBeTruthy(); + + // Before selecting Project Category + expect(content.queryAllByText('Select Project Category').length).toBe(2); + expect(content.queryAllByText('Regular').length).toBe(1); + expect(content.getAllByRole("listbox")[0].children.length).toBe(1); + fireEvent.click(projCatInput); + // After selecting Project Category + expect(content.queryAllByText('Select Project Category').length).toBe(1); + expect(content.queryAllByText('Regular').length).toBe(3); + + // Before selecting Period Category + expect(content.queryAllByText('Select Period Category').length).toBe(2); + expect(content.queryAllByText('Single Cycle').length).toBe(1); + expect(content.getAllByRole("listbox")[1].children.length).toBe(1); + fireEvent.click(projPeriodInput); + // After selecting Period Category + expect(content.queryAllByText('Select Period Category').length).toBe(1); + expect(content.queryAllByText('Single Cycle').length).toBe(3); + + // Before selecting Cycle + expect(content.queryAllByText('Cycle-0').length).toBe(1); + expect(content.getAllByRole("listbox")[2].children.length).toBe(2); + fireEvent.click(cycleInput); + // After selecting Cycle + expect(content.queryAllByText('Cycle-0').length).toBe(2); + + expect(content.queryByTestId('save-btn').hasAttribute("disabled")).toBeFalsy(); + // }); +}); + +it("save project with mandatory fields", async () => { + console.log("save project -----------------------"); + let content; + await act(async () => { + content = render(<Router><ProjectCreate /></Router>); + }); + + const nameInput = content.queryByTestId('name'); + const descInput = content.queryByTestId('description'); + const spinButtons = content.queryAllByRole("spinbutton"); + const rankInput = spinButtons.filter(function(element) { return element.id==="proj-rank"})[0]; + + fireEvent.change(nameInput, { target: { value: 'OSR' } }); + expect(nameInput.value).toBe("OSR"); + fireEvent.change(descInput, { target: { value: 'OSR' } }); + expect(descInput.value).toBe("OSR"); + fireEvent.blur(rankInput, { target: { value: 1 } }); + expect(rankInput.value).toBe("1"); + expect(content.queryByTestId('save-btn').hasAttribute("disabled")).toBeFalsy(); + expect(content.queryByTestId('projectId').value).toBe(""); + expect(content.queryByText("Success")).toBe(null); + + await act(async () => { + fireEvent.click(content.queryByTestId('save-btn')); + }); + + // After saving project, URL should be available and Success dialog should be displayed + expect(content.queryByTestId('projectId').value).toBe("http://localhost:3000/api/project/OSR"); + expect(content.queryByText("Success")).not.toBe(null); +}); \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/edit.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/edit.js new file mode 100644 index 00000000000..3f838144473 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/edit.js @@ -0,0 +1,422 @@ +import React, {Component} from 'react'; +import { Link, Redirect } from 'react-router-dom'; +import _ from 'lodash'; + +import {InputText} from 'primereact/inputtext'; +import {InputNumber} from 'primereact/inputnumber'; +import {InputTextarea} from 'primereact/inputtextarea'; +import {Checkbox} from 'primereact/checkbox'; +import {Dropdown} from 'primereact/dropdown'; +import {MultiSelect} from 'primereact/multiselect'; +import { Button } from 'primereact/button'; +import {Dialog} from 'primereact/components/dialog/Dialog'; +import {Growl} from 'primereact/components/growl/Growl'; + +import {ResourceInputList} from './ResourceInputList'; + +import CycleService from '../../services/cycle.service'; +import ProjectService from '../../services/project.service'; +import UnitConverter from '../../utils/unit.converter'; + +export class ProjectEdit extends Component { + constructor(props) { + super(props); + this.state = { + dialog: { header: '', detail: ''}, + project: { + trigger_priority: 1000, + priority_rank: null, + project_quota: [] // Mandatory Field in the back end + }, + projectQuota: {}, + validFields: {}, + validForm: false, + errors: {}, + periodCategories: [], + projectCategories: [], + resources: [], + resourceList: [], + cycles: [] + } + this.updateEnabled = true; + this.formRules = { + name: {required: true, message: "Name can not be empty"}, + description: {required: true, message: "Description can not be empty"}, + priority_rank: {required: true, message: "Enter Project Rank"} + }; + this.defaultResourcesEnabled = true; // This property and functionality to be concluded based on PO input + this.defaultResources = [{name:'LOFAR Observing Time'}, + {name:'LOFAR Observing Time prio A'}, + {name:'LOFAR Observing Time prio B'}, + {name:'LOFAR Processing Time'}, + {name:'Allocation storage'}, + {name:'Number of triggers'}, + {name:'LOFAR Support hours'} ]; + this.projectResourceDefaults = {}; + this.resourceUnitMap = UnitConverter.resourceUnitMap; + this.cycleOptionTemplate = this.cycleOptionTemplate.bind(this); + this.setProjectQuotaDefaults = this.setProjectQuotaDefaults.bind(this); + this.setProjectParams = this.setProjectParams.bind(this); + this.setUpdateEnabled = this.setUpdateEnabled.bind(this); + this.addNewResource = this.addNewResource.bind(this); + this.setProjectQuotaParams = this.setProjectQuotaParams.bind(this); + this.saveProject = this.saveProject.bind(this); + this.cancelCreate = this.cancelCreate.bind(this); + this.reset = this.reset.bind(this); + } + + componentDidMount() { + ProjectService.getDefaultProjectResources() + .then(defaults => { + this.projectResourceDefaults = defaults; + }); + CycleService.getAllCycles() + .then(cycles => { + this.setState({cycles: cycles}); + }); + ProjectService.getProjectCategories() + .then(categories => { + this.setState({projectCategories: categories}); + }); + ProjectService.getPeriodCategories() + .then(categories => { + this.setState({periodCategories: categories}); + }); + ProjectService.getResources() + .then(resourceList => { + const defaultResources = this.defaultResources; + const resources = _.remove(resourceList, function(resource) { return _.find(defaultResources, {'name': resource.name})!=null }); + // Object.assign(resources, this.defaultResources); + console.log(resources); + const projectQuota = this.setProjectQuotaDefaults(resources); + this.setState({resourceList: resourceList, resources: resources, projectQuota: projectQuota}); + }); + + } + + cycleOptionTemplate(option) { + return ( + <div className="p-clearfix"> + <span style={{fontSize:'1em',float:'right',margin:'1em .5em 0 0'}}>{option.name}</span> + </div> + ); + } + + setProjectQuotaDefaults(resources) { + let projectQuota = this.state.projectQuota; + for (const resource of resources) { + console.log(resource['name']); + projectQuota[resource['name']] = this.projectResourceDefaults[resource.name]/this.resourceUnitMap[resource.resourceUnit.name].conversionFactor; + } + return projectQuota; + } + + addNewResource(){ + if (this.state.newResource) { + console.log(this.state.newResource); + let resourceList = this.state.resourceList; + const newResource = _.remove(resourceList, {'name': this.state.newResource}); + console.log(newResource); + let resources = this.state.resources; + resources.push(newResource[0]); + this.setState({resources: resources, resourceList: resourceList, newResource: null}); + } + } + + setProjectParams(key, value) { + let project = this.state.project; + project[key] = value; + console.log(`${key} - ${value}`); + this.setState({project: project, validForm: this.validateForm(key)}); + } + + setUpdateEnabled(enable) { + this.updateEnabled = enable; + } + + setProjectQuotaParams(key, event) { + console.log(key); + console.log(event.target) + console.log(event.target.value); + if (event.target.value) { + let projectQuota = this.state.projectQuota; + let resource = _.find(this.state.resources, {'name': key}); + const resourceUnit = resource?resource.resourceUnit:null; + console.log(resourceUnit); + if (resourceUnit) { + projectQuota[key] = event.target.value.replace(this.resourceUnitMap[resourceUnit.name].display,''); + } else { + projectQuota[key] = event.target.value; + } + console.log(`${key} - ${event.target.value}`); + this.setState({projectQuota: projectQuota}); + } + } + + /** + * Function to validate the form excluding the JSON Editor values + */ + validateForm(fieldName) { + let validForm = false; + let errors = this.state.errors; + let validFields = this.state.validFields; + if (fieldName) { + delete errors[fieldName]; + delete validFields[fieldName]; + if (this.formRules[fieldName]) { + const rule = this.formRules[fieldName]; + const fieldValue = this.state.project[fieldName]; + if (rule.required) { + if (!fieldValue) { + errors[fieldName] = rule.message?rule.message:`${fieldName} is required`; + } else { + validFields[fieldName] = true; + } + } + } + } else { + errors = {}; + validFields = {}; + for (const fieldName in this.formRules) { + const rule = this.formRules[fieldName]; + const fieldValue = this.state.project[fieldName]; + if (rule.required) { + if (!fieldValue) { + errors[fieldName] = rule.message?rule.message:`${fieldName} is required`; + } else { + validFields[fieldName] = true; + } + } + } + } + console.log(errors); + this.setState({errors: errors, validFields: validFields}); + if (Object.keys(validFields).length === Object.keys(this.formRules).length) { + validForm = true; + } + return validForm; + } + + saveProject() { + if (this.validateForm) { + console.log(this.state.project); + console.log(this.state.projectQuota); + let projectQuota = []; + for (const resource in this.state.projectQuota) { + let resourceType = _.find(this.state.resources, {'name': resource}); + let quota = { project: this.state.project.name, + resource_type: resourceType['url'], + value: this.state.projectQuota[resource] * this.resourceUnitMap[resourceType.resourceUnit.name].conversionFactor}; + projectQuota.push(quota); + } + console.log(projectQuota); + ProjectService.saveProject(this.state.project, this.defaultResourcesEnabled?projectQuota:[]) + .then(project => { + if (project.url) { + let dialog = {}; + if (this.defaultResourcesEnabled) { + dialog = {header: 'Success', detail: 'Project saved successfully. Do you want to create another project?'}; + } else { + dialog = {header: 'Success', detail: 'Project saved successfully with default Resource allocations. Do you want to view and edit them?'}; + } + this.setState({dialogVisible: true, dialog: dialog}) + } else { + console.log(this.growl); + console.log(project); + this.growl.show({severity: 'error', summary: 'Error Occured', detail: 'Unable to save Project'}); + this.setState({errors: project}); + } + }); + } + } + + cancelCreate() {} + + reset() { + let resources = this.state.resources; + let resourceList = []; + const defaultResources = this.defaultResources; + if (resources) { + const nonDefaultResources = _.remove(resources, function(resource) { return _.find(defaultResources, {'name': resource.name})==null }); + resourceList = nonDefaultResources.concat(this.state.resourceList); + } + const projectQuota = this.setProjectQuotaDefaults(resources); + this.setState({ + dialog: { header: '', detail: ''}, + project: { + name: '', + description: '', + trigger_priority: 1000, + priority_rank: null, + project_quota: [] + }, + projectQuota: projectQuota, + validFields: {}, + validForm: false, + errors: {}, + dialogVisible: false, + resources: resources, + resourceList: resourceList + }); + } + + shouldComponentUpdate() { + return this.updateEnabled; + } + + render() { + if (this.state.redirect) { + return <Redirect to={ {pathname: this.state.redirect} }></Redirect> + } + + + console.log(this.defaultResources); + console.log(this.state.resourceList); + console.log(this.state.resources); + + return ( + <React.Fragment> + <div className="p-grid"> + <Dialog header={this.state.dialog.header} visible={this.state.dialogVisible} style={{width: '50vw'}} + modal={true} onHide={() => {this.setState({dialogVisible: false})}} + footer={<div> + <Button key="back" onClick={() => {this.setState({dialogVisible: false})}} label="No" /> + <Button key="submit" type="primary" onClick={this.reset} label="Yes" /> + </div> + } > + <div className="p-grid"> + <div className="col-lg-1 col-md-1 col-sm-2"> + <i className="pi pi-check-circle pi-large pi-success"></i> + </div> + <div className="col-lg-10 col-md-10 col-sm-10"> + {this.state.dialog.detail} + </div> + </div> + </Dialog> + </div> + <div className="p-grid"> + <Growl ref={(el) => this.growl = el} /> + + <div className="p-col-10 p-lg-3 p-md-4"> + <h2>Project - Edit</h2> + </div> + <div className="p-col-2 p-lg-3 p-md-4"> + <Link to={{ pathname: '/project'}} tooltip="Close Edit" > + <i className="fa fa-window-close" style={{marginTop: "10px"}}></i> + </Link> + </div> + </div> + <div> + <div className="p-fluid"> + <div className="p-field p-grid"> + <label htmlFor="projectName" className="col-lg-2 col-md-2 col-sm-12">Name <span style={{color:'red'}}>*</span></label> + <div className="col-lg-4 col-md-4 col-sm-12"> + <InputText className={this.state.errors.name ?'input-error':''} id="projectName" type="text" + value={this.state.project.name} + onChange={(e) => this.setProjectParams('name', e.target.value)} + onBlur={(e) => this.setProjectParams('name', e.target.value)}/> + <label className="error"> + {this.state.errors.name ? this.state.errors.name : ""} + </label> + </div> + <label htmlFor="description" className="col-lg-2 col-md-2 col-sm-12">Description <span style={{color:'red'}}>*</span></label> + <div className="col-lg-4 col-md-4 col-sm-12"> + <InputTextarea className={this.state.errors.description ?'input-error':''} rows={3} cols={30} + value={this.state.project.description} + onChange={(e) => this.setProjectParams('description', e.target.value)} + onBlur={(e) => this.setProjectParams('description', e.target.value)}/> + <label className="error"> + {this.state.errors.description ? this.state.errors.description : ""} + </label> + </div> + </div> + <div className="p-field p-grid"> + <label htmlFor="triggerPriority" className="col-lg-2 col-md-2 col-sm-12">Trigger Priority </label> + <div className="col-lg-4 col-md-4 col-sm-12"> + <InputNumber className={this.state.errors.name ?'input-error':''} id="triggerPriority" + value={this.state.project.trigger_priority} showButtons step={10} + onChange={(e) => this.setProjectParams('trigger_priority', e.target.value)}/> + <label className="error"> + {this.state.errors.trigger_priority ? this.state.errors.trigger_priority : ""} + </label> + </div> + <label htmlFor="trigger" className="col-lg-2 col-md-2 col-sm-12">Allows Trigger Submission</label> + <div className="col-lg-4 col-md-4 col-sm-12"> + <Checkbox checked={this.state.project.can_trigger} onChange={e => this.setProjectParams('can_trigger', e.target.checked)}></Checkbox> + </div> + </div> + <div className="p-field p-grid"> + <label htmlFor="projCategory" className="col-lg-2 col-md-2 col-sm-12">Project Category </label> + <div className="col-lg-4 col-md-4 col-sm-12"> + <Dropdown optionLabel="name" optionValue="id" + value={this.state.project.project_category} + options={this.state.projectCategories} + onChange={(e) => {this.setProjectParams('project_category', e.value)}} + placeholder="Select Project Category" /> + </div> + <label htmlFor="periodCategory" className="col-lg-2 col-md-2 col-sm-12">Period Category</label> + <div className="col-lg-4 col-md-4 col-sm-12"> + <Dropdown optionLabel="name" optionValue="id" + value={this.state.project.period_category} + options={this.state.periodCategories} + onChange={(e) => {this.setProjectParams('period_category',e.value)}} + placeholder="Select Period Category" /> + </div> + </div> + <div className="p-field p-grid"> + <label htmlFor="triggerPriority" className="col-lg-2 col-md-2 col-sm-12">Cycle(s)</label> + <div className="col-lg-4 col-md-4 col-sm-12"> + <MultiSelect optionLabel="name" optionValue="url" filter={true} + value={this.state.project.cycles} + options={this.state.cycles} + onChange={(e) => {this.setProjectParams('cycles',e.value)}} + + /> + </div> + <label htmlFor="projRank" className="col-lg-2 col-md-2 col-sm-12">Project Rank <span style={{color:'red'}}>*</span></label> + <div className="col-lg-4 col-md-4 col-sm-12"> + <InputNumber id="projRank" value={this.state.project.priority_rank} mode="decimal" showButtons min={0} max={100} + onChange={(e) => this.setProjectParams('priority_rank', e.value)} + onBlur={(e) => this.setProjectParams('priority_rank', e.target.value)} /> + <label className="error"> + {this.state.errors.priority_rank ? this.state.errors.priority_rank : ""} + </label> + </div> + </div> + {this.defaultResourcesEnabled && this.state.resourceList && + <div className="p-fluid"> + <div className="p-field p-grid"> + <div className="col-lg-3 col-md-3 col-sm-112"> + <h5>Resource Allocations:</h5> + </div> + <div className="col-lg-3 col-md-3 col-sm-10"> + <Dropdown optionLabel="name" optionValue="name" + 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} /> + </div> + </div> + <div className="p-field p-grid resource-input-grid"> + <ResourceInputList list={this.state.resources} unitMap={this.resourceUnitMap} + projectQuota={this.state.projectQuota} callback={this.setProjectQuotaParams} /> + </div> + </div> + } + </div> + </div> + <div className="p-grid p-justify-start"> + <div className="p-col-1"> + <Button label="Save" className="p-button-primary" icon="pi pi-check" onClick={this.saveProject} disabled={!this.state.validForm} /> + </div> + <div className="p-col-1"> + <Button label="Cancel" className="p-button-danger" icon="pi pi-times" onClick={this.cancelCreate} /> + </div> + </div> + </React.Fragment> + ); + } +} \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/index.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/index.js new file mode 100644 index 00000000000..ece808e56c5 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/index.js @@ -0,0 +1,4 @@ +import {ProjectCreate} from './create'; +import {ProjectEdit} from './edit'; + +export {ProjectCreate, ProjectEdit} ; diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/index.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/index.js index c2ed5db0bf2..62c30999ae3 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/index.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/index.js @@ -6,6 +6,7 @@ import { } from 'react-router-dom'; import {NotFound} from '../layout/components/NotFound'; +import {ProjectCreate, ProjectEdit} from './Project'; import {Dashboard} from './Dashboard'; import {Scheduling} from './Scheduling'; import {TaskEdit, TaskView} from './Task'; @@ -17,6 +18,10 @@ export const RoutedContent = () => { <Redirect from="/" to="/" exact /> <Route path="/not-found" exact component= {NotFound} /> <Route path="/dashboard" exact component={Dashboard} /> + <Route path="/project" exact component={NotFound} /> + <Route path="/project/create" exact component={ProjectCreate} /> + <Route path="/project/edit" exact component={ProjectEdit} /> + <Route path="/project/edit/:id" exact component={ProjectEdit} /> <Route path="/scheduling" exact component={Scheduling} /> <Route path="/task" exact component={TaskView} /> <Route path="/task/view" exact component={TaskView} /> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/services/cycle.service.js b/SAS/TMSS/frontend/tmss_webapp/src/services/cycle.service.js new file mode 100644 index 00000000000..ebdbc4c11e0 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/services/cycle.service.js @@ -0,0 +1,19 @@ +const axios = require('axios'); + +//axios.defaults.baseURL = 'http://192.168.99.100:8008/api'; +axios.defaults.headers.common['Authorization'] = 'Basic dGVzdDp0ZXN0'; + +const CycleService = { + getAllCycles: async function() { + try { + const url = `/api/cycle`; + const response = await axios.get(url); + return response.data.results; + } catch (error) { + console.error(error); + } + }, + +} + +export default CycleService; \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/services/project.service.js b/SAS/TMSS/frontend/tmss_webapp/src/services/project.service.js new file mode 100644 index 00000000000..8b1b176a59b --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/services/project.service.js @@ -0,0 +1,140 @@ +import _ from 'lodash'; + +const axios = require('axios'); + +axios.defaults.headers.common['Authorization'] = 'Basic dGVzdDp0ZXN0'; + +const ProjectService = { + getProjectCategories: async function() { + try { + const url = `/api/cycle`; + const response = await axios.get(url); + //return response.data.results; + return [ + {id: 1, name: "Regular"}, + {id: 2, name: "User Shared Support"}, + {id: 3, name: "Commissioning"}, + {id: 4, name: "DDT"}, + {id: 5, name: "Test"} + ]; + } catch (error) { + console.error(error); + } + }, + getPeriodCategories: async function() { + try { + const url = `/api/cycle`; + const response = await axios.get(url); + // return response.data.results; + return [ + {id: 1, name: "Single Cycle"}, + {id: 2, name: "Long Term"}, + {id: 3, name: "Unbounded"} + ]; + } catch (error) { + console.error(error); + } + }, + getResources: async function() { + return this.getResourceTypes() + .then(resourceTypes => { + return this.getResourceUnits() + .then(resourceUnits => { + for (let resourceType of resourceTypes) { + resourceType.resourceUnit = _.find(resourceUnits, ['name', resourceType.resource_unit_id]); + } + return resourceTypes; + }) + }) + }, + getResourceTypes: async function() { + try { + // const url = `/api/resource_type/?ordering=name`; + const url = `/api/resource_type`; + const response = await axios.get(url); + // console.log(response); + return response.data.results; + } catch (error) { + console.error(error); + } + }, + getResourceUnits: async function() { + try { + const url = `/api/resource_unit`; + const response = await axios.get(url); + // console.log(response); + return response.data.results; + } catch (error) { + console.error(error); + } + }, + getDefaultProjectResources: async function() { + try { + const url = `/api/resource_unit`; + const response = await axios.get(url); + // return response.data.results; + return {'LOFAR Observing Time': 3600, + 'LOFAR Observing Time prio A': 3600, + 'LOFAR Observing Time prio B': 3600, + 'LOFAR Processing Time': 3600, + 'Allocation storage': 1024*1024*1024*1024, + 'Number of triggers': 1, + 'LOFAR Support hours': 3600}; + } catch (error) { + console.error(error); + } + }, + saveProject: async function(project, projectQuota) { + try { + const response = await axios.post(('/api/project/'), project); + project = response.data + for (let quota of projectQuota) { + quota.project = project.url; + this.saveProjectQuota(quota); + } + return response.data; + } catch (error) { + // console.log(error); + console.log(error.response.data); + return error.response.data; + } + }, + saveProjectQuota: async function(projectQuota) { + try { + const response = await axios.post(('/api/project_quota/'), projectQuota); + return response.data; + } catch (error) { + console.error(error); + return null; + } + }, + getProjects: async function() { + try { + const response = await axios.get(('/api/project/')); + let projects = response.data.results; + const response1 = await axios.get(('/api/project_quota')); + const allProjectQuota = response1.data.results; + for (let project of projects) { + let projectQuota = _.filter(allProjectQuota, function(projQuota) { return _.includes(project.project_quota_ids, projQuota.id)}); + for (const quota of projectQuota) { + project[quota.resource_type_id] = quota; + } + } + return response.data.results; + } catch (error) { + console.error(error); + return null; + } + }, + getProjectQuota: async function(quotaId) { + try { + const response = await axios.get((`/api/project_quota/${quotaId}`)); + return response.data; + } catch (error) { + console.error(error); + return null; + } + } +} + +export default ProjectService; \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/utils/unit.converter.js b/SAS/TMSS/frontend/tmss_webapp/src/utils/unit.converter.js new file mode 100644 index 00000000000..13f234ef673 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/utils/unit.converter.js @@ -0,0 +1,14 @@ +const UnitConverter = { + resourceUnitMap: {'second':{display: 'Hours', conversionFactor: 3600, mode:'decimal', minFractionDigits:0, maxFractionDigits: 2 }, + 'byte': {display: 'TB', conversionFactor: (1024*1024*1024*1024), mode:'decimal', minFractionDigits:0, maxFractionDigits: 3}, + 'number': {display: 'Numbers', conversionFactor: 1, mode:'decimal', minFractionDigits:0, maxFractionDigits: 0}}, + + getDBResourceUnit: function() { + + }, + getUIResourceUnit: function() { + + } +}; + +export default UnitConverter; \ No newline at end of file -- GitLab