Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
create.js 26.85 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';

/**
 * Component to create a new Project
 */
export class ProjectCreate extends Component {
    constructor(props) {
        super(props);
        this.state = {
            isLoading: true,
            dialog: { header: '', detail: ''},      
            project: {
                trigger_priority: 1000,
                priority_rank: null,
                quota: [],                          // Mandatory Field in the back end, so an empty array is passed
                can_trigger: false
            },
            projectQuota: {},                       // Resource Allocations
            validFields: {},                        // For Validation
            validForm: false,                       // To enable Save Button
            errors: {},                             // Validation Errors
            periodCategories: [],
            projectCategories: [],
            resources: [],                          // Selected Resources for Allocation
            resourceList: [],                       // Available Resources for Allocation
            cycles: []
        }
        // Validateion Rules
        this.formRules = {
            name: {required: true, message: "Name can not be empty"},
            description: {required: true, message: "Description can not be empty"},
            priority_rank: {required: true, message: "Enter Project Rank"}
        };
        this.defaultResourcesEnabled = true;        // This property and functionality to be concluded based on PO input
        this.defaultResources = [{name:'LOFAR Observing Time'}, 
                                    {name:'LOFAR Observing Time prio A'}, 
                                    {name:'LOFAR Observing Time prio B'},
                                    {name:'CEP Processing Time'},
                                    {name:'LTA Storage'},
                                    {name:'Number of triggers'},
                                    {name:'LOFAR Support Time'} ];
        this.projectResourceDefaults = {};          // Default values for default resources
        this.resourceUnitMap = UnitConverter.resourceUnitMap;       // Resource unit conversion factor and constraints
        this.cycleOptionTemplate = this.cycleOptionTemplate.bind(this);         // Template for cycle multiselect
        this.tooltipOptions = {position: 'left', event: 'hover', className:"p-tooltip-custom"};

        this.setProjectQuotaDefaults = this.setProjectQuotaDefaults.bind(this);
        this.setProjectParams = this.setProjectParams.bind(this);
        this.addNewResource = this.addNewResource.bind(this);
        this.removeResource = this.removeResource.bind(this);
        this.setProjectQuotaParams = this.setProjectQuotaParams.bind(this);
        this.saveProject = this.saveProject.bind(this);
        this.cancelCreate = this.cancelCreate.bind(this);
        this.reset = this.reset.bind(this);
    }

    componentDidMount() {
        ProjectService.getDefaultProjectResources()
            .then(defaults => {
                this.projectResourceDefaults = defaults;
            });
        CycleService.getAllCycles()
            .then(cycles => {
                this.setState({cycles: cycles});
            });
        ProjectService.getProjectCategories()
            .then(categories => {
                this.setState({projectCategories: categories});
            });
        ProjectService.getPeriodCategories()
            .then(categories => {
                this.setState({periodCategories: categories});
            });
        ProjectService.getResources()
            .then(resourceList => {
                const defaultResources = this.defaultResources;
                resourceList = _.sortBy(resourceList, "name");
                const resources = _.remove(resourceList, function(resource) { return _.find(defaultResources, {'name': resource.name})!=null });
                const projectQuota = this.setProjectQuotaDefaults(resources);
                this.setState({resourceList: resourceList, resources: resources, projectQuota: projectQuota, isLoading: false});
            });
        // ProjectService.getProjects().then(projects => {
        //     console.log(projects);
        //   });
    }

    /**
     * Cycle option sub-component with cycle object
     */
    cycleOptionTemplate(option) {
        return (
            <div className="p-clearfix">
                <span style={{fontSize:'1em',float:'right',margin:'1em .5em 0 0'}}>{option.name}</span>
            </div>
        );
    }

    /**
     * Function to set project resource allocation
     * @param {Array} resources 
     */
    setProjectQuotaDefaults(resources) {
        let projectQuota = this.state.projectQuota;
        for (const resource of resources) {
            const conversionFactor = this.resourceUnitMap[resource.quantity_value]?this.resourceUnitMap[resource.quantity_value].conversionFactor:1;
            projectQuota[resource['name']] = this.projectResourceDefaults[resource.name]/conversionFactor;
        }
        return projectQuota;
    }

    /**
     * Function to add new resource to project
     */
    addNewResource(){
        if (this.state.newResource) {
            let resourceList = this.state.resourceList;
            const newResource = _.remove(resourceList, {'name': this.state.newResource});
            let resources = this.state.resources;
            resources.push(newResource[0]);
            this.setState({resources: resources, resourceList: resourceList, newResource: null});
        }
    }

