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