diff --git a/SAS/TMSS/frontend/tmss_webapp/src/components/JSONEditor/JEditor.js b/SAS/TMSS/frontend/tmss_webapp/src/components/JSONEditor/JEditor.js index 7cc46ca9851a7529b85dd822b454b4164aa53c20..4430720ff460fe9010dd7fe31d9e1ff7bb4b47f5 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/components/JSONEditor/JEditor.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/components/JSONEditor/JEditor.js @@ -50,6 +50,11 @@ function Jeditor(props) { let defKey = refUrl.substring(refUrl.lastIndexOf("/")+1); schema.definitions[defKey] = (await $RefParser.resolve(refUrl)).get(newRef); property["$ref"] = newRef; + if(schema.definitions[defKey].type && schema.definitions[defKey].type === 'array'){ + let resolvedItems = await resolveSchema(schema.definitions[defKey].items); + schema.definitions = {...schema.definitions, ...resolvedItems.definitions}; + delete resolvedItems['definitions']; + } } else if(property["type"] === "array") { // reference in array items definition let resolvedItems = await resolveSchema(property["items"]); schema.definitions = {...schema.definitions, ...resolvedItems.definitions}; diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/index.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/index.js index b48cd64f554fa3072685bbab8b48e0f18e61a4c1..afe366d4cda90e90096cd06aed7ab66760d3702a 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/index.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/index.js @@ -1,4 +1,5 @@ import {TimelineView} from './view'; import {WeekTimelineView} from './week.view'; +import { ReservationCreate } from './reservation.create'; -export {TimelineView, WeekTimelineView} ; +export {TimelineView, WeekTimelineView, ReservationCreate} ; diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/reservation.create.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/reservation.create.js new file mode 100644 index 0000000000000000000000000000000000000000..4123a6b92a3e2a2a39f314598c287ab385bbd778 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/reservation.create.js @@ -0,0 +1,413 @@ +import React, {Component} from 'react'; +import { Redirect } from 'react-router-dom'; +import _ from 'lodash'; + +import {Growl} from 'primereact/components/growl/Growl'; +import AppLoader from '../../layout/components/AppLoader'; +import PageHeader from '../../layout/components/PageHeader'; +import UIConstants from '../../utils/ui.constants'; +import {Calendar} from 'primereact/calendar'; +import { InputMask } from 'primereact/inputmask'; +import {Dropdown} from 'primereact/dropdown'; +import {InputText} from 'primereact/inputtext'; +import {InputTextarea} from 'primereact/inputtextarea'; +import { Button } from 'primereact/button'; +import {Dialog} from 'primereact/components/dialog/Dialog'; +import ProjectService from '../../services/project.service'; +import ReservationService from '../../services/reservation.service'; +import UnitService from '../../utils/unit.converter'; +import Jeditor from '../../components/JSONEditor/JEditor'; + +/** + * Component to create a new Reservation + */ +export class ReservationCreate extends Component { + constructor(props) { + super(props); + this.state= { + isLoading: true, + redirect: null, + paramsSchema: null, // JSON Schema to be generated from strategy template to pass to JSON editor + dialog: { header: '', detail: ''}, // Dialog properties + errors: { + name: '', + }, + touched: { + name: '', + }, + reservation: { + name: '', + description: '', + start_time: '', + duration: '', + project: (props.match?props.match.params.project:null) || null, + }, + errors: {}, // Validation Errors + validFields: {}, // For Validation + validForm: false, // To enable Save Button + validEditor: false, + durationError: false, + }; + this.projects = []; // All projects to load project dropdown + this.stations = []; + this.reasons = []; + this.reservations = []; + + // Validateion Rules + this.formRules = { + name: {required: true, message: "Name can not be empty"}, + description: {required: true, message: "Description can not be empty"}, + project: {required: true, message: "Project can not be empty"}, + start_time: {required: true, message: "From Date can not be empty"}, + }; + this.tooltipOptions = UIConstants.tooltipOptions; + this.setEditorOutput = this.setEditorOutput.bind(this); + this.saveReservation = this.saveReservation.bind(this); + this.reset = this.reset.bind(this); + this.cancelCreate = this.cancelCreate.bind(this); + } + + async componentDidMount(){ + const promises = [ ProjectService.getProjectList(), + ReservationService.getReservation(), + ] + await Promise.all(promises).then(responses => { + this.projects = responses[0]; + this.reservations = responses[1]; + }); + let reservationTemplate = this.reservations.find(reason => reason.name === 'resource reservation'); + let schema = { + properties: {} + }; + if(reservationTemplate) { + schema = reservationTemplate.schema; + } + + this.setState({ + paramsSchema: schema, + isLoading: false, + reservationTemplate: reservationTemplate, + }); + } + + /** + * Function to set form values to the Reservation object + * @param {string} key + * @param {object} value + */ + setReservationParams(key, value) { + this.setState({ + touched: { + ...this.state.touched, + [key]: true + } + }); + let reservation = this.state.reservation; + reservation[key] = value; + this.setState({reservation: reservation, validForm: this.validateForm(key), validEditor: this.validateEditor()}); + this.validateEditor(); + } + + /** + * This function is mainly added for Unit Tests. If this function is removed Unit Tests will fail. + */ + validateEditor() { + return this.validEditor && !this.state.durationError? true : false; + } + + /** + * Function to call on change and blur events from input components + * @param {string} key + * @param {any} value + */ + setParams(key, value, type) { + if(key === 'duration' && !this.validateDuration( value)) { + this.setState({ + durationError: true + }) + return; + } + let reservation = this.state.reservation; + switch(type) { + case 'NUMBER': { + reservation[key] = value?parseInt(value):0; + break; + } + default: { + reservation[key] = value; + break; + } + } + this.setState({reservation: reservation, validForm: this.validateForm(key), durationError: false}); + } + + validateDuration(duration) { + const splitOutput = duration.split(':'); + if (splitOutput.length < 3) { + return false; + } else { + if (parseInt(splitOutput[1])>59 || parseInt(splitOutput[2])>59) { + return false; + } + const timeValue = parseInt(splitOutput[1]*60) + parseInt(splitOutput[2]); + if (timeValue !== 'NaN' && timeValue > 3600) { + return false; + } + } + return true; + } + + /** + * 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.reservation[fieldName]; + if (rule.required) { + if (!fieldValue) { + errors[fieldName] = rule.message?rule.message:`${fieldName} is required`; + } else { + validFields[fieldName] = true; + } + } + } else { + + } + } else { + errors = {}; + validFields = {}; + for (const fieldName in this.formRules) { + const rule = this.formRules[fieldName]; + const fieldValue = this.state.reservation[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; + } + + setEditorOutput(jsonOutput, errors) { + this.paramsOutput = jsonOutput; + this.validEditor = errors.length === 0; + this.setState({ paramsOutput: jsonOutput, + validEditor: errors.length === 0, + validForm: this.validateForm()}); + } + + saveReservation(){ + let reservation = this.state.reservation; + let project = this.projects.find(project => project.name === reservation.project); + reservation['duration'] = ( reservation['duration'] === ''? 0: UnitService.getHHmmssToSecs(reservation['duration'])); + reservation['project']= project.url; + reservation['specifications_template']= this.reservations[0].url; + reservation['specifications_doc']= this.paramsOutput; + reservation = ReservationService.saveReservation(reservation); + if (reservation && reservation !== null){ + const dialog = {header: 'Success', detail: 'Reservation is created successfully. Do you want to create another Reservation?'}; + this.setState({ dialogVisible: true, dialog: dialog}) + } else { + this.growl.show({severity: 'error', summary: 'Error Occured', detail: 'Unable to save Reservation'}); + } + } + + /** + * Reset function to be called when user wants to create new Reservation + */ + reset() { + this.setState({ + dialogVisible: false, + dialog: { header: '', detail: ''}, + errors: [], + reservation: { + name: '', + description: '', + start_time: '', + duration: '', + project: '', + }, + paramsSchema: this.state.paramsSchema, + paramsOutput: null, + validEditor: false, + validFields: {}, + touched:false, + stationGroup: [] + }); + } + + /** + * Cancel Reservation creation and redirect + */ + cancelCreate() { + this.props.history.goBack(); + } + + render() { + if (this.state.redirect) { + return <Redirect to={ {pathname: this.state.redirect} }></Redirect> + } + const schema = this.state.paramsSchema; + + let jeditor = null; + if (schema) { + jeditor = React.createElement(Jeditor, {title: "Reservation Parameters", + schema: schema, + initValue: this.state.paramsOutput, + callback: this.setEditorOutput, + parentFunction: this.setEditorFunction + }); + } + return ( + <React.Fragment> + <Growl ref={(el) => this.growl = el} /> + <PageHeader location={this.props.location} title={'Reservation - Add'} + actions={[{icon: 'fa-window-close',link: this.props.history.goBack,title:'Click to close Reservation creation', props : { pathname: `/su/timelineview`}}]}/> + { this.state.isLoading ? <AppLoader /> : + <> + <div> + <div className="p-fluid"> + <div className="p-field p-grid"> + <label htmlFor="reservationname" className="col-lg-2 col-md-2 col-sm-12">Name <span style={{color:'red'}}>*</span></label> + <div className="col-lg-3 col-md-3 col-sm-12"> + <InputText className={(this.state.errors.name && this.state.touched.name) ?'input-error':''} id="reservationname" data-testid="name" + tooltip="Enter name of the Reservation Name" tooltipOptions={this.tooltipOptions} maxLength="128" + ref={input => {this.nameInput = input;}} + value={this.state.reservation.name} autoFocus + onChange={(e) => this.setReservationParams('name', e.target.value)} + onBlur={(e) => this.setReservationParams('name', e.target.value)}/> + <label className={(this.state.errors.name && this.state.touched.name)?"error":"info"}> + {this.state.errors.name && this.state.touched.name ? this.state.errors.name : "Max 128 characters"} + </label> + </div> + <div className="col-lg-1 col-md-1 col-sm-12"></div> + <label htmlFor="description" className="col-lg-2 col-md-2 col-sm-12">Description <span style={{color:'red'}}>*</span></label> + <div className="col-lg-3 col-md-3 col-sm-12"> + <InputTextarea className={(this.state.errors.description && this.state.touched.description) ?'input-error':''} rows={3} cols={30} + tooltip="Longer description of the Reservation" + tooltipOptions={this.tooltipOptions} + maxLength="128" + data-testid="description" + value={this.state.reservation.description} + onChange={(e) => this.setReservationParams('description', e.target.value)} + onBlur={(e) => this.setReservationParams('description', e.target.value)}/> + <label className={(this.state.errors.description && this.state.touched.description) ?"error":"info"}> + {(this.state.errors.description && this.state.touched.description) ? this.state.errors.description : "Max 255 characters"} + </label> + </div> + </div> + <div className="p-field p-grid"> + <label htmlFor="reservationName" className="col-lg-2 col-md-2 col-sm-12">From Date <span style={{color:'red'}}>*</span></label> + <div className="col-lg-3 col-md-3 col-sm-12"> + <Calendar + d dateFormat="dd-M-yy" + value= {this.state.reservation.start_time} + onChange= {e => this.setParams('start_time',e.value)} + onBlur= {e => this.setParams('start_time',e.value)} + data-testid="start_time" + tooltip="Moment at which the reservation starts from, that is, when its reservation can run." tooltipOptions={this.tooltipOptions} + showIcon={true} + showTime= {true} + showSeconds= {true} + hourFormat= "24" + /> + + <label className={this.state.errors.from?"error":"info"}> + {this.state.errors.start_time ? this.state.errors.start_time : ""} + </label> + </div> + <div className="col-lg-1 col-md-1 col-sm-12"></div> + + <label htmlFor="duration" className="col-lg-2 col-md-2 col-sm-12">Duration </label> + <div className="col-lg-3 col-md-3 col-sm-12" data-testid="duration" > + <InputMask + value={this.state.reservation.duration} + mask="99:99:99" + placeholder="HH:mm:ss" + className="inputmask" + onChange= {e => this.setParams('duration',e.value)} + ref={input =>{this.input = input}} + /> + <label className="error"> + {this.state.durationError ? 'Invalid duration' : ""} + </label> + </div> + </div> + + <div className="p-field p-grid"> + <label htmlFor="project" className="col-lg-2 col-md-2 col-sm-12">Project <span style={{color:'red'}}>*</span></label> + <div className="col-lg-3 col-md-3 col-sm-12" data-testid="project" > + <Dropdown inputId="project" optionLabel="name" optionValue="name" + tooltip="Project" tooltipOptions={this.tooltipOptions} + value={this.state.reservation.project} + options={this.projects} + onChange={(e) => {this.setParams('project',e.value)}} + placeholder="Select Project" /> + <label className={(this.state.errors.project && this.state.touched.project) ?"error":"info"}> + {(this.state.errors.project && this.state.touched.project) ? this.state.errors.project : "Select Project"} + </label> + </div> + </div> + + <div className="p-grid"> + <div className="p-col-12"> + {this.state.paramsSchema?jeditor:""} + </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.saveReservation} + disabled={!this.state.validEditor || !this.state.validForm} data-testid="save-btn" /> + </div> + <div className="p-col-1"> + <Button label="Cancel" className="p-button-danger" icon="pi pi-times" onClick={this.cancelCreate} /> + </div> + </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, redirect: `/su/timelineview`});}} 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" style={{margin: 'auto'}}> + <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> + ); + } +} + +export default ReservationCreate; \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/view.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/view.js index 9a0b6cc381d3dde261eaeca7a05272e03b962bac..07e33c635c206e565e3efab7ea89530f95905369 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/view.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/view.js @@ -326,7 +326,10 @@ export class TimelineView extends Component { return ( <React.Fragment> <PageHeader location={this.props.location} title={'Scheduling Units - Timeline View'} - actions={[{icon: 'fa-calendar-alt',title:'Week View', props : { pathname: `/su/timelineview/week`}}]}/> + actions={[ + {icon: 'fa-plus-square',title:'Add Reservation', props : { pathname: `/su/timelineview/reservation/create`}}, + {icon: 'fa-calendar-alt',title:'Week View', props : { pathname: `/su/timelineview/week`}}, + ]}/> { this.state.isLoading ? <AppLoader /> : <div className="p-grid"> {/* SU List Panel */} diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/week.view.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/week.view.js index 67e9ef5c7e7439c1fc5c4399014a7bc4ac9126f4..1292f3343d4e38b9360132e54104f76e2a86e5dd 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/week.view.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/week.view.js @@ -312,7 +312,9 @@ export class WeekTimelineView extends Component { return ( <React.Fragment> <PageHeader location={this.props.location} title={'Scheduling Units - Week View'} - actions={[{icon: 'fa-clock',title:'View Timeline', props : { pathname: `/su/timelineview`}}]}/> + actions={[ + {icon: 'fa-plus-square',title:'Add Reservation', props : { pathname: `/su/timelineview/reservation/create`}}, + {icon: 'fa-clock',title:'View Timeline', props : { pathname: `/su/timelineview`}}]}/> { this.state.isLoading ? <AppLoader /> : <> {/* <div className="p-field p-grid"> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/index.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/index.js index 156267acb0fad8c56c76a1c548a491683aacf184..49c828fa71156e29ab777bc0ac0a01b3e37770db 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/index.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/index.js @@ -14,7 +14,7 @@ import ViewSchedulingUnit from './Scheduling/ViewSchedulingUnit' import SchedulingUnitCreate from './Scheduling/create'; import EditSchedulingUnit from './Scheduling/edit'; import { CycleList, CycleCreate, CycleView, CycleEdit } from './Cycle'; -import {TimelineView, WeekTimelineView} from './Timeline'; +import {TimelineView, WeekTimelineView, ReservationCreate} from './Timeline'; import SchedulingSetCreate from './Scheduling/create.scheduleset'; import Workflow from './Workflow'; @@ -159,7 +159,12 @@ export const routes = [ name: 'Workflow', title: 'QA Reporting (TO)' }, - + { + path: "/su/timelineview/reservation/create", + component: ReservationCreate, + name: 'Reservation Add', + title: 'Reservation - Add' + } ]; export const RoutedContent = () => { diff --git a/SAS/TMSS/frontend/tmss_webapp/src/services/reservation.service.js b/SAS/TMSS/frontend/tmss_webapp/src/services/reservation.service.js new file mode 100644 index 0000000000000000000000000000000000000000..94ec9da9d9bbf410ee615fa7be56e925e3512058 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/services/reservation.service.js @@ -0,0 +1,27 @@ +const axios = require('axios'); + +//axios.defaults.baseURL = 'http://192.168.99.100:8008/api'; +axios.defaults.headers.common['Authorization'] = 'Basic dGVzdDp0ZXN0'; + +const ReservationService = { + getReservation: async function () { + try { + const url = `/api/reservation_template`; + const response = await axios.get(url); + return response.data.results; + } catch (error) { + console.error(error); + } + }, + saveReservation: async function (reservation) { + try { + const response = await axios.post(('/api/reservation/'), reservation); + return response.data; + } catch (error) { + console.error(error); + return null; + } + } +} + +export default ReservationService; diff --git a/SAS/TMSS/frontend/tmss_webapp/src/utils/unit.converter.js b/SAS/TMSS/frontend/tmss_webapp/src/utils/unit.converter.js index 5173d5a077ebe0c809cb89085b398a09551bea59..fb1d585b3b8e618ebdd35dba8a5fa1ecffad2fcd 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/utils/unit.converter.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/utils/unit.converter.js @@ -1,3 +1,5 @@ +import _ from 'lodash'; + const UnitConverter = { resourceUnitMap: {'time':{display: 'Hours', conversionFactor: 3600, mode:'decimal', minFractionDigits:0, maxFractionDigits: 2 }, 'bytes': {display: 'TB', conversionFactor: (1024*1024*1024*1024), mode:'decimal', minFractionDigits:0, maxFractionDigits: 3}, @@ -29,6 +31,13 @@ const UnitConverter = { } return seconds; }, + getHHmmssToSecs: function(seconds) { + if (seconds) { + const strSeconds = _.split(seconds, ":"); + return strSeconds[0]*3600 + strSeconds[1]*60 + Number(strSeconds[2]); + } + return 0; + }, radiansToDegree: function(object) { for(let type in object) { if (type === 'transit_offset') {