diff --git a/SAS/TMSS/frontend/tmss_webapp/package.json b/SAS/TMSS/frontend/tmss_webapp/package.json index 969859842dc64dc1c550025c4ae7933bad115bd8..8230388bc6f2c1f16805cf9a25ba4f05f6b6b000 100644 --- a/SAS/TMSS/frontend/tmss_webapp/package.json +++ b/SAS/TMSS/frontend/tmss_webapp/package.json @@ -46,6 +46,7 @@ "react-transition-group": "^2.5.1", "reactstrap": "^8.5.1", "styled-components": "^5.1.1", + "suneditor-react": "^2.14.4", "typescript": "^3.9.5", "yup": "^0.29.1" }, diff --git a/SAS/TMSS/frontend/tmss_webapp/src/layout/_overrides.scss b/SAS/TMSS/frontend/tmss_webapp/src/layout/_overrides.scss index 330a00a4d7520597c039ec2d3cb010075797b93b..4ec1204d72a8ead8c5565e6457231059a9e82108 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/layout/_overrides.scss +++ b/SAS/TMSS/frontend/tmss_webapp/src/layout/_overrides.scss @@ -109,6 +109,34 @@ .hide { display: none !important; } +.grouping { + padding: 0 15px; +} +.grouping fieldset { + border-width: 1px; + border-style: double; + border-color: #ccc; + border-image: initial; + padding: 10px; + width: 100%; +} +.grouping legend { + width: auto; +} + +.comments{ + margin: 90px; + position: absolute; + margin-left: 15px; +} +.qaButton{ + margin-left: 0; + position: relative; + top: 350px; +} +.plots{ + padding-left: 2px; +} diff --git a/SAS/TMSS/frontend/tmss_webapp/src/layout/layout.scss b/SAS/TMSS/frontend/tmss_webapp/src/layout/layout.scss index 5980a6378545ea73371eef791355b27aba58dd6d..e098f79b4c332c0f90d52c99d6ed41fe01215e72 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/layout/layout.scss +++ b/SAS/TMSS/frontend/tmss_webapp/src/layout/layout.scss @@ -1,3 +1,4 @@ @import "./_variables"; @import "./sass/_layout"; -@import "./_overrides"; \ No newline at end of file +@import "./_overrides"; +@import "./sass/stations"; \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_stations.scss b/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_stations.scss new file mode 100644 index 0000000000000000000000000000000000000000..e0c2e01575b7d261bb353d311a22730592c8af06 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_stations.scss @@ -0,0 +1,84 @@ +.grouping { + padding: 0 15px; +} +.grouping fieldset { + border-width: 1px; + border-style: double; + border-color: #ccc; + border-image: initial; + padding: 10px; + width: 100%; +} +.grouping legend { + width: auto; +} +.grouping .selected_stations { + margin-top: 10px; +} +.selected_stations .info { + background-color: transparent !important; + border: none !important; + padding: 0; + width: auto !important; + top: 2px; + span { + font-size: 14px !important; + padding: 0 !important; + } +} +.text-caps { + text-transform: capitalize; +} +.station-container { + padding: 10px; + max-height: 200px; + overflow-y: auto; + label { + display: block; + } +} +.custom-label { + padding-left: 8px !important; +} +.custom-value { + padding-right: 8px !important; +} +.custom-field { + padding-left: 30px !important; +} +.error-message { + font-size: 12px; + color: red; +} +.custom-missingstation-label{ + padding-left: 4px !important; +} + +.customMissingStationLabel{ + padding-left: 22px !important; +} +#missingStation{ + width: 110%; +} +.station_header { + // padding-left: 22px !important; +} +#stationgroup-label{ + padding-left: 5px; +} +.custom-station-wrapper { + position: relative; +} +.custom-remove { + position: absolute; + left: -12px; + background-color: transparent !important; + border: none !important; + padding: 0; + width: auto !important; + top: 2px; + span { + font-size: 14px !important; + padding: 0 !important; + } +} diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/Stations.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/Stations.js new file mode 100644 index 0000000000000000000000000000000000000000..448eeb568f2de0ee34fee6950afff81d6bf5133b --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/Stations.js @@ -0,0 +1,328 @@ + + +import React, { useState, useEffect } from 'react'; +import _ from 'lodash'; +import {MultiSelect} from 'primereact/multiselect'; +import { OverlayPanel } from 'primereact/overlaypanel'; +import {InputText} from 'primereact/inputtext'; +import { Button } from 'primereact/button'; +import UIConstants from '../../utils/ui.constants'; +import ScheduleService from '../../services/schedule.service'; +/* eslint-disable react-hooks/exhaustive-deps */ +/* +const props = { + selectedStations, + stationOptions, + selectedStrategyId, + observStrategies, + customStations +} +*/ + +export default (props) => { + const { tooltipOptions } = UIConstants; + let op; + + const [selectedStations, setSelectedStations] = useState([]); + const [stationOptions, setStationOptions] = useState([]); + const [customStations, setCustomStations] = useState([]); + const [customStationsOptions, setCustomStationsOptions] = useState([]); + const [stations, setStations] = useState([]); + const [missing_StationFieldsErrors, setmissing_StationFieldsErrors] = useState([]); + const [state, setState] = useState({ + Custom: { + stations: [] + } + }); + + useEffect(() => { + if (props.stationGroup && props.stationGroup.length) { + getAllStations(); + } + }, [props.stationGroup]); + + /** + * Fetches all stations + */ + const getAllStations = async () => { + const stationGroup = await ScheduleService.getStationGroup(); + + const promises = []; + stationGroup.forEach(st => { + promises.push(ScheduleService.getStations(st.value)) + }); + Promise.all(promises).then(responses => { + getStationsDetails(stationGroup, responses); + }); + setStationOptions(stationGroup); + }; + + /** + * Cosntruct and set appropriate values to each station by finding station from station_group + * like error, missing fields, etc. + * Also will construct stations for custom group by merging all the stations + */ + const getStationsDetails = (stations, responses) => { + let stationState = { + Custom: { + stations: [] + } + }; + let custom_Stations = []; + setStationOptions(stations); + let selected_Stations = []; + responses.forEach((response, index) => { + const StationName = stations[index].value; + const missing_StationFields = props.stationGroup.find(i => { + if (i.stations.length === response.stations.length && i.stations[0] === response.stations[0]) { + i.stationType = StationName; + return true; + } + return false; + }); + // Missing fields present then it matched with station type otherwise its a custom... + if (missing_StationFields) { + selected_Stations = [...selected_Stations, StationName]; + } + stationState ={ + ...stationState, + [StationName]: { + stations: response.stations, + missing_StationFields: missing_StationFields ? missing_StationFields.max_nr_missing : '' + }, + Custom: { + stations: [...stationState['Custom'].stations, ...response.stations], + }, + }; + // Setting in Set to avoid duplicate, otherwise have to loop multiple times. + custom_Stations = new Set([...custom_Stations, ...response.stations]); + }); + // Find the custom one + const custom_stations = props.stationGroup.filter(i => !i.stationType); + stationState = { + ...stationState + }; + setCustomStations(custom_stations); + setSelectedStationGroup([...selected_Stations]); + setState(stationState); + let custom_stations_options = Array.from(custom_Stations); + // Changing array of sting into array of objects to support filter in primereact multiselect + custom_stations_options = custom_stations_options.map(i => ({ value: i })); + setCustomStationsOptions(custom_stations_options); + if (props.onUpdateStations) { + updateSchedulingComp(stationState, [...selected_Stations], missing_StationFieldsErrors, customStations); + } + }; + + /** + * Method will trigger on change of station group multiselect. + * Same timw will update the parent component also + * *param value* -> array of string + */ + const setSelectedStationGroup = (value) => { + setSelectedStations(value); + if (props.onUpdateStations) { + updateSchedulingComp(state, value, missing_StationFieldsErrors, customStations); + } + }; + + /** + * Method will trigger on change of custom station dropdown. + */ + const onChangeCustomSelectedStations = (value, index) => { + const custom_selected_options = [...customStations]; + custom_selected_options[index].stations = value; + if (value < custom_selected_options[index].max_nr_missing || !value.length) { + custom_selected_options[index].error = true; + } else { + custom_selected_options[index].error = false; + } + setCustomStations(custom_selected_options); + updateSchedulingComp(state, selectedStations, missing_StationFieldsErrors, custom_selected_options); + }; + + /** + * Method will trigger on click of info icon to show overlay + * param event -> htmlevent object + * param key -> string - selected station + */ + const showStations = (event, key) => { + op.toggle(event); + setStations((state[key] && state[key].stations ) || []); + }; + + /** + * Method will trigger on change of missing fields. + * Will store all fields error in array of string to enable/disable save button. + */ + const setNoOfmissing_StationFields = (key, value) => { + let cpmissing_StationFieldsErrors = [...missing_StationFieldsErrors]; + if (value > state[key].stations.length || value === '') { + if (!cpmissing_StationFieldsErrors.includes(key)) { + cpmissing_StationFieldsErrors.push(key); + } + } else { + cpmissing_StationFieldsErrors = cpmissing_StationFieldsErrors.filter(i => i !== key); + } + setmissing_StationFieldsErrors(cpmissing_StationFieldsErrors); + const stationState = { + ...state, + [key]: { + ...state[key], + missing_StationFields: value, + error: value > state[key].stations.length || value === '' + }, + }; + setState(stationState); + if (props.onUpdateStations) { + updateSchedulingComp(stationState, selectedStations, cpmissing_StationFieldsErrors, customStations); + } + } + + /** + * Method will trigger onchange of missing fields in custom + * @param {*} value string + * @param {*} index number + */ + const setMissingFieldsForCustom = (value, index) => { + const custom_selected_options = [...customStations]; + if (value > custom_selected_options[index].stations.length || value === '' || !custom_selected_options[index].stations.length) { + custom_selected_options[index].error = true; + } else { + custom_selected_options[index].error = false; + } + custom_selected_options[index].touched = true; + custom_selected_options[index].max_nr_missing = value; + setCustomStations(custom_selected_options); + updateSchedulingComp(state, selectedStations, missing_StationFieldsErrors, custom_selected_options); + }; + + /** + * Method will get trigger on click of add custom + */ + const addCustom = () => { + const custom_selected_options = [...customStations]; + custom_selected_options.push({ + stations: [], + max_nr_missing: 0, + error: true + }); + setCustomStations(custom_selected_options); + updateSchedulingComp(state, selectedStations, missing_StationFieldsErrors, custom_selected_options); + }; + + const updateSchedulingComp = (param_State, param_SelectedStations, param_missing_StationFieldsErrors, param_Custom_selected_options) => { + const isError = param_missing_StationFieldsErrors.length || param_Custom_selected_options.filter(i => i.error).length; + debugger + props.onUpdateStations(param_State, param_SelectedStations, isError, param_Custom_selected_options); + }; + /** + * Method to remove the custom stations + * @param {*} index number + */ + const removeCustomStations = (index) => { + const custom_selected_options = [...customStations]; + custom_selected_options.splice(index,1); + setCustomStations(custom_selected_options); + updateSchedulingComp(state, selectedStations, missing_StationFieldsErrors, custom_selected_options); + }; + + return ( + <div className="p-field p-grid grouping p-fluid"> + <fieldset> + <legend> + <label>Stations<span style={{color:'red'}}>*</span></label> + </legend> + {!props.view && <div className="col-sm-12 p-field p-grid" data-testid="stations"> + <div className="col-md-6 d-flex"> + <label htmlFor="stationgroup" className="col-sm-6 station_header">Station Groups</label> + <div className="col-sm-6"> + <MultiSelect data-testid="stations" id="stations" optionLabel="value" optionValue="value" filter={true} + tooltip="Select Stations" tooltipOptions={tooltipOptions} + value={selectedStations} + options={stationOptions} + placeholder="Select Stations" + onChange={(e) => setSelectedStationGroup(e.value)} + /> + </div> + </div> + <div className="add-custom"> + <Button onClick={addCustom} label="Add Custom" icon="pi pi-plus" disabled={!stationOptions.length}/> + </div> + </div>} + {selectedStations.length ? <div className="col-sm-12 selected_stations" data-testid="selected_stations"> + {<div className="col-sm-12"><label style={{paddingLeft: '8px'}}>Maximum number of stations that can be missed in the selected groups</label></div>} + <div className="col-sm-12 p-0 d-flex flex-wrap"> + {selectedStations.map(i => ( + <div className="p-field p-grid col-md-6" key={i}> + <label className="col-sm-6 text-caps"> + {i} + <Button icon="pi pi-info-circle" className="p-button-rounded p-button-secondary p-button-text info" onClick={(e) => showStations(e, i)} /> + </label> + <div className="col-sm-6"> + <InputText id="missingstation" data-testid="name" + className={(state[i] && state[i].error) ?'input-error':''} + tooltip="Max No. of Missing Stations" tooltipOptions={tooltipOptions} maxLength="128" + placeholder="Max No. of Missing Stations" + value={state[i] ? state[i].missing_StationFields : ''} + disabled={props.view} + onChange={(e) => setNoOfmissing_StationFields(i, e.target.value)}/> + {(state[i] && state[i].error) && <span className="error-message">{state[i].missing_StationFields ? `Max. no of missing stations is ${state[i] ? state[i].stations.length : 0}` : 'Max. no of missing stations required'}</span>} + </div> + </div> + ))} + {customStations.map((stat, index) => ( + <div className="p-field p-grid col-md-12 custom-station-wrapper" key={index}> + {!props.view && <Button icon="pi pi-trash" className="p-button-secondary p-button-text custom-remove" onClick={() => removeCustomStations(index)} />} + + <div className="col-md-6 p-field p-grid"> + <label className="col-sm-6 text-caps custom-label"> + Custom {index + 1} + </label> + <div className="col-sm-6 pr-8 custom-value"> + <MultiSelect data-testid="custom_stations" id="custom_stations" filter + tooltip="Select Stations" tooltipOptions={tooltipOptions} + value={stat.stations} + options={customStationsOptions} + placeholder="Select Stations" + disabled={props.view} + optionLabel="value" + optionValue="value" + onChange={(e) => onChangeCustomSelectedStations(e.value, index)} + /> + </div> + </div> + <div className="col-md-6 p-field p-grid"> + <label className="col-sm-6 customMissingStationLabel"> + Maximum No. Of Missing Stations + </label> + <div className="col-sm-6 pr-8 custom-field"> + <InputText id="missingStation" data-testid="name" + className={(stat.error && stat.touched) ?'input-error':''} + tooltip="Max Number of Missing Stations" tooltipOptions={tooltipOptions} + placeholder="Max Number of Missing Stations" + value={stat.max_nr_missing} + disabled={props.view} + onChange={(e) => setMissingFieldsForCustom(e.target.value, index)}/> + {(stat.error && stat.touched) && <span className="error-message">{stat.max_nr_missing ? `Max. no of missing stations is ${stat.stations.length}` : 'Max. no of missing stations required'}</span>} + {/* {props.view && + <span className="info">Max No. of Missing Stations</span>} */} + + </div> + </div> + </div> + ))} + </div> + + </div> : null} + <OverlayPanel ref={(el) => op = el} dismissable style={{width: '450px'}}> + <div className="station-container"> + {(stations || []).map(i => ( + <label>{i}</label> + ))} + </div> + </OverlayPanel> + </fieldset> + </div> + ); +}; \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/ViewSchedulingUnit.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/ViewSchedulingUnit.js index c9145d81439dfe3417fbba1765138d492bd2321f..22914ed8871b0fe0ed58a456c2aa4baa4021b5bd 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/ViewSchedulingUnit.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/ViewSchedulingUnit.js @@ -12,6 +12,7 @@ import moment from 'moment'; import SchedulingConstraint from './Scheduling.Constraints'; import { Dialog } from 'primereact/dialog'; import TaskStatusLogs from '../Task/state_logs'; +import Stations from './Stations'; class ViewSchedulingUnit extends Component{ constructor(props){ @@ -24,7 +25,7 @@ class ViewSchedulingUnit extends Component{ paths: [{ "View": "/task", }], - + missingStationFieldsErrors: [], defaultcolumns: [ { status_logs: "Status Logs", tasktype:{ @@ -70,16 +71,19 @@ class ViewSchedulingUnit extends Component{ "Relative Start Time (HH:mm:ss)": "filter-input-75", "Relative End Time (HH:mm:ss)": "filter-input-75", "Status":"filter-input-100" - }] + }], + stationGroup: [] } this.actions = [ {icon: 'fa-window-close',title:'Click to Close Scheduling Unit View', link: this.props.history.goBack} ]; + this.stations = []; this.constraintTemplates = []; if (this.props.match.params.type === 'draft') { this.actions.unshift({icon: 'fa-edit', title: 'Click to edit', props : { pathname:`/schedulingunit/edit/${ this.props.match.params.id}`} }); } else { + this.actions.unshift({icon: 'fa-sitemap',title :'View Workflow',props :{pathname:`/schedulingunit/${this.props.match.params.id}/workflow`}}); this.actions.unshift({icon: 'fa-lock', title: 'Cannot edit blueprint'}); } if (this.props.match.params.id) { @@ -90,7 +94,7 @@ class ViewSchedulingUnit extends Component{ } } - componentDidMount(){ + async componentDidMount(){ let schedule_id = this.state.scheduleunitId; let schedule_type = this.state.scheduleunitType; if (schedule_type && schedule_id) { @@ -101,6 +105,8 @@ class ViewSchedulingUnit extends Component{ </button> ); }; + this.stations = await ScheduleService.getStationGroup(); + this.setState({stationOptions: this.stations}); this.getScheduleUnit(schedule_type, schedule_id) .then(schedulingUnit =>{ if (schedulingUnit) { @@ -114,11 +120,13 @@ class ViewSchedulingUnit extends Component{ task.status_logs = task.tasktype === "Blueprint"?subtaskComponent(task):""; return task; }); + const targetObservation = tasks.find(task => task.name === 'Target Observation'); this.setState({ scheduleunit : schedulingUnit, schedule_unit_task : tasks, isLoading: false, - }); + stationGroup: targetObservation.specifications_doc.station_groups + }, this.getAllStations); }); } else { this.setState({ @@ -128,7 +136,7 @@ class ViewSchedulingUnit extends Component{ }); } } - + getScheduleUnitTasks(type, scheduleunit){ if(type === 'draft') return ScheduleService.getTasksBySchedulingUnit(scheduleunit.id); @@ -205,6 +213,13 @@ class ViewSchedulingUnit extends Component{ </div> </> } + + {<Stations + stationGroup={this.state.stationGroup} + targetObservation={this.state.targetObservation} + view + />} + {this.state.scheduleunit && this.state.scheduleunit.scheduling_constraints_doc && <SchedulingConstraint disable constraintTemplate={this.state.constraintSchema} initValue={this.state.scheduleunit.scheduling_constraints_doc} />} <div> <h3>Tasks Details</h3> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/create.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/create.js index 8f2b1dd4dd1408d676e2d18e849caa8ae9a18aea..5116438d25a47eda68eb07a38d525585b93ebad7 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/create.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/create.js @@ -20,6 +20,8 @@ import TaskService from '../../services/task.service'; import UIConstants from '../../utils/ui.constants'; import PageHeader from '../../layout/components/PageHeader'; import SchedulingConstraint from './Scheduling.Constraints'; +import Stations from './Stations'; + /** * Component to create a new SchedulingUnit from Observation strategy template */ @@ -33,6 +35,10 @@ export class SchedulingUnitCreate extends Component { redirect: null, // URL to redirect errors: [], // Form Validation errors schedulingSets: [], // Scheduling set of the selected project + missing_StationFieldsErrors: [], // Validation for max no.of missing station + stationOptions: [], + stationGroup: [], + customSelectedStations: [], // custom stations schedulingUnit: { project: (props.match?props.match.params.project:null) || null, }, @@ -42,7 +48,7 @@ export class SchedulingUnitCreate extends Component { constraintSchema:null, validEditor: false, // For JSON editor validation validFields: {}, // For Form Validation - } + }; this.projects = []; // All projects to load project dropdown this.schedulingSets = []; // All scheduling sets to be filtered for project this.observStrategies = []; // All Observing strategy templates @@ -77,14 +83,16 @@ export class SchedulingUnitCreate extends Component { ScheduleService.getSchedulingSets(), ScheduleService.getObservationStrategies(), TaskService.getTaskTemplates(), - ScheduleService.getSchedulingConstraintTemplates()] + ScheduleService.getSchedulingConstraintTemplates(), + ScheduleService.getStationGroup()] Promise.all(promises).then(responses => { this.projects = responses[0]; this.schedulingSets = responses[1]; this.observStrategies = responses[2]; this.taskTemplates = responses[3]; this.constraintTemplates = responses[4]; - // Setting first value as constraint template + this.stations = responses[5]; + // Setting first value as constraint template this.constraintStrategy(this.constraintTemplates[0]); if (this.state.schedulingUnit.project) { const projectSchedSets = _.filter(this.schedulingSets, {'project_id': this.state.schedulingUnit.project}); @@ -105,7 +113,7 @@ export class SchedulingUnitCreate extends Component { schedulingUnit.project = projectName; this.setState({schedulingUnit: schedulingUnit, schedulingSets: projectSchedSets, validForm: this.validateForm('project')}); } - + /** * Function called when observation strategy template is changed. * It generates the JSON schema for JSON editor and defult vales for the parameters to be captured @@ -113,6 +121,8 @@ export class SchedulingUnitCreate extends Component { */ async changeStrategy (strategyId) { const observStrategy = _.find(this.observStrategies, {'id': strategyId}); + const station_group = observStrategy.template.tasks['Target Observation'].specifications_doc.station_groups; + this.setState({ stationGroup: station_group }); const tasks = observStrategy.template.tasks; let paramsOutput = {}; let schema = { type: 'object', additionalProperties: false, @@ -278,7 +288,7 @@ export class SchedulingUnitCreate extends Component { if (Object.keys(validFields).length === Object.keys(this.formRules).length) { validForm = true; } - return validForm; + return validForm && !this.state.missingStationFieldsErrors; } /** @@ -308,9 +318,25 @@ export class SchedulingUnitCreate extends Component { } } } - /* for (let type in constStrategy.sky.transit_offset) { - constStrategy.sky.transit_offset[type] = constStrategy.sky.transit_offset[type] * 60; - }*/ + //station + const station_groups = []; + (this.state.selectedStations || []).forEach(key => { + let station_group = {}; + const stations = this.state[key] ? this.state[key].stations : []; + const max_nr_missing = parseInt(this.state[key] ? this.state[key].missing_StationFields : 0); + station_group = { + stations, + max_nr_missing + }; + station_groups.push(station_group); + }); + + this.state.customSelectedStations.forEach(station => { + station_groups.push({ + stations: station.stations, + max_nr_missing:parseInt(station.max_nr_missing) + }); + }); UnitConversion.degreeToRadians(constStrategy.sky); @@ -320,7 +346,7 @@ export class SchedulingUnitCreate extends Component { $refs.set(observStrategy.template.parameters[index]['refs'][0], this.state.paramsOutput['param_' + index]); }); const const_strategy = {scheduling_constraints_doc: constStrategy, id: this.constraintTemplates[0].id, constraint: this.constraintTemplates[0]}; - const schedulingUnit = await ScheduleService.saveSUDraftFromObservStrategy(observStrategy, this.state.schedulingUnit, const_strategy); + const schedulingUnit = await ScheduleService.saveSUDraftFromObservStrategy(observStrategy, this.state.schedulingUnit, const_strategy, station_groups); if (schedulingUnit) { // this.growl.show({severity: 'success', summary: 'Success', detail: 'Scheduling Unit and tasks created successfully!'}); const dialog = {header: 'Success', detail: 'Scheduling Unit and Tasks are created successfully. Do you want to create another Scheduling Unit?'}; @@ -373,6 +399,21 @@ export class SchedulingUnitCreate extends Component { this.state.editorFunction(); } + onUpdateStations = (state, selectedStations, missing_StationFieldsErrors, customSelectedStations) => { + this.setState({ + ...state, + selectedStations, + missing_StationFieldsErrors, + customSelectedStations + + }, () => { + this.setState({ + validForm: this.validateForm() + }); + + }); + }; + render() { if (this.state.redirect) { return <Redirect to={ {pathname: this.state.redirect} }></Redirect> @@ -475,8 +516,11 @@ export class SchedulingUnitCreate extends Component { </div> </div> - - </div> + <Stations + stationGroup={this.state.stationGroup} + onUpdateStations={this.onUpdateStations.bind(this)} + /> + </div> {this.state.constraintSchema && <div className="p-fluid"> <div className="p-grid"> <div className="p-col-12"> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/edit.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/edit.js index f3205da562ad3e81bf3654e0bd59bf30cd8fbd5d..d3060f0f30d53126e4b4d95596323c40e99a879f 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/edit.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/edit.js @@ -14,6 +14,7 @@ import AppLoader from '../../layout/components/AppLoader'; import PageHeader from '../../layout/components/PageHeader'; import Jeditor from '../../components/JSONEditor/JEditor'; import UnitConversion from '../../utils/unit.converter'; +import Stations from './Stations'; import ProjectService from '../../services/project.service'; import ScheduleService from '../../services/schedule.service'; @@ -40,7 +41,10 @@ export class EditSchedulingUnit extends Component { constraintSchema:null, validEditor: false, // For JSON editor validation validFields: {}, // For Form Validation - observStrategyVisible: false + observStrategyVisible: false, + missingStationFieldsErrors: [], // Validation for max no.of missing station + stationGroup: [], + customSelectedStations: [] // Custom Stations } this.projects = []; // All projects to load project dropdown this.schedulingSets = []; // All scheduling sets to be filtered for project @@ -142,7 +146,8 @@ export class EditSchedulingUnit extends Component { TaskService.getTaskTemplates(), ScheduleService.getSchedulingUnitDraftById(this.props.match.params.id), ScheduleService.getTasksDraftBySchedulingUnitId(this.props.match.params.id), - ScheduleService.getSchedulingConstraintTemplates() + ScheduleService.getSchedulingConstraintTemplates(), + ScheduleService.getStationGroup() ]; Promise.all(promises).then(responses => { this.projects = responses[0]; @@ -150,11 +155,16 @@ export class EditSchedulingUnit extends Component { this.observStrategies = responses[2]; this.taskTemplates = responses[3]; this.constraintTemplates = responses[6]; + this.stations = responses[7]; responses[4].project = this.schedulingSets.find(i => i.id === responses[4].scheduling_set_id).project_id; this.setState({ schedulingUnit: responses[4], taskDrafts: responses[5].data.results, observStrategyVisible: responses[4].observation_strategy_template_id?true:false }); if (responses[4].observation_strategy_template_id) { this.changeStrategy(responses[4].observation_strategy_template_id); + const targetObservation = responses[5].data.results.find(task => task.name === 'Target Observation'); + this.setState({ + stationGroup: targetObservation.specifications_doc.station_groups + }); } if (this.state.schedulingUnit.project) { const projectSchedSets = _.filter(this.schedulingSets, {'project_id': this.state.schedulingUnit.project}); @@ -261,7 +271,7 @@ export class EditSchedulingUnit extends Component { if (Object.keys(validFields).length === Object.keys(this.formRules).length) { validForm = true; } - return validForm; + return validForm && !this.state.missingStationFieldsErrors; } /** @@ -305,7 +315,26 @@ export class EditSchedulingUnit extends Component { }); const schUnit = { ...this.state.schedulingUnit }; schUnit.scheduling_constraints_doc = constStrategy; - const schedulingUnit = await ScheduleService.updateSUDraftFromObservStrategy(observStrategy,schUnit,this.state.taskDrafts, this.state.tasksToUpdate); + //station + const station_groups = []; + (this.state.selectedStations || []).forEach(key => { + let station_group = {}; + const stations = this.state[key] ? this.state[key].stations : []; + const max_nr_missing = parseInt(this.state[key] ? this.state[key].missing_StationFields : 0); + station_group = { + stations, + max_nr_missing + }; + station_groups.push(station_group); + }); + this.state.customSelectedStations.forEach(station => { + station_groups.push({ + stations: station.stations, + max_nr_missing: parseInt(station.max_nr_missing) + }); + }); + + const schedulingUnit = await ScheduleService.updateSUDraftFromObservStrategy(observStrategy,schUnit,this.state.taskDrafts, this.state.tasksToUpdate, station_groups); if (schedulingUnit) { // this.growl.show({severity: 'success', summary: 'Success', detail: 'Scheduling Unit and tasks edited successfully!'}); this.props.history.push({ @@ -318,7 +347,8 @@ export class EditSchedulingUnit extends Component { this.growl.show({severity: 'error', summary: 'Error Occured', detail: 'Template Missing.'}); } } - + + /** * Cancel SU creation and redirect */ @@ -330,6 +360,19 @@ export class EditSchedulingUnit extends Component { this.setState({ constraintSchema: schema, initValue: initValue}); } + onUpdateStations = (state, selectedStations, missingStationFieldsErrors, customSelectedStations) => { + this.setState({ + ...state, + selectedStations, + missingStationFieldsErrors, + customSelectedStations + }, () => { + this.setState({ + validForm: this.validateForm() + }); + }); + }; + render() { if (this.state.redirect) { return <Redirect to={ {pathname: this.state.redirect} }></Redirect> @@ -430,6 +473,14 @@ export class EditSchedulingUnit extends Component { </div> </div> </div> + + + <Stations + stationGroup={this.state.stationGroup} + onUpdateStations={this.onUpdateStations.bind(this)} + /> + + {this.state.constraintSchema && <div className="p-fluid"> <div className="p-grid"> <div className="p-col-12"> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/QAreporting.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/QAreporting.js new file mode 100644 index 0000000000000000000000000000000000000000..5ffe01ea22288a4b72aa794753358c400d8862c6 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/QAreporting.js @@ -0,0 +1,97 @@ +import React, { Component } from 'react'; +import PageHeader from '../../layout/components/PageHeader'; +import {Growl} from 'primereact/components/growl/Growl'; +import { Button } from 'primereact/button'; +// import AppLoader from '../../layout/components/AppLoader'; +import SunEditor from 'suneditor-react'; +import 'suneditor/dist/css/suneditor.min.css'; // Import Sun Editor's CSS File +import {Dropdown} from 'primereact/dropdown'; +// import {InputText} from 'primereact/inputtext'; +import ScheduleService from '../../services/schedule.service'; +import { Link } from 'react-router-dom'; + +class QAreporting extends Component{ + + constructor(props) { + super(props); + this.state={}; + } + + componentDidMount() { + ScheduleService.getSchedulingUnitBlueprintById(this.props.match.params.id) + .then(schedulingUnit => { + this.setState({schedulingUnit: schedulingUnit}); + }) + } + + render() { + return ( + <React.Fragment> + <Growl ref={(el) => this.growl = el} /> + <PageHeader location={this.props.location} title={'QA Reporting (TO)'} actions={[{icon:'fa-window-close',link:this.props.history.goBack, title:'Click to Close Workflow', props:{ pathname: '/schedulingunit/view'}}]}/> + {this.state.schedulingUnit && + <> + <div> + <div className="p-fluid"> + <div className="p-field p-grid"> + <label htmlFor="suStatus" className="col-lg-2 col-md-2 col-sm-12">Scheduling Unit</label> + <div className="col-lg-3 col-md-3 col-sm-12"> + <Link to={ { pathname:`/schedulingunit/view/blueprint/${this.state.schedulingUnit.id}`}}>{this.state.schedulingUnit.name}</Link> + </div> + <div className="col-lg-1 col-md-1 col-sm-12"></div> + <label htmlFor="suStatus" className="col-lg-2 col-md-2 col-sm-12">Scheduling Unit Status</label> + <div className="col-lg-3 col-md-3 col-sm-12"> + {/* <InputText id="suStatus" data-testid="name" disabled + value={this.state.schedulingUnit.status}/> */} + <span>{this.state.schedulingUnit.status}</span> + </div> + </div> + <div className="p-field p-grid"> + <label htmlFor="assignTo" className="col-lg-2 col-md-2 col-sm-12">Assign To </label> + <div className="col-lg-3 col-md-3 col-sm-12" data-testid="assignTo" > + <Dropdown inputId="projCat" optionLabel="value" optionValue="value" + options={[{value: 'User 1'},{value: 'User 2'},{value: 'User 3'}]} + placeholder="Assign To" /> + </div> + <div className="col-lg-1 col-md-1 col-sm-12"></div> + <label htmlFor="viewPlots" className="col-lg-2 col-md-2 col-sm-12">View Plots</label> + <div className="col-lg-3 col-md-3 col-sm-12" style={{paddingLeft:'2px'}}> + <label className="col-sm-10 " > + <a href="https://proxy.lofar.eu/inspect/HTML/" target="_blank">Inspection plots</a> + </label> + <label className="col-sm-10 "> + <a href="https://proxy.lofar.eu/qa" target="_blank">Adder plots</a> + </label> + <label className="col-sm-10 "> + <a href=" https://proxy.lofar.eu/lofmonitor/" target="_blank">Station Monitor</a> + </label> + </div> + </div> + <div className="p-grid" style={{padding: '10px'}}> + <label htmlFor="comments" >Comments</label> + <div className="col-lg-12 col-md-12 col-sm-12"></div> + <SunEditor height="250" enableToolbar={true} + setOptions={{ + buttonList: [ + ['undo', 'redo', 'bold', 'underline', 'fontColor', 'table', 'link', 'image', 'video','italic', 'strike', 'subscript', + 'superscript','outdent', 'indent','fullScreen', 'showBlocks', 'codeView','preview', 'print','removeFormat'] + ] + }} /> + </div> + </div> + <div className="p-grid" style={{marginTop: '20px'}}> + <div className="p-col-1"> + <Button label="Save" className="p-button-primary" icon="pi pi-check" /> + </div> + <div className="p-col-1"> + <Button label="Cancel" className="p-button-danger" icon="pi pi-times" /> + </div> + </div> + + </div> + </> + } + </React.Fragment> + )}; +} +export default QAreporting; \ 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 d226ed315f5bd573ccf6d06f7bdf45c2592223b5..a5c43e081e64a4c5801d01d50d1ef4ad9d4b35d7 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/index.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/index.js @@ -15,7 +15,8 @@ import SchedulingUnitCreate from './Scheduling/create'; import EditSchedulingUnit from './Scheduling/edit'; import { CycleList, CycleCreate, CycleView, CycleEdit } from './Cycle'; import {TimelineView, WeekTimelineView} from './Timeline'; -import SchedulingSetCreate from './Scheduling/create.scheduleset' +import SchedulingSetCreate from './Scheduling/create.scheduleset'; +import QAreporting from './Workflow/QAreporting'; export const routes = [ { @@ -150,7 +151,13 @@ export const routes = [ path: "/schedulingset/schedulingunit/create", component: SchedulingSetCreate, name: 'Scheduling Set Add' - } + }, + { + path: "/schedulingunit/:id/workflow", + component: QAreporting, + name: 'QA Reporting (TO)', + title: 'QA Reporting (TO)' + } ]; export const RoutedContent = () => { 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 dbd8f51805d1772f2234904d34787d1ed23cf5ca..6c7570bb5988cac32bb82e12a79eb8a48940ca3f 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/services/schedule.service.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/services/schedule.service.js @@ -115,7 +115,7 @@ const ScheduleService = { } scheduletask['created_at'] = moment(task['created_at'], moment.ISO_8601).format("YYYY-MMM-DD HH:mm:ss"); scheduletask['updated_at'] = moment(task['updated_at'], moment.ISO_8601).format("YYYY-MMM-DD HH:mm:ss"); - + scheduletask['specifications_doc'] = task['specifications_doc']; scheduletask.duration = moment.utc((scheduletask.duration || 0)*1000).format('HH:mm:ss'); scheduletask.relative_start_time = moment.utc(scheduletask.relative_start_time*1000).format('HH:mm:ss'); scheduletask.relative_stop_time = moment.utc(scheduletask.relative_stop_time*1000).format('HH:mm:ss'); @@ -228,7 +228,7 @@ const ScheduleService = { return null; }; }, - saveSUDraftFromObservStrategy: async function(observStrategy, schedulingUnit, constraint) { + saveSUDraftFromObservStrategy: async function(observStrategy, schedulingUnit, constraint,station_groups) { try { // Create the scheduling unit draft with observation strategy and scheduling set const url = `/api/scheduling_unit_observing_strategy_template/${observStrategy.id}/create_scheduling_unit/?scheduling_set_id=${schedulingUnit.scheduling_set_id}&name=${schedulingUnit.name}&description=${schedulingUnit.description}` @@ -237,12 +237,10 @@ const ScheduleService = { if (schedulingUnit && schedulingUnit.id) { // Update the newly created SU draft requirement_doc with captured parameter values schedulingUnit.requirements_doc = observStrategy.template; - if(constraint){ - schedulingUnit.scheduling_constraints_doc = constraint.scheduling_constraints_doc; - schedulingUnit.scheduling_constraints_template_id = constraint.id; - schedulingUnit.scheduling_constraints_template = constraint.constraint.url; - } - + schedulingUnit.requirements_doc.tasks['Target Observation'].specifications_doc.station_groups = station_groups; + schedulingUnit.scheduling_constraints_doc = constraint.scheduling_constraints_doc; + schedulingUnit.scheduling_constraints_template_id = constraint.id; + schedulingUnit.scheduling_constraints_template = constraint.constraint.url; delete schedulingUnit['duration']; schedulingUnit = await this.updateSchedulingUnitDraft(schedulingUnit); if (!schedulingUnit || !schedulingUnit.id) { @@ -261,13 +259,17 @@ const ScheduleService = { }; }, - updateSUDraftFromObservStrategy: async function(observStrategy,schedulingUnit,tasks,tasksToUpdate) { + updateSUDraftFromObservStrategy: async function(observStrategy,schedulingUnit,tasks,tasksToUpdate,station_groups) { try { delete schedulingUnit['duration']; + schedulingUnit = await this.updateSchedulingUnitDraft(schedulingUnit); for (const taskToUpdate in tasksToUpdate) { let task = tasks.find(task => { return task.name === taskToUpdate}); task.specifications_doc = observStrategy.template.tasks[taskToUpdate].specifications_doc; + if (task.name === 'Target Observation') { + task.specifications_doc.station_groups = station_groups; + } delete task['duration']; delete task['relative_start_time']; delete task['relative_stop_time']; @@ -344,6 +346,36 @@ const ScheduleService = { console.error('[project.services.getSchedulingUnitBySet]',error); } }, + getStationGroup: async function() { + try { + // const response = await axios.get('/api/station_type/'); + // return response.data.results; + return [{ + value: 'Dutch' + },{ + value: 'International' + },{ + value: 'Core' + },{ + value: 'Remote' + },{ + value: 'Superterp' + }] + } catch(error) { + console.error(error); + return []; + }; + }, + getStations: async function(e) { + try { + // const response = await axios.get('/api/station_groups/stations/1/dutch'); + const response = await axios.get(`/api/station_groups/stations/1/${e}`); + return response.data; + } catch(error) { + console.error(error); + return []; + } + }, getProjectList: async function() { try { const response = await axios.get('/api/project/'); @@ -354,5 +386,4 @@ const ScheduleService = { } } - export default ScheduleService; \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/services/workflow.service.js b/SAS/TMSS/frontend/tmss_webapp/src/services/workflow.service.js new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/SAS/TMSS/services/tmss_postgres_listener/bin/tmss_postgres_listener_service b/SAS/TMSS/services/tmss_postgres_listener/bin/tmss_postgres_listener_service old mode 100755 new mode 100644 diff --git a/SAS/TMSS/services/tmss_postgres_listener/test/t_tmss_postgres_listener_service.py b/SAS/TMSS/services/tmss_postgres_listener/test/t_tmss_postgres_listener_service.py old mode 100755 new mode 100644 diff --git a/SAS/TMSS/services/tmss_postgres_listener/test/t_tmss_postgres_listener_service.run b/SAS/TMSS/services/tmss_postgres_listener/test/t_tmss_postgres_listener_service.run old mode 100755 new mode 100644 diff --git a/SAS/TMSS/services/tmss_postgres_listener/test/t_tmss_postgres_listener_service.sh b/SAS/TMSS/services/tmss_postgres_listener/test/t_tmss_postgres_listener_service.sh old mode 100755 new mode 100644