Code owners
Assign users and groups as approvers for specific file changes. Learn more.
edit.js 25.99 KiB
import React, {Component} from 'react';
import { Link, Redirect } from 'react-router-dom';
import _ from 'lodash';
import {InputText} from 'primereact/inputtext';
import {InputNumber} from 'primereact/inputnumber';
import {InputTextarea} from 'primereact/inputtextarea';
import {Checkbox} from 'primereact/checkbox';
import {Dropdown} from 'primereact/dropdown';
import {MultiSelect} from 'primereact/multiselect';
import { Button } from 'primereact/button';
import {Dialog} from 'primereact/components/dialog/Dialog';
import {Growl} from 'primereact/components/growl/Growl';
import {ResourceInputList} from './ResourceInputList';
import AppLoader from '../../layout/components/AppLoader';
import CycleService from '../../services/cycle.service';
import ProjectService from '../../services/project.service';
import UnitConverter from '../../utils/unit.converter';
export class ProjectEdit extends Component {
constructor(props) {
super(props);
this.state = {
isLoading: true,
dialog: { header: '', detail: ''},
project: {
trigger_priority: 1000,
priority_rank: null,
quota: [] // Mandatory Field in the back end
},
projectQuota: {}, // Holds the value of resources selected with resource_type_id as key
validFields: {}, // Holds the list of valid fields based on the form rules
validForm: false, // To enable Save Button
errors: {},
periodCategories: [],
projectCategories: [],
resources: [], // Selected resources for the project
resourceList: [], // Available resources to select for the project
cycles: [],
redirect: this.props.match.params.id?"":'/project/list' //If no project name passed redirect to Project list page
}
this.projectQuota = [] // Holds the old list of project_quota saved for the project
// Validation Rules
this.formRules = {
name: {required: true, message: "Name can not be empty"},
description: {required: true, message: "Description can not be empty"},
priority_rank: {required: true, message: "Enter Project Rank"}
};
this.defaultResources = [{name:'LOFAR Observing Time'},
{name:'LOFAR Observing Time prio A'},
{name:'LOFAR Observing Time prio B'},
{name:'LOFAR Processing Time'},
{name:'Allocation storage'},
{name:'Number of triggers'},
{name:'LOFAR Support hours'} ];
this.projectResourceDefaults = {};
this.resourceUnitMap = UnitConverter.resourceUnitMap;
this.getProjectDetails = this.getProjectDetails.bind(this);
this.cycleOptionTemplate = this.cycleOptionTemplate.bind(this);
this.setProjectQuotaDefaults = this.setProjectQuotaDefaults.bind(this);
this.setProjectParams = this.setProjectParams.bind(this);
this.addNewResource = this.addNewResource.bind(this);
this.removeResource = this.removeResource.bind(this);
this.setProjectQuotaParams = this.setProjectQuotaParams.bind(this);
this.saveProject = this.saveProject.bind(this);
this.saveProjectQuota = this.saveProjectQuota.bind(this);
this.cancelEdit = this.cancelEdit.bind(this);
}
componentDidMount() {
ProjectService.getDefaultProjectResources()
.then(defaults => {
this.projectResourceDefaults = defaults;
});
CycleService.getAllCycles()
.then(cycles => {
this.setState({cycles: cycles});
});
ProjectService.getProjectCategories()
.then(categories => {
this.setState({projectCategories: categories});
});
ProjectService.getPeriodCategories()
.then(categories => {
this.setState({periodCategories: categories});
});
ProjectService.getResources()
.then(resourceList => {
this.setState({resourceList: resourceList});
})
.then((resourceList, resources) => {
this.getProjectDetails();
});
}
/**
* Function retrieve project details and resource allocations(project_quota) and assign to appropriate varaibles
*/
async getProjectDetails() {
let project = await ProjectService.getProjectDetails(this.props.match.params.id);
let resourceList = this.state.resourceList;
let projectQuota = {};
if (project) {
// Get project_quota for the project and asssign to the component variable
for (const id of project.quota_ids) {
let quota = await ProjectService.getProjectQuota(id);
let resource = _.find(resourceList, ['name', quota.resource_type_id]);
quota.resource = resource;
this.projectQuota.push(quota);
const conversionFactor = this.resourceUnitMap[resource.quantity_value]?this.resourceUnitMap[resource.quantity_value].conversionFactor:1;
projectQuota[quota.resource_type_id] = quota.value / conversionFactor;
};
// Remove the already assigned resources from the resoureList
const resources = _.remove(resourceList, (resource) => { return _.find(this.projectQuota, {'resource_type_id': resource.name})!=null });
this.setState({project: project, resourceList: resourceList, resources: resources,
projectQuota: projectQuota, isLoading: false});
// Validate form if all values are as per the form rules and enable Save button
this.validateForm();
} else {
this.setState({redirect: '../../not-found'});
}
}
/**
* Cycle option sub-component with cycle object
*/
cycleOptionTemplate(option) {
return (
<div className="p-clearfix">
<span style={{fontSize:'1em',float:'right',margin:'1em .5em 0 0'}}>{option.name}</span>
</div>
);
}
/**
* Function to set project resource allocation
* @param {Array} resources
*/
setProjectQuotaDefaults(resources) {
let projectQuota = this.state.projectQuota;
for (const resource of resources) {
const conversionFactor = this.resourceUnitMap[resource.quantity_value]?this.resourceUnitMap[resource.quantity_value].conversionFactor:1;
projectQuota[resource['name']] = this.projectResourceDefaults[resource.name]/conversionFactor;
}
return projectQuota;
}
/**
* Function to add new resource to project
*/
addNewResource(){
if (this.state.newResource) {
let resourceList = this.state.resourceList;
const newResource = _.remove(resourceList, {'name': this.state.newResource});
let resources = this.state.resources;
resources.push(newResource[0]);
this.setState({resources: resources, resourceList: resourceList, newResource: null});
}
}
/**
* Callback function to be called from ResourceInpulList when a resource is removed from it
* @param {string} name - resource_type_id
*/
removeResource(name) {
let resources = this.state.resources;
let resourceList = this.state.resourceList;
let projectQuota = this.state.projectQuota;
const removedResource = _.remove(resources, (resource) => { return resource.name === name });
resourceList.push(removedResource[0]);
delete projectQuota[name];
this.setState({resourceList: resourceList, resources: resources, projectQuota: projectQuota});
}
/**
* Function to call on change and blur events from input components
* @param {string} key
* @param {any} value
*/
setProjectParams(key, value, type) {
let project = this.state.project;
switch(type) {
case 'NUMBER': {
console.log("Parsing Number");
project[key] = value?parseInt(value):0;
break;
}
default: {
project[key] = value;
break;
}
}
this.setState({project: project, validForm: this.validateForm(key)});
}
/**
* Callback Function to call from ResourceInputList on change and blur events
* @param {string} key
* @param {InputEvent} event
*/
setProjectQuotaParams(key, event) {
let projectQuota = this.state.projectQuota;
if (event.target.value) {
let resource = _.find(this.state.resources, {'name': key});
let newValue = 0;
if (this.resourceUnitMap[resource.quantity_value] &&
event.target.value.toString().indexOf(this.resourceUnitMap[resource.quantity_value].display)>=0) {
newValue = event.target.value.replace(this.resourceUnitMap[resource.quantity_value].display,'');
} else {
newValue = event.target.value;
}
projectQuota[key] = (newValue==="NaN" || isNaN(newValue))?0:newValue;
} else {
let projectQuota = this.state.projectQuota;
projectQuota[key] = 0;
}
this.setState({projectQuota: projectQuota});
}
/**
* Validation function to validate the form or field based on the form rules.
* If no argument passed for fieldName, validates all fields in the form.
* @param {string} fieldName
*/
validateForm(fieldName) {
let validForm = false;
let errors = this.state.errors;
let validFields = this.state.validFields;
if (fieldName) {
delete errors[fieldName];
delete validFields[fieldName];
if (this.formRules[fieldName]) {
const rule = this.formRules[fieldName];
const fieldValue = this.state.project[fieldName];
if (rule.required) {
if (!fieldValue) {
errors[fieldName] = rule.message?rule.message:`${fieldName} is required`;
} else {
validFields[fieldName] = true;
}
}
}
} else {
errors = {};
validFields = {};
for (const fieldName in this.formRules) {
const rule = this.formRules[fieldName];
const fieldValue = this.state.project[fieldName];
if (rule.required) {
if (!fieldValue) {
errors[fieldName] = rule.message?rule.message:`${fieldName} is required`;
} else {
validFields[fieldName] = true;
}
}
}
}
if (Object.keys(validFields).length === Object.keys(this.formRules).length) {
validForm = true;
}
this.setState({errors: errors, validFields: validFields, validForm: validForm});
return validForm;
}
/**
* Function to call when 'Save' button is clicked to update the project.
*/
saveProject() {
if (this.validateForm) {
ProjectService.updateProject(this.props.match.params.id, this.state.project)
.then(async (project) => {
if (project && this.state.project.updated_at !== project.updated_at) {
this.saveProjectQuota(project);
} else {
this.growl.show({severity: 'error', summary: 'Error Occured', detail: 'Unable to update Project'});
this.setState({errors: project});
}
});
}
}
/**
* Function to Create, Update & Delete project_quota for the project
*/
async saveProjectQuota(project) {
let dialog = {};
let quotaError = {};
let updatingProjectQuota = [];
let newProjectQuota = [];
let deletingProjectQuota = [];
for (const resource in this.state.projectQuota) {
const resourceType = _.find(this.state.resources, {'name': resource});
const conversionFactor = this.resourceUnitMap[resourceType.quantity_value]?this.resourceUnitMap[resourceType.quantity_value].conversionFactor:1
let quotaValue = this.state.projectQuota[resource] * conversionFactor;
let existingQuota = _.find(this.projectQuota, {'resource_type_id': resource});
if (!existingQuota) {
let quota = { project: project.url,
resource_type: resourceType['url'],
value: quotaValue };
newProjectQuota.push(quota);
} else if (existingQuota && existingQuota.value !== quotaValue) {
existingQuota.project = project.url;
existingQuota.value = quotaValue;
updatingProjectQuota.push(existingQuota);
}
}
let projectQuota = this.state.projectQuota;
deletingProjectQuota = _.filter(this.projectQuota, function(quota) { return !projectQuota[quota.resource_type_id]});
for (const projectQuota of deletingProjectQuota) {
const deletedProjectQuota = await ProjectService.deleteProjectQuota(projectQuota);
if (!deletedProjectQuota) {
quotaError[projectQuota.resource_type_id] = true;
}
}
for (const projectQuota of updatingProjectQuota) {
const updatedProjectQuota = await ProjectService.updateProjectQuota(projectQuota);
if (!updatedProjectQuota) {
quotaError[projectQuota.resource_type_id] = true;
}
}
for (const projectQuota of newProjectQuota) {
const createdProjectQuota = await ProjectService.saveProjectQuota(projectQuota);
if (!createdProjectQuota) {
quotaError[projectQuota.resource_type_id] = true;
}
}
if (_.keys(quotaError).length === 0) {
dialog = {header: 'Success', detail: 'Project updated successfully.'};
} else {
dialog = {header: 'Error', detail: 'Project updated successfully but resource allocation not updated properly. Try again!'};
}
this.setState({dialogVisible: true, dialog: dialog});
}
/**
* Cancel edit and redirect to Project View page
*/
cancelEdit() {
this.setState({redirect: `/project/view/${this.state.project.name}`});
}
render() {
if (this.state.redirect) {
return <Redirect to={ {pathname: this.state.redirect} }></Redirect>
}
return (
<React.Fragment>
<div className="p-grid">
<Growl ref={(el) => this.growl = el} />
<div className="p-col-10 p-lg-10 p-md-10">
<h2>Project - Edit</h2>
</div>
<div className="p-col-2 p-lg-2 p-md-2">
<Link to={{ pathname: `/project/view/${this.state.project.name}`}} title="Close Edit" style={{float: "right"}}>
<i className="fa fa-window-close" style={{marginTop: "10px"}}></i>
</Link>
</div>
</div>
{ this.state.isLoading ? <AppLoader/> :
<>
<div>
<div className="p-fluid">
<div className="p-field p-grid">
<label htmlFor="projectName" className="col-lg-2 col-md-2 col-sm-12">Name <span style={{color:'red'}}>*</span></label>
<div className="col-lg-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">
<InputNumber inputId="trig_prio" name="trig_prio" className={this.state.errors.name ?'input-error':''}
value={this.state.project.trigger_priority} showButtons
min={0} max={1001} step={10} useGrouping={false}
onChange={(e) => this.setProjectParams('trigger_priority', e.value)}
onBlur={(e) => this.setProjectParams('trigger_priority', e.target.value, 'NUMBER')} />
<label className="error">
{this.state.errors.trigger_priority ? this.state.errors.trigger_priority : ""}
</label>
</div>
<label htmlFor="trigger" className="col-lg-2 col-md-2 col-sm-12">Allows Trigger Submission</label>
<div className="col-lg-4 col-md-4 col-sm-12">
<Checkbox inputId="trigger" role="trigger" checked={this.state.project.can_trigger} onChange={e => this.setProjectParams('can_trigger', e.target.checked)}></Checkbox>
</div>
</div>
<div className="p-field p-grid">
<label htmlFor="projCategory" className="col-lg-2 col-md-2 col-sm-12">Project Category </label>
<div className="col-lg-4 col-md-4 col-sm-12">
<Dropdown inputId="projCat" optionLabel="value" optionValue="url"
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="value" optionValue="url"
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">
<InputNumber inputId="proj-rank" name="rank" data-testid="rank" value={this.state.project.priority_rank}
mode="decimal" showButtons min={0} max={100}
onChange={(e) => this.setProjectParams('priority_rank', e.value)}
onBlur={(e) => this.setProjectParams('priority_rank', e.target.value, 'NUMBER')} />
<label className="error">
{this.state.errors.priority_rank ? this.state.errors.priority_rank : ""}
</label>
</div>
</div>
{this.state.resourceList &&
<div className="p-fluid">
<div className="p-field p-grid">
<div className="col-lg-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={_.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} data-testid="add_res_btn" />
</div>
</div>
{_.keys(this.state.projectQuota).length>0 &&
<div className="p-field p-grid resource-input-grid">
<ResourceInputList list={this.state.resources} unitMap={this.resourceUnitMap}
projectQuota={this.state.projectQuota} callback={this.setProjectQuotaParams}
removeInputCallback={this.removeResource} />
</div>
}
</div>
}
</div>
</div>
<div className="p-grid p-justify-start">
<div className="p-col-1">
<Button label="Save" className="p-button-primary" id="save-btn" data-testid="save-btn" icon="pi pi-check" onClick={this.saveProject} disabled={!this.state.validForm} />
</div>
<div className="p-col-1">
<Button label="Cancel" className="p-button-danger" icon="pi pi-times" onClick={this.cancelEdit} />
</div>
</div>
</>
}
{/* Dialog component to show messages and get input */}
<div className="p-grid" data-testid="confirm_dialog">
<Dialog header={this.state.dialog.header} visible={this.state.dialogVisible} style={{width: '30vw'}} inputId="confirm_dialog"
modal={true} onHide={() => {this.setState({dialogVisible: false})}}
footer={<div>
<Button key="back" onClick={() => {this.setState({dialogVisible: false}); this.cancelEdit();}} label="Ok" />
{/* <Button key="submit" type="primary" onClick={this.reset} label="Yes" /> */}
</div>
} >
<div className="p-grid">
<div className="col-lg-2 col-md-2 col-sm-2">
<i className="pi pi-check-circle pi-large pi-success"></i>
</div>
<div className="col-lg-10 col-md-10 col-sm-10">
<span style={{marginTop:"5px"}}>{this.state.dialog.detail}</span>
</div>
</div>
</Dialog>
</div>
</React.Fragment>
);
}
}