diff --git a/SAS/TMSS/frontend/tmss_webapp/package.json b/SAS/TMSS/frontend/tmss_webapp/package.json index b5328e779ebfa77c91b24d16cf90c040bc06e3a4..aa36b24233af3cc9b0c57a2bafd5c32e3da553d7 100644 --- a/SAS/TMSS/frontend/tmss_webapp/package.json +++ b/SAS/TMSS/frontend/tmss_webapp/package.json @@ -3,11 +3,13 @@ "version": "0.1.0", "private": true, "dependencies": { + "@fortawesome/fontawesome-free": "^5.13.1", "@json-editor/json-editor": "^2.3.0", "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^7.1.2", "axios": "^0.19.2", + "bootstrap": "^4.5.0", "font-awesome": "^4.7.0", "history": "^5.0.0", "node-sass": "^4.12.0", @@ -33,6 +35,7 @@ "test": "react-scripts test", "eject": "react-scripts eject" }, + "proxy": "http://192.168.99.100:8008", "eslintConfig": { "extends": "react-app" }, diff --git a/SAS/TMSS/frontend/tmss_webapp/public/manifest.json b/SAS/TMSS/frontend/tmss_webapp/public/manifest.json index 080d6c77ac21bb2ef88a6992b2b73ad93daaca92..1f2f141fafdeb1d31d85b008ec5132840c5e6362 100644 --- a/SAS/TMSS/frontend/tmss_webapp/public/manifest.json +++ b/SAS/TMSS/frontend/tmss_webapp/public/manifest.json @@ -6,16 +6,6 @@ "src": "favicon.ico", "sizes": "64x64 32x32 24x24 16x16", "type": "image/x-icon" - }, - { - "src": "logo192.png", - "type": "image/png", - "sizes": "192x192" - }, - { - "src": "logo512.png", - "type": "image/png", - "sizes": "512x512" } ], "start_url": ".", diff --git a/SAS/TMSS/frontend/tmss_webapp/src/App.css b/SAS/TMSS/frontend/tmss_webapp/src/App.css index 18ccb9475ca09cfadf5cd73e300c156090c9812f..0fd3de74678b7f14f3aadbe2bb0b246362a66830 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/App.css +++ b/SAS/TMSS/frontend/tmss_webapp/src/App.css @@ -29,6 +29,40 @@ color: #61dafb; } +label { + font-size: 14px; + font-weight: 600; + align-items: flex-start !important; +} + +.chips-readonly > ul { + border: none; +} + +p { + font-size: 14px; +} + +.card { + border: none; +} + +.card-title { + margin-bottom: 0.5rem; +} + +.card-body { + padding: 0.25rem; +} + +.task-list { + padding-inline-start: 0px; +} + +.task-list > li { + list-style: none; +} + @keyframes App-logo-spin { from { transform: rotate(0deg); diff --git a/SAS/TMSS/frontend/tmss_webapp/src/App.js b/SAS/TMSS/frontend/tmss_webapp/src/App.js index 0d607d738c330273f30c7f9dc5a6421c4a0069c7..6f95be3abbc5ed817fbc4a46e5dc0a26604facb7 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/App.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/App.js @@ -8,9 +8,12 @@ import {RoutedContent} from './routes'; // import {Dashboard} from './routes/dashboard/dashboard'; +import 'primeicons/primeicons.css'; +import 'primereact/resources/themes/nova-light/theme.css'; +import 'primereact/resources/primereact.css'; import './layout/layout.scss'; -// import './App.css'; - +import './App.scss'; +import './App.css'; class App extends Component { @@ -23,14 +26,14 @@ class App extends Component { this.onMenuItemClick = this.onMenuItemClick.bind(this) this.menu = [ {label: 'Dashboard', icon: 'pi pi-fw pi-home', to:'/dashboard'}, - {label: 'Scheduling Units', icon: 'pi pi-fw pi-calendar', to:'/scheduling'} + {label: 'Scheduling Units', icon: 'pi pi-fw pi-calendar', to:'/scheduling'}, + {label: 'Tasks', icon: 'pi pi-fw pi-check-square', to:'/task'} ]; // this.menuComponent = {'Dashboard': Dashboard} } onMenuItemClick(event) { - console.log(event); this.setState({currentMenu:event.item.label, currentPath: event.item.path}); } diff --git a/SAS/TMSS/frontend/tmss_webapp/src/App.scss b/SAS/TMSS/frontend/tmss_webapp/src/App.scss new file mode 100644 index 0000000000000000000000000000000000000000..93c4b1c7a72602ac2fa3e7d2a16fd445736e30e9 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/App.scss @@ -0,0 +1 @@ +@import "~bootstrap/scss/bootstrap"; \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/components/JSONEditor/JEditor.js b/SAS/TMSS/frontend/tmss_webapp/src/components/JSONEditor/JEditor.js new file mode 100644 index 0000000000000000000000000000000000000000..106e5c7f23a34a53d5f8c69abc04819883d108fd --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/components/JSONEditor/JEditor.js @@ -0,0 +1,326 @@ +/** + * This is the custom component to use "@json-editor/json-editor" + * to create form using JSON Schema and get JSON output + */ +import React, {useEffect, useRef} from 'react'; +import _ from 'lodash'; + +// Sample JSON schema. The same can be received from an API call +// import SchemaDB from './task.observation.schema'; + + import "@fortawesome/fontawesome-free/css/all.css"; +// import '../../styles/components/fa_all.scss'; +const JSONEditor = require("@json-editor/json-editor").JSONEditor; + +function Jeditor(props) { + // console.log("In JEditor"); + // State object to hold the editor reference to get output + // const [stateEditor, setStateEditor] = useState({editor:null}); + const editorRef = useRef(null); + let editor = null; + useEffect(() => { + const element = document.getElementById('editor_holder'); + let schema = {}; + Object.assign(schema, props.schema?props.schema:{}); + + for (const definitionKey in schema.definitions) { + if (definitionKey === 'pointing') { + const defintion = schema.definitions[definitionKey]; + let defProperties = defintion.properties; + if (defProperties) { + for (const propName in defProperties) { + if (propName === 'angle1' || propName === 'angle2') { + let defProperty = getAngleProperty(defProperties[propName], propName === 'angle2'); + defProperties[propName] = defProperty; + } + } + } + } + } + + getCustomProperties(schema.properties); + + schema.title = props.title; + const validator = validateSubbandOutput; + JSONEditor.defaults.custom_validators.push((schema, value, path) => { + const errors = []; + if (schema.customType === "subband_list") { + if (!validator(value)) { + errors.push({ + path: path, + property: 'customType', + message: 'Not a valid input for Subband List' + }); + } + } + return errors; + }); + + const editorOptions = { + form_name_root: "specification", + schema: schema, + // display_required_only: true, // circular references will blow up without this + theme: 'bootstrap4', + iconlib: 'fontawesome5', + // theme: 'tailwind', + // iconlib: 'spectre', + display_required_only: false, + // startval: props.initValue, + remove_button_labels: true, + disable_edit_json: true, + disable_properties: true, + disable_collapse: true, + compact: true + }; + if (props.initValue) { + editorOptions.startval = updateInput(_.cloneDeep(props.initValue)); + } + editor = new JSONEditor(element, editorOptions); + // editor.setValue(props.initValue); + // editor.getEditor('root').disable(); + if (props.disabled) { + editor.disable(); + } + if (!props.initValue) { + // editor.setValue(updateInput(editor.getValue())); + } + // setStateEditor({editor: editor}); + if (props.parentFunc) { + props.parentFunc(childFunc); + } + editorRef.current = editor; + editor.on('change', () => {setEditorOutput()}); + }, [props.schema]); + + /** + * Function to call on button click and send the output back to parent through callback + * + */ + function setEditorOutput(){ + const editorOutput = editorRef.current.getValue(); + const formattedOutput = updateOutput(_.cloneDeep(editorOutput)); + const editorValidationErrors = editorRef.current.validate(); + if (props.callback) { + props.callback(formattedOutput, editorValidationErrors); + } + } + + function childFunc() { + editorRef.current.destroy(); + } + + function getAngleProperty(defProperty, isDegree) { + let newProperty = { + "type": "object", + "additionalProperties": false, + "format": "grid", + "title": defProperty.title, + "description": defProperty.description}; + let subProperties = {}; + if (isDegree) { + subProperties["dd"] = { "type": "number", + "title": "DD", + "description": "Degrees", + "default": 0, + "minimum": 0, + "maximum": 90 }; + } else { + subProperties["hh"] = { "type": "number", + "title": "HH", + "description": "Hours", + "default": 0, + "minimum": 0, + "maximum": 23 }; + + } + subProperties["mm"] = { "type": "number", + "title": "MM", + "description": "Minutes", + "default": 0, + "minimum": 0, + "maximum": 59 }; + subProperties["ss"] = { "type": "number", + "title": "SS", + "description": "Seconds", + "default": 0, + "minimum": 0, + "maximum": 59 }; + + newProperty.properties = subProperties; + newProperty.required = isDegree?["dd", "mm", "ss"]:["hh", "mm", "ss"]; + return newProperty; + } + + function getCustomProperties(properties) { + for (const propertyKey in properties) { + const propertyValue = properties[propertyKey]; + if (propertyKey === 'subbands') { + let newProperty = {}; + newProperty.additionalItems = false; + newProperty.title = propertyValue.title; + newProperty.type = 'string'; + newProperty.default = ''; + newProperty.description = "For Range enter Start and End seperated by 2 dots. Mulitple ranges can be separated by comma. Minimum should be 0 and maximum should be 511. For exmaple 11..20, 30..50"; + newProperty.customType = 'subband_list'; + properties[propertyKey] = newProperty; + } else if (propertyKey.toLowerCase() === 'duration') { + propertyValue.title = "Duration (minutes)"; + propertyValue.default = 1; + } else if (propertyValue instanceof Object) { + getCustomProperties(propertyValue); + } + } + } + + function updateInput(editorInput) { + for (const inputKey in editorInput) { + const inputValue = editorInput[inputKey]; + if (inputValue instanceof Object) { + if (inputKey.endsWith('pointing')) { + inputValue.angle1 = getAngleInput(inputValue.angle1); + inputValue.angle2 = getAngleInput(inputValue.angle2, true); + } else if (inputKey === 'subbands') { + editorInput[inputKey] = getSubbandInput(inputValue); + } else { + updateInput(inputValue); + } + } else if (inputKey.toLowerCase() === 'duration') { + editorInput[inputKey] = inputValue/60; + } + } + return editorInput; + } + + function updateOutput(editorOutput) { + for (const outputKey in editorOutput) { + let outputValue = editorOutput[outputKey]; + if (outputValue instanceof Object) { + if (outputKey.endsWith('pointing')) { + outputValue.angle1 = getAngleOutput(outputValue.angle1); + outputValue.angle2 = getAngleOutput(outputValue.angle2); + } else { + updateOutput(outputValue); + } + } else if (outputKey === 'subbands') { + editorOutput[outputKey] = getSubbandOutput(outputValue); + } else if (outputKey.toLowerCase() === 'duration') { + editorOutput[outputKey] = outputValue * 60; + } + } + return editorOutput; + } + + function getAngleInput(prpInput, isDegree) { + const degrees = prpInput * 180 / Math.PI; + if (isDegree) { + const dd = Math.floor(prpInput * 180 / Math.PI); + const mm = Math.floor((degrees-dd) * 60); + const ss = +((degrees-dd-(mm/60)) * 3600).toFixed(0); + return { + dd: dd, + mm: mm, + ss: ss + } + } else { + const hh = Math.floor(degrees/15); + const mm = Math.floor((degrees - (hh*15))/15 * 60 ); + const ss = +((degrees -(hh*15)-(mm*15/60))/15 * 3600).toFixed(0); + return { + hh: hh, + mm: mm, + ss: ss + } + } + } + + function getSubbandInput(prpInput) { + let subbandString = ""; + for (let index=0; index < prpInput.length; index++) { + if (subbandString.length > 0) { + subbandString += ","; + } + let firstVal = prpInput[index] + let nextVal = prpInput[index]; + if (prpInput[index+1] - nextVal === 1) { + subbandString += firstVal + ".."; + while( prpInput[index+1] - nextVal === 1) { + index++; + nextVal = prpInput[index]; + } + subbandString += nextVal; + } else { + subbandString += firstVal; + } + } + return subbandString; + } + + function getAngleOutput(prpOutput) { + if ('dd' in prpOutput) { + return ((prpOutput.dd + prpOutput.mm/60 + prpOutput.ss/3600)*Math.PI/180); + } else { + return ((prpOutput.hh*15 + prpOutput.mm/4 + prpOutput.ss/240)*Math.PI/180); + } + } + + function validateSubbandOutput(prpOutput){ + try { + if (prpOutput) { + const subbandArray = prpOutput.split(","); + for (const subband of subbandArray ) { + const subbandRange = subband.split('..'); + if (subbandRange.length > 1) { + const firstVal = parseInt(subbandRange[0]); + const nextVal = parseInt(subbandRange[1]) + 1; + if (isNaN(firstVal * nextVal) || firstVal < 0 || firstVal > 510 + || nextVal < 0 || nextVal > 511 + || firstVal >nextVal) { + return false; + } + } else { + if (isNaN(parseInt(subbandRange[0]))) { + return false; + } + if (parseInt(subbandRange[0]) < 0 || parseInt(subbandRange[0]) > 511) { + return false; + } + } + } + } else { + return false + } + } catch(exception) { + return false; + } + return true; + } + + function getSubbandOutput(prpOutput) { + // if (validateSubbandOutput(prpOutput)) { + const subbandArray = prpOutput.split(","); + let subbandList = []; + for (const subband of subbandArray ) { + const subbandRange = subband.split('..'); + if (subbandRange.length > 1) { + subbandList = subbandList.concat( _.range(subbandRange[0], (parseInt(subbandRange[1])+1))); + } else { + subbandList = subbandList.concat(parseInt(subbandRange[0])); + } + } + prpOutput = subbandList; + // } else { + // alert("Subband list values are not valid!"); + // prpOutput = []; + // } + return prpOutput; + } + + return ( + <React.Fragment> + <div id='editor_holder'></div> + {/* <div><input type="button" onClick={setEditorOutput} value="Show Output" /></div> */} + </React.Fragment> + ); +}; + +export default Jeditor; \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/components/JSONEditor/JViewer.js b/SAS/TMSS/frontend/tmss_webapp/src/components/JSONEditor/JViewer.js new file mode 100644 index 0000000000000000000000000000000000000000..220a6cfa73cf42d7dc933eb253cb382ebba4acfb --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/components/JSONEditor/JViewer.js @@ -0,0 +1,31 @@ +/** + * Component to view the JSON data using 'react-json-view' package + */ +import React, {Component} from 'react'; +import ReactJson from 'react-json-view'; + +export default class JViewer extends Component { + constructor(props) { + super(props); + this.state = { + outputJSON: props.outputJSON + } + this.updateOutput = this.updateOutput.bind(this); + } + + /** + * Function to be called by the parent to update the JSON content of the viewer + * @param {JSON} outputJSON + */ + updateOutput(outputJSON) { + this.state.outputJSON = outputJSON; + } + + render() { + return ( + <React.Fragment> + <ReactJson src={this.state.outputJSON} /> + </React.Fragment> + ); + } +} \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/components/JSONEditor/index.js b/SAS/TMSS/frontend/tmss_webapp/src/components/JSONEditor/index.js new file mode 100644 index 0000000000000000000000000000000000000000..60bb36fd1b4c5645a2dead54172db7fc3c9f0e1f --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/components/JSONEditor/index.js @@ -0,0 +1,3 @@ +import { JSONEditor } from './jsonEditor'; + +export default JSONEditor; \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/index.js b/SAS/TMSS/frontend/tmss_webapp/src/index.js index f5185c1ec7a5dccf30b55a8e3f89afc3eca764a1..76fb92e2e220af22b7f1a3afe63e967fbae4a8bd 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/index.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/index.js @@ -5,9 +5,9 @@ import App from './App'; import * as serviceWorker from './serviceWorker'; ReactDOM.render( - <React.StrictMode> - <App /> - </React.StrictMode>, + // <React.StrictMode> + <App />, + // </React.StrictMode>, document.getElementById('root') ); diff --git a/SAS/TMSS/frontend/tmss_webapp/src/layout/components/AppMenu.js b/SAS/TMSS/frontend/tmss_webapp/src/layout/components/AppMenu.js index 7f0e4e18c1ee09448b7ed7f2330702f7b3d875dc..51e3e785c404eb0517378faafebd259825e057df 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/layout/components/AppMenu.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/layout/components/AppMenu.js @@ -92,11 +92,6 @@ class AppSubmenu extends Component { let active = this.state.activeIndex === i; // let styleClass = classNames(item.badgeStyleClass, {'active-menuitem': active && !item.to}); let styleClass = classNames(item.badgeStyleClass, {'active-menuitem': active && item.to}); - console.log(item.badgeStyleClass); - console.log(i); - console.log(this.state.activeIndex); - console.log(active); - console.log(styleClass); return ( <li className={styleClass} key={i}> {item.items && this.props.root===true && <div className='arrow'></div>} @@ -124,7 +119,6 @@ export class AppMenu extends Component { } render() { - // console.log(authenticationService.currentUserValue); return ( <div className={'layout-sidebar layout-sidebar-light'} > <div className="layout-menu-container"> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/edit.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/edit.js new file mode 100644 index 0000000000000000000000000000000000000000..ca9ecd6b48f36e4879c177d3241637548956cf3d --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/edit.js @@ -0,0 +1,198 @@ +import React, {Component} from 'react'; +import { Link, Redirect } from 'react-router-dom'; +import moment from 'moment'; +import _ from 'lodash'; + +import {InputText} from 'primereact/inputtext'; +import {InputTextarea} from 'primereact/inputtextarea'; +import { Calendar } from 'primereact/calendar'; +import {Chips} from 'primereact/chips'; +import {Checkbox} from 'primereact/checkbox'; +import {Dropdown} from 'primereact/dropdown'; +import { Button } from 'primereact/button'; + +import Jeditor from '../../components/JSONEditor/JEditor'; + +import TaskService from '../../services/task.services'; + +export class TaskEdit extends Component { + templateOutput = {}; + + constructor(props) { + super(props); + this.state = { + task: { + name: "", + created_at: null, + updated_at: null, + tags:[], + do_cancel: false + }, + redirect: null, + taskTemplates:[], + validEditor: false + // templateOutput:{}, // id: selectedTemplateId, output: values enetered in the editor form + }; + this.setEditorOutput = this.setEditorOutput.bind(this); + this.setTaskParams = this.setTaskParams.bind(this); + this.changeTaskTemplate = this.changeTaskTemplate.bind(this); + this.setEditorFunc = this.setEditorFunc.bind(this); + this.saveTask = this.saveTask.bind(this); + this.cancelEdit = this.cancelEdit.bind(this); + } + + setEditorOutput(jsonOutput, errors) { + this.templateOutput[this.state.task.specifications_template_id] = jsonOutput; + if (errors.length === 0 && !this.state.validEditor) { + this.setState({validEditor: true}); + } else if (errors.length > 0 && this.state.validEditor) { + this.setState({validEditor: false}); + } + } + + setTaskParams(key, value) { + let task = this.state.task; + task[key] = value; + this.setState({task: task}); + } + + setEditorFunc(editorFunc) { + this.setState({editorFunc: editorFunc}); + } + + changeTaskTemplate(templateId) { + const template = _.find(this.state.taskTemplates, {'id': templateId}); + let task = this.state.task; + task.specifications_template_id = templateId; + task.specifications_template = template.url; + this.setState({taskSchema: null}); + this.setState({task: task, taskSchema: template.schema}); + this.state.editorFunc(); + } + + saveTask() { + let task = this.state.task; + task.specifications_doc = this.templateOutput[task.specifications_template_id]; + TaskService.updateTask("draft", task) + .then( (taskDraft) => { + if (taskDraft) { + this.setState({redirect: '/task/view'}); + } + }); + } + + cancelEdit() { + this.setState({redirect: '/task/view'}); + } + + componentDidMount() { + TaskService.getTaskTemplates() + .then((templates) => { + this.setState({taskTemplates: templates}); + }); + TaskService.getTaskDetails("draft", this.props.taskId?this.props.taskId:this.props.location.state.taskId) + .then((task) => { + if (task) { + TaskService.getSchedulingUnit("draft", task.scheduling_unit_draft_id) + .then((schedulingUnit) => { + this.setState({schedulingUnit: schedulingUnit}); + }); + + this.templateOutput[task.specifications_template_id] = task.specifications_doc; + TaskService.getTaskTemplate(task.specifications_template_id) + .then((taskTemplate) => { + this.setState({task: task, taskSchema: taskTemplate.schema}); + }); + } + }); + } + + render() { + if (this.state.redirect) { + return <Redirect to={ {pathname: this.state.redirect, + state: {taskId: this.state.task.id}} }></Redirect> + } + const taskSchema = this.state.taskSchema; + let jeditor, created_at, updated_at = null; + if (this.state.taskSchema) { + jeditor = React.createElement(Jeditor, {title: "Specification", + schema: taskSchema, + //initValue: this.state.templateOutput[this.state.task.specifications_template_id], + initValue: this.templateOutput[this.state.task.specifications_template_id], + callback: this.setEditorOutput, + parentFunc: this.setEditorFunc + }); + created_at = moment(this.state.task.created_at).toDate(); + updated_at = moment(this.state.task.created_at).toDate(); + } + + return ( + <React.Fragment> + <div style={{marginBottom: "10px"}}> + <h2>Task - Edit</h2> + </div> + <div> + <div className="p-fluid"> + <div className="p-field p-grid"> + <label htmlFor="taskName" className="p-col-2">Name</label> + <div className="p-col-4"> + <InputText id="taskName" type="text" value={this.state.task.name} onChange={(e) => this.setTaskParams('name', e.target.value)}/> + </div> + <label htmlFor="description" className="p-col-2">Description</label> + <div className="p-col-4"> + <InputTextarea rows={3} cols={30} value={this.state.task.description} onChange={(e) => this.setTaskParams('description', e.target.value)}/> + </div> + </div> + {/* <div className="p-field p-grid"> + <label htmlFor="createdAt" className="p-col-2">Created At</label> + <div className="p-col-4"> + <Calendar showTime={true} hourFormat="24" value={created_at} onChange={(e) => this.setState({date2: e.value})}></Calendar> + </div> + <label htmlFor="updatedAt" className="p-col-2">Updated At</label> + <div className="p-col-4"> + <Calendar showTime={true} hourFormat="24" value={updated_at} onChange={(e) => this.setState({date2: e.value})}></Calendar> + </div> + </div> */} + <div className="p-field p-grid"> + <label htmlFor="tags" className="p-col-2">Tags</label> + <div className="p-col-4"> + <Chips value={this.state.task.tags?this.state.task.tags:[]} onChange={(e) => this.setTaskParams('tags', e.value)}></Chips> + </div> + {/* <label htmlFor="doCancel" className="p-col-2">Do Cancel</label> + <div className="p-col-4"> + <Checkbox onChange={e => this.setTaskParams('do_cancel', e.checked)} checked={this.state.task.do_cancel}></Checkbox> + </div> */} + <label className="p-col-2">Scheduling Unit</label> + <Link className="p-col-4" to={ { pathname:'/scheduling'}}>{this.state.schedulingUnit?this.state.schedulingUnit.name:''}</Link> + </div> + <div className="p-field p-grid"> + <label htmlFor="tags" className="p-col-2">Template</label> + <div className="p-col-4"> + <Dropdown optionLabel="name" optionValue="id" + value={this.state.task.specifications_template_id} + options={this.state.taskTemplates} + onChange={(e) => {this.changeTaskTemplate(e.value)}} + placeholder="Select Task Template"/> + </div> + + </div> + + </div> + </div> + <div className="p-fluid"> + <div className="p-grid"><div className="p-col-12"> + {this.state.taskSchema?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} /> + </div> + <div className="p-col-1"> + <Button label="Cancel" className="p-button-danger" icon="pi pi-times" onClick={this.cancelEdit} /> + </div> + </div> + </React.Fragment> + ); + } +} \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/index.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/index.js new file mode 100644 index 0000000000000000000000000000000000000000..7ab687e3398c9015f5b074cdaedbafeeb81f92d0 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/index.js @@ -0,0 +1,4 @@ +import {TaskEdit} from './edit'; +import {TaskView} from './view'; + +export {TaskEdit, TaskView} ; \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/view.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/view.js new file mode 100644 index 0000000000000000000000000000000000000000..d44828e1edaed7803433d54c11b986db984c069c --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/view.js @@ -0,0 +1,149 @@ +import React, {Component} from 'react'; +import {Link} from 'react-router-dom' +import moment from 'moment'; + +import Jeditor from '../../components/JSONEditor/JEditor'; + +import TaskService from '../../services/task.services'; +import { Chips } from 'primereact/chips'; + +export class TaskView extends Component { + DATE_FORMAT = 'YYYY-MMM-DD HH:mm:ss'; + constructor(props) { + super(props); + this.state = { + }; + this.setEditorOutput = this.setEditorOutput.bind(this); + this.setEditorFunc = this.setEditorFunc.bind(this); + } + + setEditorOutput(outputJson) {} + + setEditorFunc(editorFunc) { + this.setState({editorFunc: editorFunc}); + } + + static getDerivedStateFromProps(nextProps, prevstate){ + if (prevstate.task && nextProps.location.state && + nextProps.location.state.taskId === prevstate.task.id) { + return {taskId: prevstate.task.id} + } + return null; + } + + componentDidUpdate(prevProps, prevState) { + if (this.state.task && this.props.location.state && + this.state.task.id !== this.props.location.state.taskId) { + this.getTaskDetails(this.props.location.state.taskId); + } + } + + componentDidMount() { + const taskId = this.props.taskId?this.props.taskId:1; + this.getTaskDetails(taskId); + } + + getTaskDetails(taskId) { + TaskService.getTaskDetails("draft", taskId) + .then((task) => { + if (task) { + TaskService.getSchedulingUnit("draft", task.scheduling_unit_draft_id) + .then((schedulingUnit) => { + this.setState({schedulingUnit: schedulingUnit}); + }); + TaskService.getTaskTemplate(task.specifications_template_id) + .then((taskTemplate) => { + if (this.state.editorFunc) { + this.state.editorFunc(); + } + this.setState({task: task, taskTemplate: taskTemplate}); + }); + + } + }); + } + + render() { + let jeditor = null + if (this.state.taskTemplate) { + jeditor = React.createElement(Jeditor, {title: "Specification", + schema: this.state.taskTemplate.schema, + initValue: this.state.task.specifications_doc, + disabled: true, + // callback: this.setEditorOutput, + parentFunc: this.setEditorFunc + }); + } + + const TaskRelationList = ({ list }) => ( + <ul className="task-list"> + {list.map(item => ( + <li key={item.id}> + <Link to={ { pathname:'/task', state: {taskId: item.id}}}>{item.name}</Link> + </li> + ))} + </ul> + ); + + return ( + <React.Fragment> + {/* <div style={{marginBottom: "10px"}}> */} + <div className="p-grid"> + <div className="p-col-2"> + <h2>Task - View </h2> + </div> + <div className="p-col-1"> + <Link to={{ pathname: '/task/edit', state: {taskId: this.state.task?this.state.task.id:''}}} tooltip="Edit Task" > + <i className="pi pi-pencil" style={{marginTop: "10px"}}></i> + </Link> + </div> + </div> + { this.state.task && + <React.Fragment> + <div className="p-grid"> + <label className="p-col-2">Name</label> + <span className="p-col-4">{this.state.task.name}</span> + <label className="p-col-2">Description</label> + <span className="p-col-4">{this.state.task.description}</span> + </div> + <div className="p-grid"> + <label className="p-col-2">Created At</label> + <span className="p-col-4">{moment.utc(this.state.task.created_at).format(this.DATE_FORMAT)}</span> + <label className="p-col-2">Updated At</label> + <span className="p-col-4">{moment.utc(this.state.task.updated_at).format(this.DATE_FORMAT)}</span> + {/* <span className="p-col-4">{moment(this.state.task.updated_at).format()}</span> */} + </div> + <div className="p-grid"> + <label className="p-col-2">Tags</label> + <Chips className="p-col-4 chips-readonly" disabled value={this.state.task.tags}></Chips> + {/* <label className="p-col-2">Do Cancel?</label> + <span className="p-col-4">{this.state.task.do_cancel}</span> */} + <label className="p-col-2">Scheduling Unit</label> + <Link className="p-col-4" to={ { pathname:'/scheduling'}}>{this.state.schedulingUnit?this.state.schedulingUnit.name:''}</Link> + </div> + <div className="p-grid"> + <label className="p-col-2">Predecessors</label> + <div className="p-col-4"> + <TaskRelationList list={this.state.task.predecessors} /> + </div> + <label className="p-col-2">Successors</label> + <div className="p-col-4"> + <TaskRelationList list={this.state.task.successors} /> + </div> + </div> + <div className="p-grid"> + <label className="p-col-2">Template</label> + <span className="p-col-4">{this.state.taskTemplate.name}</span> + + </div> + <div className="p-fluid"> + <div className="p-grid"><div className="p-col-12"> + {this.state.taskTemplate?jeditor:""} + </div></div> + </div> + </React.Fragment> + } + </React.Fragment> + ); + } +} \ 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 323019846cfc267c7874b64bee9ed4bd8a795f10..42a57b784c881506b9d8a72c382fd12d04feddca 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/index.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/index.js @@ -7,6 +7,7 @@ import { import {Dashboard} from './Dashboard'; import {Scheduling} from './Scheduling'; +import {TaskEdit, TaskView} from './Task'; export const RoutedContent = () => { return ( @@ -14,6 +15,9 @@ export const RoutedContent = () => { <Redirect from="/" to="/" exact /> <Route path="/dashboard" exact component={Dashboard} /> <Route path="/scheduling" exact component={Scheduling} /> + <Route path="/task" exact component={TaskView} /> + <Route path="/task/view" exact component={TaskView} /> + <Route path="/task/edit" exact component={TaskEdit} /> </Switch> ); } \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/services/task.services.js b/SAS/TMSS/frontend/tmss_webapp/src/services/task.services.js new file mode 100644 index 0000000000000000000000000000000000000000..e41791aa60fceb8fd2ab52c5dfe07d43a449c7a2 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/services/task.services.js @@ -0,0 +1,103 @@ +const axios = require('axios'); + +//axios.defaults.baseURL = 'http://192.168.99.100:8008/api'; +axios.defaults.headers.common['Authorization'] = 'Basic dGVzdDp0ZXN0'; + +const TaskService = { + getTaskDetails: async function (taskType, taskId) { + try { + const url = taskType === 'blueprint'? '/api/task_blueprint/': '/api/task_draft/'; + const response = await axios.get(url + taskId); + response.data.predecessors = []; + response.data.successors = []; + return this.getTaskRelations('draft', response.data.id) + .then(relations => { + response.data.predecessors = relations.predecessors; + response.data.successors = relations.successors; + return response.data; + }); + + } catch (error) { + console.error(error); + } + }, + getTaskTemplate: async function(templateId) { + try { + const response = await axios.get('/api/task_template/' + templateId); + return response.data; + } catch (error) { + console.log(error); + } + }, + getTaskTemplates: async function() { + try { + const response = await axios.get('/api/task_template/'); + return response.data.results; + } catch (error) { + console.log(error); + } + }, + getSchedulingUnit: async function(type, id) { + try { + const response = await axios.get('/api/scheduling_unit_draft/' + id); + return response.data; + } catch (error) { + console.error(error); + } + }, + updateTask: async function(type, task) { + try { + const response = await axios.put(('/api/task_draft/' + task.id + "/"), task); + return response.data; + } catch (error) { + console.error(error); + return null; + } + }, + getTaskRelation: async function(type, id) { + try { + const url = type === 'blueprint'? '/api/task_blueprint/': `/api/task_draft/${id}/task_relation_draft/`; + const response = await axios.get(url); + return response.data; + } catch (error) { + console.error(error); + } + }, + getTaskRelations: async function(type, id) { + try { + let relations = {}; + return this.getTaskPredecessors(type, id) + .then( predecessors => { + relations.predecessors = predecessors; + return this.getTaskSuccessors(type, id); + }) + .then( successors => { + relations.successors = successors; + return relations; + }); + } catch (error) { + console.log(error); + } + }, + getTaskPredecessors: async function(type, id) { + try { + const url = type === 'blueprint'? `/api/task_blueprint/${id}/predecessors`: `/api/task_draft/${id}/predecessors/`; + const response = await axios.get(url); + return response.data; + } catch (error) { + console.error(error); + } + }, + getTaskSuccessors: async function(type, id) { + try { + const url = type === 'blueprint'? `/api/task_blueprint/${id}/successors`: `/api/task_draft/${id}/successors/`; + const response = await axios.get(url); + return response.data; + } catch (error) { + console.error(error); + } + } + +} + +export default TaskService; \ No newline at end of file