diff --git a/SAS/TMSS/frontend/tmss_webapp/package.json b/SAS/TMSS/frontend/tmss_webapp/package.json index 98a358b8eab1a02049d514cb456e457f2e997b1d..69db9ad13a90ad324e34161dcd75df86581af5ad 100644 --- a/SAS/TMSS/frontend/tmss_webapp/package.json +++ b/SAS/TMSS/frontend/tmss_webapp/package.json @@ -3,6 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { + "@apidevtools/json-schema-ref-parser": "^9.0.6", "@fortawesome/fontawesome-free": "^5.13.1", "@json-editor/json-editor": "^2.3.0", "@testing-library/jest-dom": "^4.2.4", diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/create.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/create.js new file mode 100644 index 0000000000000000000000000000000000000000..7c28ac73ca0ba5c9eadc0827604017f1321f0069 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/create.js @@ -0,0 +1,339 @@ +import React, {Component} from 'react'; +import { Link, Redirect } from 'react-router-dom'; +import _ from 'lodash'; +import $RefParser from "@apidevtools/json-schema-ref-parser"; + +import {InputText} from 'primereact/inputtext'; +import {InputNumber} from 'primereact/inputnumber'; +import {InputTextarea} from 'primereact/inputtextarea'; +import {Dropdown} from 'primereact/dropdown'; +import { Button } from 'primereact/button'; +import {Growl} from 'primereact/components/growl/Growl'; + +import AppLoader from '../../layout/components/AppLoader'; +import Jeditor from '../../components/JSONEditor/JEditor'; + +import ProjectService from '../../services/project.service'; +import ScheduleService from '../../services/schedule.service'; +import TaskService from '../../services/task.service'; +import UIConstants from '../../utils/ui.constants'; + +/** + * Component to create a new SchedulingUnit + */ +export class SchedulingUnitCreate extends Component { + constructor(props) { + super(props); + this.state = { + isLoading: true, + redirect: null, + errors: [], + schedulingSets: [], + schedulingUnit: {}, + project: props.match.params.project || null, + projectDisabled: props.match.params.project || false, + observStrategy: {}, + paramsSchema: {}, + schema: null + } + this.projects = []; + this.schedulingSets = []; + this.observStrategies = []; + this.taskTemplates = []; + this.tooltipOptions = UIConstants.tooltipOptions; + this.formRules = { + name: {required: true, message: "Name can not be empty"}, + description: {required: true, message: "Description can not be empty"} + }; + + this.setEditorOutput = this.setEditorOutput.bind(this); + this.changeProject = this.changeProject.bind(this); + this.changeStrategy = this.changeStrategy.bind(this); + this.setSchedUnitParams = this.setSchedUnitParams.bind(this); + this.validateForm = this.validateForm.bind(this); + this.setEditorFunction = this.setEditorFunction.bind(this); + this.saveSchedulingUnit = this.saveSchedulingUnit.bind(this); + this.cancelCreate = this.cancelCreate.bind(this); + } + + componentDidMount() { + const promises = [ ProjectService.getProjectList(), + ScheduleService.getSchedulingSets(), + ScheduleService.getObservationStrategies(), + TaskService.getTaskTemplates()] + Promise.all(promises).then(responses => { + this.projects = responses[0]; + this.schedulingSets = responses[1]; + this.observStrategies = responses[2]; + this.taskTemplates = responses[3]; + if (this.state.project) { + const projectSchedSets = _.filter(this.schedulingSets, {'project_id': this.state.project}); + this.setState({isLoading: false, schedulingSets: projectSchedSets}); + } else { + this.setState({isLoading: false}); + } + }); + } + + changeProject(projectName) { + const projectSchedSets = _.filter(this.schedulingSets, {'project_id': projectName}); + this.setState({project: projectName, schedulingSets: projectSchedSets}); + } + + async changeStrategy (strategyId) { + + const observStrategy = _.find(this.observStrategies, {'id': strategyId}); + const $refs = await $RefParser.resolve(observStrategy.template); + console.log($refs.get(observStrategy.template.parameters[0]['refs'][0])); + console.log($refs.get("#/tasks")); + const tasks = observStrategy.template.tasks; + const requiredFields = _.map(observStrategy.template.parameters, 'name'); + let paramsOutput = {}; + let schema = { type: 'object', additionalProperties: false, + properties: {}, definitions:{}, + // required: requiredFields + }; + + observStrategy.template.parameters.forEach(async(param, index) => { + }); + + for (const taskName in tasks) { + // _.keys(tasks).forEach( async(taskName) => { + const task = tasks[taskName]; + const $taskRefs = await $RefParser.resolve(task); + const taskTemplate = _.find(this.taskTemplates, {'name': task['specifications_template']}); + console.log(taskTemplate); + schema['$id'] = taskTemplate.schema['$id']; + schema['$schema'] = taskTemplate.schema['$schema']; + observStrategy.template.parameters.forEach(async(param, index) => { + if (param.refs[0].indexOf(`/tasks/${taskName}`) > 0) { + let property = { }; + let tempProperty = null; + console.log(taskTemplate); + const $templateRefs = await $RefParser.resolve(taskTemplate); + console.log($templateRefs.values()); + try { + console.log(param.refs[0].replace(`#/tasks/${taskName}/specifications_doc`, '#')); + tempProperty = $templateRefs.get(param.refs[0].replace(`#/tasks/${taskName}/specifications_doc`, '#/schema/properties')) + } catch(error) { + console.log(error); + console.log(error.message); + const taskPaths = param.refs[0].split("/"); + console.log(taskPaths); + //tempProperty = $templateRefs.get(`#/schema/properties/${taskPaths[4]}`); + tempProperty = taskTemplate.schema.properties[taskPaths[4]]; + if (tempProperty.type === 'array') { + tempProperty = tempProperty.items.properties[taskPaths[6]]; + } + property = tempProperty; + } + property.title = param.name; + property.default = $taskRefs.get(param.refs[0].replace(`#/tasks/${taskName}`, '#')); + paramsOutput[`param_${index}`] = property.default; + console.log(property); + // console.log($templateRefs.get(param.refs[0].replace(`#/tasks/${taskName}/specifications_doc`, '#'))); + schema.properties[`param_${index}`] = property; + for (const definitionName in taskTemplate.schema.definitions) { + schema.definitions[definitionName] = taskTemplate.schema.definitions[definitionName]; + } + } + }); + } + console.log(schema); + console.log(paramsOutput); + this.setState({observStrategy: observStrategy, schema: schema, paramsOutput: paramsOutput}); + } + + /** + * This is the callback method to be passed to the JSON editor. + * JEditor will call this function when there is change in the editor. + * @param {Object} jsonOutput + * @param {Array} errors + */ + setEditorOutput(jsonOutput, errors) { + this.setState({paramsOutput: jsonOutput}); + if (errors.length === 0 && !this.state.validEditor) { + this.setState({validEditor: true, validForm: this.validateForm()}); + } else if (errors.length > 0 && this.state.validEditor) { + this.setState({validEditor: false, validForm: this.validateForm()}); + } + } + + setSchedUnitParams(key, value) { + + } + + /** + * JEditor's function that to be called when parent wants to trigger change in the JSON Editor + * @param {Function} editorFunction + */ + setEditorFunction(editorFunction) { + this.setState({editorFunction: editorFunction}); + } + + /** + * Function to validate the form excluding the JSON Editor values + */ + validateForm() { + let validForm = false; + let errors = {}; + for (const fieldName in this.formRules) { + const rule = this.formRules[fieldName]; + const fieldValue = this.state.schedulingUnit[fieldName]; + if (rule.required) { + if (!fieldValue) { + errors[fieldName] = rule.message?rule.message:`${fieldName} is required`; + } + } + } + this.setState({errors: errors}); + if (this.state.schedulingUnit.name && this.state.schedulingUnit.description) { + validForm = true; + } + return validForm; + } + + saveSchedulingUnit() { + + } + + cancelCreate() { + + } + + render() { + if (this.state.redirect) { + return <Redirect to={ {pathname: this.state.redirect} }></Redirect> + } + + const ObservParameters = (props) => { + const params = props.params; + return ( + <> + <label>{params.length}</label> + {params.length>0 && params.map((param, index) => ( + <> + <label key={'label' + index}>{param.name}</label> + </> + ))} + </> + ); + }; + + const schema = this.state.schema; + + let jeditor = null; + if (this.state.schema) { + jeditor = React.createElement(Jeditor, {title: "Specification", + schema: schema, + //initValue: this.state.templateOutput[this.state.task.specifications_template_id], + // initValue: this.state.paramsOutput, + callback: this.setEditorOutput, + parentFunction: this.setEditorFunction + }); + } + + 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>Scheduling Unit - Add</h2> + </div> + <div className="p-col-2 p-lg-2 p-md-2"> + <Link to={{ pathname: '/schedulingunit'}} tite="Close" style={{float: "right"}}> + <i className="fa fa-window-close" style={{marginTop: "10px"}}></i> + </Link> + </div> + </div> + { this.state.isLoading ? <AppLoader /> : + <> + <div> + <div className="p-fluid"> + <div className="p-field p-grid"> + <label htmlFor="schedUnitName" 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 ?'input-error':''} id="schedUnitName" data-testid="name" + tooltip="Enter name of the Scheduling Unit" tooltipOptions={this.tooltipOptions} maxLength="128" + value={this.state.schedulingUnit.name} + onChange={(e) => this.setSchedUnitParams('name', e.target.value)} + onBlur={(e) => this.setSchedUnitParams('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> + <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 ?'input-error':''} rows={3} cols={30} + tooltip="Longer description of the scheduling unit" tooltipOptions={this.tooltipOptions} maxLength="128" + data-testid="description" value={this.state.schedulingUnit.description} + onChange={(e) => this.setSchedUnitParams('description', e.target.value)} + onBlur={(e) => this.setSchedUnitParams('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="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.project} disabled={this.state.projectDisabled} + options={this.projects} + onChange={(e) => {this.changeProject(e.value)}} + placeholder="Select Project" /> + </div> + <div className="col-lg-1 col-md-1 col-sm-12"></div> + <label htmlFor="schedSet" className="col-lg-2 col-md-2 col-sm-12">Scheduling Set <span style={{color:'red'}}>*</span></label> + <div className="col-lg-3 col-md-3 col-sm-12"> + <Dropdown data-testid="schedSet" id="schedSet" optionLabel="name" optionValue="id" + tooltip="Period Category" tooltipOptions={this.tooltipOptions} + value={this.state.schedulingUnit.scheduling_set_id} + options={this.state.schedulingSets} + onChange={(e) => {this.setSchedUnitParams('scheduling_set_id',e.value)}} + placeholder="Select Scheduling Set" /> + </div> + </div> + <div className="p-field p-grid"> + <label htmlFor="observStrategy" className="col-lg-2 col-md-2 col-sm-12">Observation Strategy </label> + <div className="col-lg-3 col-md-3 col-sm-12" data-testid="observStrategy" > + <Dropdown inputId="observStrategy" optionLabel="name" optionValue="id" + tooltip="Observation Strategy Template used to be used to create the Scheduling Unit and Tasks" tooltipOptions={this.tooltipOptions} + value={this.state.observStrategy.id} + options={this.observStrategies} + onChange={(e) => {this.changeStrategy(e.value)}} + placeholder="Select Project" /> + </div> + <div className="col-lg-1 col-md-1 col-sm-12"></div> + </div> + {/* <ObservParameters params={this.state.observStrategy.template?this.state.observStrategy.template.parameters:[]}></ObservParameters> */} + + </div> + <div className="p-fluid"> + <div className="p-grid"> + <div className="p-col-12"> + {this.state.schema?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.saveTask} disabled={!this.state.validEditor || !this.state.validForm} /> + </div> + <div className="p-col-1"> + <Button label="Cancel" className="p-button-danger" icon="pi pi-times" onClick={this.cancelEdit} /> + </div> + </div> + </div> + </> + } + </React.Fragment> + ); + } +} + +export default SchedulingUnitCreate; \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/index.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/index.js index 0739e7824a404d7e1a21de18298cd809ae0dd79d..4aac9725ecf90e9a85e5e9f4dd07de422b6aa35e 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/index.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/index.js @@ -10,7 +10,8 @@ import {ProjectList, ProjectCreate, ProjectView, ProjectEdit} from './Project'; import {Dashboard} from './Dashboard'; import {Scheduling} from './Scheduling'; import {TaskEdit, TaskView} from './Task'; -import ViewSchedulingUnit from './Scheduling/ViewSchedulingUnit' +import ViewSchedulingUnit from './Scheduling/ViewSchedulingUnit'; +import SchedulingUnitCreate from './Scheduling/create'; import { CycleList, CycleCreate, CycleView, CycleEdit } from './Cycle'; export const routes = [ @@ -25,6 +26,10 @@ export const routes = [ path: "/schedulingunit", component: Scheduling, name: 'Scheduling Unit' + },{ + path: "/schedulingunit/create", + component: SchedulingUnitCreate, + name: 'Scheduling Unit Add' },{ path: "/task", component: TaskView, @@ -65,6 +70,10 @@ export const routes = [ path: "/project/edit/:id", component: ProjectEdit, name: 'Project Edit' + },{ + path: "/project/:project/schedulingunit/create", + component: SchedulingUnitCreate, + name: 'Scheduling Unit Add' },{ path: "/cycle/edit/:id", component: CycleEdit, diff --git a/SAS/TMSS/frontend/tmss_webapp/src/services/schedule.service.js b/SAS/TMSS/frontend/tmss_webapp/src/services/schedule.service.js index f09e1da9e877f7867ce4f2614cabadccf3a9f6aa..3c5e1eeb3965e8c823edeea47b4632b82baea807 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/services/schedule.service.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/services/schedule.service.js @@ -119,6 +119,24 @@ const ScheduleService = { }); return res; }, + getSchedulingSets: async function() { + try { + const response = await axios.get('/api/scheduling_set/'); + return response.data.results; + } catch(error) { + console.error(error); + return []; + }; + }, + getObservationStrategies: async function() { + try { + const response = await axios.get('/api/scheduling_unit_observing_strategy_template/'); + return response.data.results; + } catch(error) { + console.error(error); + return []; + }; + } } export default ScheduleService; \ No newline at end of file