        /**
     * Callback function to be called from ResourceInpulList when a resource is removed from it
     * @param {string} name - resource_type_id
     */
    removeResource(name) {
        let resources = this.state.resources;
        let resourceList = this.state.resourceList;
        let projectQuota = this.state.projectQuota;
        const removedResource = _.remove(resources, (resource) => { return resource.name === name });
        resourceList.push(removedResource[0]);
        resourceList = _.sortBy(resourceList, 'name');
        delete projectQuota[name];
        this.setState({resourceList: resourceList, resources: resources, projectQuota: projectQuota});
    }

    /**
     * Function to call on change and blur events from input components
     * @param {string} key 
     * @param {any} value 
     */
    setProjectParams(key, value, type) {
        let project = this.state.project;
        switch(type) {
            case 'NUMBER': {
                console.log("Parsing Number");
                project[key] = value?parseInt(value):0;
                break;
            }
            default: {
                project[key] = value;
                break;
            }
        }
        this.setState({project: project, validForm: this.validateForm(key)});
    }

    /**
     * Callback Function to call from ResourceInputList on change and blur events
     * @param {string} key 
     * @param {InputEvent} event 
     */
    setProjectQuotaParams(key, event) {
        let projectQuota = this.state.projectQuota;
        if (event.target.value) {
            let resource = _.find(this.state.resources, {'name': key});
            
            let newValue = 0;
            if (this.resourceUnitMap[resource.quantity_value] && 
                event.target.value.toString().indexOf(this.resourceUnitMap[resource.quantity_value].display)>=0) {
                newValue = event.target.value.replace(this.resourceUnitMap[resource.quantity_value].display,'');
            }   else {
                newValue = event.target.value;
            }
            projectQuota[key] = (newValue==="NaN" || isNaN(newValue))?0:newValue;
        }   else {
            let projectQuota = this.state.projectQuota;
            projectQuota[key] = 0;
        }
        this.setState({projectQuota: projectQuota});
    }

    /**
     * Validation function to validate the form or field based on the form rules.
     * If no argument passed for fieldName, validates all fields in the form.
     * @param {string} fieldName 
     */
    validateForm(fieldName) {
        let validForm = false;
        let errors = this.state.errors;
        let validFields = this.state.validFields;
        if (fieldName) {
            delete errors[fieldName];
            delete validFields[fieldName];
            if (this.formRules[fieldName]) {
                const rule = this.formRules[fieldName];
                const fieldValue = this.state.project[fieldName];
                if (rule.required) {
                    if (!fieldValue) {
                        errors[fieldName] = rule.message?rule.message:`${fieldName} is required`;
                    }   else {
                        validFields[fieldName] = true;
                    }
                }
            }
        }   else {
            errors = {};
            validFields = {};
            for (const fieldName in this.formRules) {
                const rule = this.formRules[fieldName];
                const fieldValue = this.state.project[fieldName];
                if (rule.required) {
                    if (!fieldValue) {
                        errors[fieldName] = rule.message?rule.message:`${fieldName} is required`;
                    }   else {
                        validFields[fieldName] = true;
                    }
                }
            }
        }
        this.setState({errors: errors, validFields: validFields});
        if (Object.keys(validFields).length === Object.keys(this.formRules).length) {
            validForm = true;
        }
        return validForm;
    }
    
    /**
     * Function to call when 'Save' button is clicked to save the project.
     */
    saveProject() {
        if (this.validateForm) {
            let projectQuota = [];
            for (const resource in this.state.projectQuota) {
                let resourceType = _.find(this.state.resources, {'name': resource});
                let quota = { project: this.state.project.name,
                                resource_type: resourceType['url'],
                                value: this.state.projectQuota[resource] * (this.resourceUnitMap[resourceType.quantity_value]?this.resourceUnitMap[resourceType.quantity_value].conversionFactor:1)};
                projectQuota.push(quota);
            }
            ProjectService.saveProject(this.state.project, this.defaultResourcesEnabled?projectQuota:[])
                .then(project => {
                    if (project.url) {
                        let dialog = {};
                        if (this.defaultResourcesEnabled) {
                            dialog = {header: 'Success', detail: 'Project saved successfully. Do you want to create another project?'};
                        }   else {
                            dialog = {header: 'Success', detail: 'Project saved successfully with default Resource allocations. Do you want to view and edit them?'};
                        }
                        this.setState({project:project, dialogVisible: true, dialog: dialog})
                    }   else {
                        this.growl.show({severity: 'error', summary: 'Error Occured', detail: 'Unable to save Project'});
                        this.setState({errors: project});
                    }
                });
        }
    }

    /**
     * Function to cancel form creation and navigate to other page/component
     */
    cancelCreate() {
        this.setState({redirect: '/project'});
    }

    /**
     * Reset function to be called to reset the form fields
     */
    reset() {
        if (this.defaultResourcesEnabled) {
            let prevResources = this.state.resources;
            let resourceList = [];
            let resources = [];
            if (resources) {
                // const nonDefaultResources = _.remove(resources, function(resource) { return _.find(defaultResources, {'name': resource.name})==null });
                // resourceList = nonDefaultResources.concat(this.state.resourceList);
                const defaultResources = this.defaultResources;
                resourceList = _.sortBy(prevResources.concat(this.state.resourceList), "name");
                resources = _.remove(resourceList, function(resource) { return _.find(defaultResources, {'name': resource.name})!=null });
                // const projectQuota = this.setProjectQuotaDefaults(resources);
                // this.setState({resourceList: resourceList, resources: resources, projectQuota: projectQuota});
            }
            const projectQuota = this.setProjectQuotaDefaults(resources);
            this.setState({
                dialog: { header: '', detail: ''},
                project: {
                    name: '',
                    description: '',
                    trigger_priority: 1000,
                    priority_rank: null,
                    project_quota: []
                },
                projectQuota: projectQuota,
                validFields: {},
                validForm: false,
                errors: {},
                dialogVisible: false,
                resources: resources,
                resourceList: resourceList
            });
        }   else {
            this.setState({redirect: `/project/edit/${this.state.project.name}`})
        }
    }

    render() {
        if (this.state.redirect) {
            return <Redirect to={ {pathname: this.state.redirect} }></Redirect>
        }
        
        return (
            <React.Fragment>
                <div className="p-grid">
                    <Growl ref={(el) => this.growl = el} />
                
                    <div className="p-col-10 p-lg-10 p-md-10">
                        <h2>Project - Add</h2>
                    </div>
                    <div className="p-col-2 p-lg-2 p-md-2">
                        <Link to={{ pathname: '/project'}} tite="Close Edit" style={{float: "right"}}>
                            <i className="fa fa-window-close" style={{marginTop: "10px"}}></i>
                        </Link>
                    </div>
                </div>
                { this.state.isLoading ? <AppLoader /> :
                <>
                <div>
                    <div className="p-fluid">
                        <div className="p-field p-grid" style={{display: 'none'}}>
                            <label htmlFor="projectId" className="col-lg-2 col-md-2 col-sm-12">URL </label>
                            <div className="col-lg-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" 
                                            tooltip="Enter name of the project" tooltipOptions={this.tooltipOptions} maxLength="128"
                                            value={this.state.project.name} 
                                            onChange={(e) => this.setProjectParams('name', e.target.value)}
                                            onBlur={(e) => this.setProjectParams('name', e.target.value)}/>
                                <label className={this.state.errors.name?"error":"info"}>
                                    {this.state.errors.name ? this.state.errors.name : "Max 128 characters"}
                                </label>
                            </div>
                            <label htmlFor="description" className="col-lg-2 col-md-2 col-sm-12">Description <span style={{color:'red'}}>*</span></label>
                            <div className="col-lg-4 col-md-4 col-sm-12">
                                <InputTextarea className={this.state.errors.description ?'input-error':''} rows={3} cols={30} 
                                            tooltip="Short description of the project" tooltipOptions={this.tooltipOptions} maxLength="128"
                                            data-testid="description" value={this.state.project.description} 
                                            onChange={(e) => this.setProjectParams('description', e.target.value)}
                                            onBlur={(e) => this.setProjectParams('description', e.target.value)}/>
                                <label className={this.state.errors.description ?"error":"info"}>
                                    {this.state.errors.description ? this.state.errors.description : "Max 255 characters"}
                                </label>
                            </div>
                        </div>
                        <div className="p-field p-grid">
                            <label htmlFor="triggerPriority" className="col-lg-2 col-md-2 col-sm-12">Trigger Priority </label>
                            <div className="col-lg-4 col-md-4 col-sm-12" data-testid="trig_prio">
                                <InputNumber inputId="trig_prio" name="trig_prio" value={this.state.project.trigger_priority} 
                                        tooltip="Priority of this project w.r.t. triggers" tooltipOptions={this.tooltipOptions}
                                        mode="decimal" showButtons min={0} max={1001} step={10} useGrouping={false}
                                        onChange={(e) => this.setProjectParams('trigger_priority', e.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" data-testid="trigger">
                                <Checkbox inputId="trigger" role="trigger" 
                                        tooltip="Is this project allowed to supply observation requests on the fly, possibly interrupting currently running observations (responsive telescope)?" 
                                        tooltipOptions={this.tooltipOptions}
                                        checked={this.state.project.can_trigger} onChange={e => this.setProjectParams('can_trigger', e.target.checked)}></Checkbox>
                            </div>
                        </div>
                        <div className="p-field p-grid">
                            <label htmlFor="projCat" className="col-lg-2 col-md-2 col-sm-12">Project Category </label>
                            <div className="col-lg-4 col-md-4 col-sm-12" data-testid="projCat" >
                                <Dropdown inputId="projCat" optionLabel="value" optionValue="url" 
                                        tooltip="Project Category" tooltipOptions={this.tooltipOptions}
                                        value={this.state.project.project_category} 
                                        options={this.state.projectCategories} 
                                        onChange={(e) => {this.setProjectParams('project_category', e.value)}} 
                                        placeholder="Select Project Category" />
                            </div>
                            <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" 
                                        tooltip="Period Category" tooltipOptions={this.tooltipOptions}
                                        value={this.state.project.period_category} 
                                        options={this.state.periodCategories} 
                                        onChange={(e) => {this.setProjectParams('period_category',e.value)}} 
                                        placeholder="Select Period Category" />
                            </div>
                        </div>
                        <div className="p-field p-grid">
                            <label htmlFor="triggerPriority" className="col-lg-2 col-md-2 col-sm-12">Cycle(s)</label>
                            <div className="col-lg-4 col-md-4 col-sm-12">
                                <MultiSelect data-testid="cycle" id="cycle" optionLabel="name" optionValue="url" filter={true}
                                        tooltip="Cycle(s) to which this project belongs" tooltipOptions={this.tooltipOptions}
                                        value={this.state.project.cycles} 
                                        options={this.state.cycles} 
                                        onChange={(e) => {this.setProjectParams('cycles',e.value)}} 
                                        
                                />
                            </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} 
                                        tooltip="Priority of this project w.r.t. other projects. Projects can interrupt observations of lower-priority projects." 
                                        tooltipOptions={this.tooltipOptions}
                                        mode="decimal" showButtons min={0} max={100}
                                        onChange={(e) => this.setProjectParams('priority_rank', e.value)}
                                        onBlur={(e) => this.setProjectParams('priority_rank', e.target.value, 'NUMBER')} />
                                <label className="error">
                                    {this.state.errors.priority_rank ? this.state.errors.priority_rank : ""}
                                </label>
                            </div>
                        </div>
                        
                        {this.defaultResourcesEnabled && this.state.resourceList &&
                            <div className="p-fluid">
                                <div className="p-field p-grid">
                                    <div className="col-lg-3 col-md-3 col-sm-112">
                                        <h5 data-testid="resource_alloc">Resource Allocations</h5>
                                    </div>
                                    <div className="col-lg-3 col-md-3 col-sm-10">
                                        <Dropdown optionLabel="name" optionValue="name" 
                                            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} 
                                                      projectQuota={this.state.projectQuota} callback={this.setProjectQuotaParams} 
                                                      removeInputCallback={this.removeResource} />
                                </div>
                            </div>
                        }
                    </div>
                </div>
                <div className="p-grid p-justify-start">
                    <div className="col-lg-1 col-md-2 col-sm-6">
                        <Button label="Save" className="p-button-primary" id="save-btn" data-testid="save-btn" icon="pi pi-check" onClick={this.saveProject} disabled={!this.state.validForm} />
                    </div>
                    <div className="col-lg-1 col-md-2 col-sm-6">
                        <Button label="Cancel" className="p-button-danger" icon="pi pi-times" onClick={this.cancelCreate}  />
                    </div>
                </div>
                </>
                }

                {/* Dialog component to show messages and get input */}
                <div className="p-grid" data-testid="confirm_dialog">
                    <Dialog header={this.state.dialog.header} visible={this.state.dialogVisible} style={{width: '25vw'}} inputId="confirm_dialog"
                            modal={true}  onHide={() => {this.setState({dialogVisible: false})}} 
                            footer={<div>
                                <Button key="back" onClick={() => {this.setState({dialogVisible: false}); this.cancelCreate();}} label="No" />
                                <Button key="submit" type="primary" onClick={this.reset} label="Yes" />
                                </div>
                            } >
                            <div className="p-grid">
                                <div className="col-lg-2 col-md-2 col-sm-2">
                                    <i className="pi pi-check-circle pi-large pi-success"></i>
                                </div>
                                <div className="col-lg-10 col-md-10 col-sm-10">
                                    {this.state.dialog.detail}
                                </div>
                            </div>
                    </Dialog>
                </div>
                
            </React.Fragment>
        );
    }
}