-
Reinder Kraaij authoredReinder Kraaij authored
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
create.js 63.00 KiB
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { Redirect } from 'react-router-dom';
import _ from 'lodash';
import $RefParser from "@apidevtools/json-schema-ref-parser";
import moment from 'moment';
import { publish } from '../../App';
import { Checkbox } from 'primereact/checkbox';
import { InputText } from 'primereact/inputtext';
import { InputTextarea } from 'primereact/inputtextarea';
import { Dropdown } from 'primereact/dropdown';
import { Toast } from 'primereact/toast';
import AppLoader from '../../layout/components/AppLoader';
import Jeditor from '../../components/JSONEditor/JEditor';
import UnitConversion from '../../utils/unit.converter';
import UnitConverter from '../../utils/unit.converter';
import ProjectService from '../../services/project.service';
import ScheduleService from '../../services/schedule.service';
import TaskService from '../../services/task.service';
import UIConstants from '../../utils/ui.constants';
import ParserUtility from '../../utils/parser.utility';
import PageHeader from '../../layout/components/PageHeader';
import SchedulingConstraint from './Scheduling.Constraints';
import Stations from './Stations';
import { CustomDialog } from '../../layout/components/CustomDialog';
import SchedulingSet from './schedulingset.create';
import UtilService from '../../services/util.service';
import AuthUtil from '../../utils/auth.util';
import { appGrowl, setAppGrowl } from '../../layout/components/AppGrowl';
import FormActionbar from '../../components/FormActionbar';
import { getConstraintTemplate, getTaskTemplateForTask } from "./create.helper";
import ObservationStrategySelector, {
queryMatchingObservationStrategyTemplates
} from "./components/ObservationStrategySelector";
import ProjectScheduleSetSelector from "./components/ProjectScheduleSetSelector";
/**
* Component to create a new SchedulingUnit from Observation strategy template
*/
export class SchedulingUnitCreate extends Component {
constructor(props) {
super(props);
this.state = {
observStrategyFilters: {
states: ['active'],
purpose: []
},
constraintValidEditor: false,
validForm: false,
validConstraints: false,
validSpecification: false,
userrole: {},
selectedProject: {},
showAddSet: false,
showDialog: false,
isDirty: false,
isLoading: true, // Flag for loading spinner
dialog: { header: '', detail: '' }, // Dialog properties
touched: {},
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: [],
stationGroups: {},
customSelectedStations: [], // custom stations
selectedStationsOfTask: {},
missingStationsofTask: {},
missingStnErorsofTask: {},
customStationsOfTask: {},
stationsCollapsed: false,
schedulingUnit: {
name: '',
description: '',
project: (props.match ? props.match.params.project : null) || null,
rank: 0.0001,
},
projectDisabled: (props.match ? (props.match.params.project ? true : false) : false), // Disable project selection if
observStrategy: {}, // Selected strategy to create SU
paramsSchema: null, // JSON Schema to be generated from strategy template to pass to JSOn editor
constraintSchema: null,
validEditor: false, // For JSON editor validation
validFields: {}, // For Form Validation
newSet: null,
observationIdSet: [],
createAnother: false,
multiCreateCount: 1
};
this.defaultConstraints = {}
this.taskFilters = []; // To get Short_Description details
this.priorityQueueTypes = [];
this.projects = []; // All projects to load project dropdown
this.taskTemplates = []; // All task templates to be filtered based on tasks in selected strategy template
this.tooltipOptions = UIConstants.tooltipOptions;
this.constraintTemplates = [];
this.myRoles = {};
this.nameInput = React.createRef(); // Ref to Name field for auto focus
this.formRules = { // Form validation rules
name: { required: true, message: "Name can not be empty" },
description: { required: true, message: "Description can not be empty" },
project: { required: true, message: "Select project to get Scheduling Sets" },
scheduling_set_id: { required: true, message: "Select the Scheduling Set" },
};
this.isDebuggLoggingEnabled = false
this.setEditorOutput = this.setEditorOutput.bind(this);
this.setConstraintsEditorOutput = this.setConstraintsEditorOutput.bind(this);
this.setConstraintEditorFun = this.setConstraintEditorFun.bind(this);
this.changeProject = this.changeProject.bind(this);
this.changeStrategy = this.changeStrategy.bind(this);
this.setStrategyConstraint = this.setStrategyConstraint.bind(this);
this.setSchedUnitParams = this.setSchedUnitParams.bind(this);
this.validateForm = this.validateForm.bind(this);
this.validateStations = this.validateStations.bind(this);
this.validateEditor = this.validateEditor.bind(this);
this.setEditorFunction = this.setEditorFunction.bind(this);
this.saveSchedulingUnit = this.saveSchedulingUnit.bind(this);
this.cancelCreate = this.cancelCreate.bind(this);
this.reset = this.reset.bind(this);
this.refreshSchedulingSet = this.refreshSchedulingSet.bind(this);
this.checkIsDirty = this.checkIsDirty.bind(this);
this.close = this.close.bind(this);
this.setSUSet = this.setSUSet.bind(this);
this.updateConstraintsParams = this.updateConstraintsParams.bind(this);
this.setCreateAnother = this.setCreateAnother.bind(this);
this.setState = this.setState.bind(this);
this.init = this.init.bind(this);
}
componentDidMount() {
this.init();
}
async init() {
const permission = await AuthUtil.getUserPermissionByModule('scheduleunit_draft');
this.setState({ userrole: permission });
const promises = [ProjectService.getProjectList(false, 'name,url'),
TaskService.getTaskTemplates(),
ScheduleService.getSchedulingConstraintTemplates(),
UtilService.getPriorityQueueType(),
TaskService.getTaskFilterDefinition('draft'),
ScheduleService.getStrategyPurpose(),
ScheduleService.getStrategyState(),
ProjectService.getMyRoles(),
];
Promise.all(promises).then(async (responses) => {
this.projects = responses[0];
this.taskTemplates = responses[1];
this.constraintTemplates = responses[2];
this.priorityQueueTypes = responses[3];
this.taskFilters = responses[4];
this.templatePurposes = this.getStrategyFilterOptions(responses[5], 'purpose');
this.templateState = this.getStrategyFilterOptions(responses[6], 'state');
this.myRoles = responses[7];
const systemRoles = this.myRoles.system_roles.map(role => {
return role.toLowerCase()
});
const projectRoles = Object.fromEntries(
Object.entries(this.myRoles.project_roles).map(([project, roles]) => [project.toLowerCase(), roles])
);
// Users with system_roles other than superuser, operator & support can view the projects for which they have shared_support project_role
if (_.intersection(systemRoles, ['superuser', 'operator', 'support']).length < 1) {
this.projects = _.filter(this.projects, (project) => {
return _.includes(projectRoles[project.name.toLowerCase()], 'shared_support')
})
}
if (this.state.schedulingUnit.project) {
const selectedProject = _.filter(this.projects, { 'name': this.state.schedulingUnit.project });
const projectSchedSets = await ScheduleService.getSchedulingSets(`project=${encodeURIComponent(this.state.schedulingUnit.project)}`);
let tmpSU = this.state.schedulingUnit;
tmpSU['output_pinned'] = selectedProject[0].auto_pin;
this.setState({
isLoading: false,
schedulingSets: projectSchedSets,
selectedProject: selectedProject,
schedulingUnit: tmpSU
});
} else {
this.setState({ isLoading: false });
}
});
let observStrategyFilter = UtilService.localStore({ type: 'get', key: 'strategyFilterSUCreate' });
if (Object.keys(observStrategyFilter).length === 0) {
observStrategyFilter = { states: ['active'], purpose: [] }
}
this.setState({
observStrategies: await this.getMatchingObservationStrategyTemplates(observStrategyFilter),
observStrategyFilters: observStrategyFilter
});
}
async getMatchingObservationStrategyTemplates(filters) {
return await queryMatchingObservationStrategyTemplates(filters)
}
getStrategyFilterOptions(filterValues, filterId) {
if (filterValues) {
let optionList = [];
filterValues.forEach(choice => {
const tmpVar = { name: choice.value, value: choice.value };
if (filterId === 'purpose') {
optionList.push(tmpVar);
}
if (filterId === 'state' && tmpVar.value !== 'obsolete') {
optionList.push(tmpVar);
}
});
return _.sortBy(optionList, ['name']);
}
}
/**
* Function to call on change of project and reload scheduling set dropdown
* @param {string} projectName
*/
async changeProject(projectName) {
const projectSchedSets = await ScheduleService.getSchedulingSets(`project=${encodeURIComponent(projectName)}`);
let schedulingUnit = this.state.schedulingUnit;
schedulingUnit.project = projectName;
if (projectSchedSets?.length==1) {
schedulingUnit.scheduling_set_id = projectSchedSets[0].id;
} else { schedulingUnit.scheduling_set_id = null;}
const selectedProject = _.filter(this.projects, { 'name': projectName });
schedulingUnit['output_pinned'] = selectedProject[0].auto_pin;
if (!this.state.isDirty) {
publish('edit-dirty', true);
}
this.setState({
selectedProject: selectedProject,
schedulingUnit: schedulingUnit,
schedulingSets: projectSchedSets,
validForm: this.validateForm('project', projectName, schedulingUnit) && this.validateForm('scheduling_set_id'), // project is valid, we selected, but we deselected scheduling
isDirty: true
});
}
/**
* 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
* @param {number} strategyId
*/
async changeStrategy(strategyId, isSecondRound = false) {
if (this.isDebuggLoggingEnabled) console.log("changing strategy to " + strategyId)
const observStrategy = _.cloneDeep(_.find(this.state.observStrategies, { 'id': strategyId }));
if (ParserUtility.addStationParameters(observStrategy)) {
this.setState({
observStrategy: observStrategy,
constraintSchema: null
});
}
let stationGroups = {};
const tasks = observStrategy.template.tasks;
const parameters = observStrategy.template.parameters;
let paramsOutput = {};
let schema = {
type: 'object', additionalProperties: false,
properties: {}, definitions: {}
};
let observationType = null;
let bandPassFilter = null;
// Take the constraint template schema as mentioned in the observing strategy
this.setStrategyConstraint(observStrategy.template.scheduling_constraints_template.name, observStrategy.template.scheduling_constraints_template.version);
if (this.isDebuggLoggingEnabled) console.log(" was one changing strategy to " + strategyId)
const $strategyRefs = await $RefParser.resolve(observStrategy.template);
// TODo: This schema reference resolving code has to be moved to common file and needs to rework
for (const param of parameters) {
// TODO: make parameter handling more generic, instead of task specific.
if (!param.refs[0].startsWith("#/tasks/")) {
continue;
}
let taskPaths = param.refs[0].split("/");
const taskName = taskPaths[2];
/**
* For Short_Description, the task path length will be 4, so added below condition to get short_description details
* #/tasks/Combined Observation/short_description
*/
taskPaths = taskPaths.slice((taskPaths.length === 4 ? 3 : 4), taskPaths.length);
const task = tasks[taskName];
if (task) {
if (taskPaths.includes("station_groups")) {
stationGroups[param.name] = $strategyRefs.get(param.refs[0]);
} else {
const taskTemplate = getTaskTemplateForTask(this.taskTemplates, task);
if (this.isDebuggLoggingEnabled) console.log("changing strategy to " + strategyId)
if (['target observation', 'beamforming observation'].indexOf(taskTemplate.name.toLowerCase()) >= 0) {
observationType = taskTemplate.name;
}
if (this.isDebuggLoggingEnabled) console.log("observation type changed to " + observationType)
// Get the default Bandpass filter and pass to the editor for frequency calculation from subband list
if (taskTemplate.type_value === 'observation' && task.specifications_doc.station_configuration?.filter) {
bandPassFilter = task.specifications_doc.station_configuration?.filter;
} else if (taskTemplate.type_value === 'observation' && taskTemplate.ref_resolved_schema.definitions.filter) {
bandPassFilter = taskTemplate.ref_resolved_schema.definitions.filter.default;
}
schema.definitions = { ...schema.definitions, ...taskTemplate?.ref_resolved_schema.definitions };
taskPaths.reverse();
const paramProp = await ParserUtility.getParamProperty($strategyRefs, taskPaths, taskTemplate.ref_resolved_schema, this.taskFilters);
schema.properties[param.name] = _.cloneDeep(paramProp);
if (schema.properties[param.name]) {
schema.properties[param.name].title = param.name;
try {
schema.properties[param.name].default = $strategyRefs.get(param.refs[0]);
} catch (err) { /* empty */ }
paramsOutput[param.name] = schema.properties[param.name].default || ParserUtility.getDefaultParamValue(schema.properties[param.name].type);
}
}
}
}
if (!this.state.isDirty) {
publish('edit-dirty', true);
}
if (this.isDebuggLoggingEnabled) console.log("changing strategy in progress " + schema, stationGroups, paramsOutput, bandPassFilter)
this.setState({
observStrategy: observStrategy, paramsSchema: _.cloneDeep(schema), constraintParamsOutput: null,
observationType: observationType, stationsCollapsed: false,
paramsOutput: paramsOutput, stationGroups: stationGroups,
bandPassFilter: bandPassFilter, isDirty: true
}, this.state.callbackFunction);
// Function called to clear the JSON Editor fields and reload with new schema
if (!isSecondRound) {
setTimeout(() => { this.changeStrategy(strategyId, true) }, 1);
}
}
/**
* 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.paramsOutput = jsonOutput;
this.validEditor = errors.length === 0;
this.updateParameters();
if (errors.length > 0) {
this.setState({ errors: [...errors] })
}
this.setState({
paramsOutput: jsonOutput,
validEditor: errors.length === 0,
validForm: this.validateForm()
});
}
/**
* Common function validate specification
* @param {String} template Template url
* @param {Object} updatedSpecificationDoc Object which needs to be validated
*/
async validateSpecificationsDoc(updatedObservStrategy) {
if (updatedObservStrategy) {
const promises = [];
promises.push(ScheduleService.validateSpecificationsDoc(updatedObservStrategy.scheduling_unit_template, updatedObservStrategy.template))
const validation_result = await Promise.all(promises);
if (this.isDebuggLoggingEnabled) console.log("validateSpecificationsDoc", updatedObservStrategy, validation_result)
if (validation_result.length > 0) {
let validationMessage = validation_result[0]['message'];
if (validation_result[0]['valid']) {
validationMessage = "Task Parameters specification is valid"
}
this.setState({
validSpecification: validation_result[0]['valid'],
validSpecificationMessage: validationMessage
});
}
}
}
async validateSchedulingConstraints(updatedConstraintParmas) {
if (updatedConstraintParmas) {
let schedulingConstraintsDoc = updatedConstraintParmas.scheduling_constraints_doc;
if (schedulingConstraintsDoc.time)
for (const key of _.keys(schedulingConstraintsDoc.time)) {
if (!schedulingConstraintsDoc.time[key]) {
delete schedulingConstraintsDoc.time[key];
}
}
const promises = [];
promises.push(ScheduleService.validateSpecificationsDoc(updatedConstraintParmas.constraint.url, schedulingConstraintsDoc))
const validation_result = await Promise.all(promises);
if (validation_result.length > 0) {
let validationMessage = validation_result[0]['message'];
if (validation_result[0]['valid']) {
validationMessage = "Constraints specification is valid"
}
this.setState({
validConstraints: validation_result[0]['valid'],
validConstraintMessage: validationMessage
});
}
}
}
loadConstraints(properties, parentProps, defaultValues) {
parentProps = parentProps ? parentProps : _.cloneDeep(this.defaultConstraints);
for (const key in properties) {
let propertyValue = properties[key];
if (propertyValue instanceof Object && key !== "reference_pointing") {
if (defaultValues) {
parentProps[key] = propertyValue;
this.loadConstraints(propertyValue, parentProps[key], true);
} else {
this.loadConstraints(propertyValue, parentProps[key], false);
}
} else {
if (_.includes(['from', 'to'], key)) {
propertyValue = (propertyValue < 0 ? '-' : '') + UnitConverter.getSecsToHHmmss(propertyValue);
}
if (!_.includes(_.keys(parentProps), "angle1") && _.includes(['sun', 'moon', 'jupiter', 'target', 'calibrator'], key) && !defaultValues) {
propertyValue = ((propertyValue * 180) / Math.PI).toFixed(2);
}
if (_.includes(['between', 'not_between'], key)) {
propertyValue = [];
}
parentProps[key] = propertyValue;
}
}
if (defaultValues) {
this.defaultConstraints = parentProps;
} else {
this.setState({ constraintInitParams: parentProps });
}
// After converting the sky values, set the value directly in the JSON editor using the function passed from the editor
if (this.state.constraintEditorFunction) {
this.state.constraintEditorFunction("setValue", [parentProps]);
}
}
setConstraintsEditorOutput(jsonOutput, errors) {
let err = [...errors];
if (jsonOutput.scheduler === 'online' || jsonOutput.scheduler === 'dynamic') {
err = err.filter(e => e.path !== 'root.time.at');
}
this.constraintParamsOutput = jsonOutput;
// When the constraint template is changed, get the default values from the template and overwrite with values from strategy
if (!this.state.constraintParamsOutput) {
if (this.state.observStrategy.template !== undefined) {
this.loadConstraints(_.cloneDeep(this.state.observStrategy.template.scheduling_constraints_doc), _.cloneDeep(jsonOutput), false);
}
}
this.constraintValidEditor = err.length === 0;
if (!this.state.isDirty && this.state.constraintParamsOutput && !_.isEqual(this.state.constraintParamsOutput, jsonOutput)) {
if (!_.isEmpty(this.state.observStrategy)) {
if (!this.state.isDirty) {
publish('edit-dirty', true);
}
this.setState({
constraintParamsOutput: jsonOutput,
constraintValidEditor: err.length === 0,
validForm: this.validateForm(),
isDirty: true
});
}
} else {
this.setState({
constraintParamsOutput: jsonOutput,
constraintValidEditor: err.length === 0,
validForm: this.validateForm()
});
}
this.updateConstraintsParams();
}
/**
* This function is mainly added for Unit Tests. If this function is removed Unit Tests will fail.
*/
validateEditor() {
return this.validEditor && this.constraintValidEditor ? true : false;
}
/**
* Function to set form values to the SU object
* @param {string} key
* @param {object} value
*/
async setSchedUnitParams(key, value) {
if (this.isDebuggLoggingEnabled) console.log("setSchedUnitParams", key, value)
this.setState({
touched: {
...this.state.touched,
[key]: true
}
});
let schedulingUnit = _.cloneDeep(this.state.schedulingUnit);
if (key === 'rank') {
const previousValue = schedulingUnit.rank;
value = value.replace(/([^0-9.]+)/, "");
const match = /(\d{0,1})[^.]*((?:\.\d{0,4})?)/g.exec(value);
const val = match[1] + match[2];
if (val === '' || (val >= 0 && val <= 1)) {
schedulingUnit[key] = val === '' ? '' : Number(val);
} else {
schedulingUnit[key] = previousValue;
}
} else if (key === 'scheduling_set_id') {
schedulingUnit[key] = value;
let schedulingUnitList = await ScheduleService.getSchedulingBySet(value, 'observation_strategy_template_id');
let observationIdSet = _.uniq(_.map(schedulingUnitList, 'observation_strategy_template_id'));
this.setState({ observationIdSet: observationIdSet });
} else {
schedulingUnit[key] = value;
}
if (!this.state.isDirty && !_.isEqual(this.state.schedulingUnit, schedulingUnit)) {
if (!this.state.isDirty) {
publish('edit-dirty', true);
}
this.setState({ schedulingUnit: schedulingUnit, validForm: this.validateForm(key, value), validEditor: this.validateEditor(), isDirty: true }, this.validateEditor);
} else {
this.setState({ schedulingUnit: schedulingUnit, validForm: this.validateForm(key, value), validEditor: this.validateEditor() }, this.validateEditor);
}
}
/**
* 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 });
}
setConstraintEditorFun(editorFunction) {
this.setState({ constraintEditorFunction: editorFunction });
}
/**
* 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, value = this.state.schedulingUnit[fieldName], schedulingUnit = this.state.schedulingUnit) {
if (this.isDebuggLoggingEnabled) console.log("validateForm validating " + fieldName + " with value " + value, this.state.schedulingUnit)
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 = value
if (rule.required) {
if (!fieldValue) {
if (this.isDebuggLoggingEnabled) console.log(" Not Filled. validateForm", fieldName, rule, fieldValue, this.state.schedulingUnit)
errors[fieldName] = rule.message ? rule.message : `${fieldName} is required`;
} else {
if (this.isDebuggLoggingEnabled) console.log(" All Good validateForm", fieldName, rule, fieldValue, this.state.schedulingUnit)
validFields[fieldName] = true;
}
}
}
} else {
errors = {};
validFields = {};
for (const fieldName in this.formRules) {
const rule = this.formRules[fieldName];
const fieldValue = schedulingUnit[fieldName];
if (rule.required) {
if (!fieldValue) {
errors[fieldName] = rule.message ? rule.message : `${fieldName} is required`;
} else {
validFields[fieldName] = true;
}
}
}
}
const updatedErrors = errors.length > 0 ? [...this.state.errors, errors] : this.state.errors
const updatedValidFields = validFields ? { ...this.state.validFields, ...validFields } : this.state.validFields;
if (this.isDebuggLoggingEnabled) console.log("set state validFields", updatedValidFields)
this.setState({ errors: updatedErrors, validFields: updatedValidFields });
let lengthkeys = Object.keys(validFields).length;
let lengthRules = Object.keys(this.formRules).length;
if (lengthkeys === lengthRules) {
validForm = true;
}
const validStations = this.validateStations();
let sumofstations = _.sum(_.values(this.state.missingStnErorsofTask)) <= 0;
if (this.isDebuggLoggingEnabled) console.log("Validating Form result:" + validForm, " validStations:", validStations, " sumofstations:" + sumofstations, "lengthkeys+" + lengthkeys + " , lengthRules+", lengthRules, validFields, this.formRules, schedulingUnit)
return validForm && validStations && sumofstations;
}
/*
* Checks if at least one station selected for each station_group parameter and max_nr_missing station value is entered.
* @returns Boolean
*/
validateStations() {
let validStations = false;
// Check if at least one station selected for all station groups(parameters)
// Break the loop if any of the required condition check is not satisfied
for (const stationGroup of _.keys(this.state.stationGroups)) {
// Check if the selected stations and custom stations are not empty
if (this.state.selectedStationsOfTask[stationGroup] && this.state.customStationsOfTask[stationGroup]) {
// Get the stations list for the station group(parameter)
const stationsList = this.state.selectedStationsOfTask[stationGroup].concat(this.state.customStationsOfTask[stationGroup]);
//If stationsList is empty then no station group and custom stations are selected
if (stationsList.length === 0) {
validStations = false;
break;
} else {
//If predefined station group(Dutch, Remote, etc.,) is/are selected, validate if max_nr_missing is entered
if (this.state.selectedStationsOfTask[stationGroup].length > 0) {
for (const selectedStations of this.state.selectedStationsOfTask[stationGroup]) {
const selectedGroup = this.state.missingStationsofTask[stationGroup][selectedStations]
if (selectedGroup) {
if (selectedGroup.max_nr_missing === "") {
validStations = false;
break;
} else {
validStations = true;
}
}
}
if (!validStations) {
break;
} else {
validStations = true;
}
}
//If custom station group(s) is/are selected, validate if max_nr_missing is entered
if (this.state.customStationsOfTask[stationGroup].length > 0) {
for (const customStations of this.state.customStationsOfTask[stationGroup]) {
if (customStations.max_nr_missing === "") {
validStations = false;
break;
} else {
validStations = true;
}
}
if (!validStations) {
break;
} else {
validStations = true;
}
}
}
} else {
validStations = false;
break;
}
}
return validStations;
}
/**
* Function to update task parameters
*/
async updateParameters() {
let observStrategy = _.cloneDeep(this.state.observStrategy);
const $refs = await $RefParser.resolve(observStrategy.template);
observStrategy.template.parameters.forEach(async (param, index) => {
// $refs.set(observStrategy.template.parameters[index]['refs'][0], this.state.paramsOutput[param.name]);
let refIndex = 0;
for (const ref of param.refs) {
let station_groups = [];
if (ref.includes("station_groups")) {
if (this.state.selectedStationsOfTask[param.name])
for (const selectedGroup of this.state.selectedStationsOfTask[param.name]) {
const stnGroup = this.state.missingStationsofTask[param.name][selectedGroup];
station_groups.push({
stations: stnGroup.stations,
max_nr_missing: parseInt(stnGroup.max_nr_missing)
});
}
if (this.state.customStationsOfTask[param.name])
for (const customGroup of this.state.customStationsOfTask[param.name]) {
station_groups.push({
stations: customGroup.stations,
max_nr_missing: parseInt(customGroup.max_nr_missing)
});
}
$refs.set(observStrategy.template.parameters[index]['refs'][refIndex], station_groups);
} else {
$refs.set(observStrategy.template.parameters[index]['refs'][refIndex], this.state.paramsOutput[param.name]);
}
refIndex++;
}
});
if (this.state.validEditor) {
this.validateSpecificationsDoc(observStrategy);
} else {
let message = 'Task Parameters specification is not valid: '
this.state.errors.forEach(error => {
message = message + error.message + "\n"
})
this.setState({
validSpecification: false,
validSpecificationMessage: message
})
}
this.setState({ updatedObservStrategy: observStrategy });
}
/**
* Function to update constraints
*/
updateConstraintsParams() {
const constStrategy = _.cloneDeep(this.state.constraintParamsOutput);
if (constStrategy)
for (let type in constStrategy.time) {
if (constStrategy.scheduler === 'online' || constStrategy.scheduler === 'dynamic') {
// delete constStrategy.time.at;
}
if (!constStrategy.time.after) {
delete constStrategy.time.after;
}
if (!constStrategy.time.before) {
delete constStrategy.time.before;
}
if (constStrategy.time[type] && constStrategy.time[type].length > 0) {
if (typeof constStrategy.time[type] === 'string') {
constStrategy.time[type] = `${moment(constStrategy.time[type]).format("YYYY-MM-DDTHH:mm:ss.SSSSS", { trim: false })}Z`;
} else {
constStrategy.time[type].forEach(time => {
for (let key in time) {
time[key] = `${moment(time[key]).format("YYYY-MM-DDTHH:mm:ss.SSSSS", { trim: false })}Z`;
}
})
}
}
}
if (constStrategy?.sky.transit_offset) {
constStrategy.sky.transit_offset.from = UnitConversion.getHHmmssToSecs(constStrategy.sky.transit_offset.from);
constStrategy.sky.transit_offset.to = UnitConversion.getHHmmssToSecs(constStrategy.sky.transit_offset.to);
}
UnitConversion.degreeToRadians(constStrategy?.sky);
const const_strategy = {
scheduling_constraints_doc: constStrategy,
id: this.state.constraintSchema.id,
constraint: this.state.constraintSchema
};
if (this.state.constraintValidEditor) {
this.validateSchedulingConstraints(const_strategy)
} else {
this.setState({ validConstraints: false, validConstraintMessage: 'Constraints specification is not valid' })
}
this.setState({ updatedConstraintParmas: const_strategy });
}
/**
* Function to create Scheduling unit
*/
async saveSchedulingUnit() {
await this.updateParameters();
let observStrategy = _.cloneDeep(this.state.updatedObservStrategy);
let newSpecificationDoc = {}
for (const parameter of observStrategy.template.parameters) {
for (const ref of parameter.refs) {
let paramPath = ref.replace(new RegExp('#/', "g"), "");
paramPath = paramPath.replace(new RegExp('/', "g"), '.');
const value = _.get(observStrategy.template, paramPath);
if (value !== undefined) {
newSpecificationDoc = _.set(newSpecificationDoc, paramPath, value);
}
}
}
newSpecificationDoc['scheduling_constraints_doc'] = _.cloneDeep(this.state.updatedConstraintParmas).scheduling_constraints_doc;
let schedulingUnit = this.state.schedulingUnit;
schedulingUnit.observation_strategy_template_id = observStrategy.id;
schedulingUnit.rank = schedulingUnit.rank === undefined || schedulingUnit.rank === '' ? 0 : schedulingUnit.rank.toFixed(4);
schedulingUnit = await ScheduleService.saveSchedulingUnit(schedulingUnit, newSpecificationDoc);
if (schedulingUnit.isSUUpdated) {
if (appGrowl === null) {
setAppGrowl(this.growl);
}
appGrowl.show({
severity: 'success',
summary: 'Success',
detail: 'Scheduling Unit and Tasks are created successfully'
});
if (this.state.createAnother) {
this.setState({ schedulingUnit: schedulingUnit.data, isDirty: false });
this.reset();
} else {
this.setState({
schedulingUnit: schedulingUnit.data,
isDirty: false,
redirect: `/schedulingunit/view/draft/${schedulingUnit.data.id}`
});
}
publish('edit-dirty', false);
} else {
this.growl.show({
severity: 'error',
summary: 'Error Occured',
detail: schedulingUnit.message || 'Unable to save Scheduling Unit/Tasks'
});
}
}
/**
* warn before cancel the page if any changes detected
*/
checkIsDirty() {
if (this.state.isDirty) {
this.setState({ showDialog: true });
} else {
this.cancelCreate();
}
}
close() {
this.setState({ showDialog: false });
}
/**
* Cancel SU creation and redirect
*/
cancelCreate() {
publish('edit-dirty', false);
if (this.state.multiCreateCount > 1) {
if (this.state.isDirty) {
this.props.history.push(`/schedulingunit`);
} else {
this.props.history.go(this.state.multiCreateCount * -1);
}
} else {
this.props.history.length > 1 ? this.state.isDirty ? this.props.history.go(-2) : this.props.history.go(-1) : this.props.history.push(`/schedulingunit`);
}
this.setState({ showDialog: false });
}
async setStrategyConstraint(name, version) {
let strategyConstraint = getConstraintTemplate(this.constraintTemplates, name, version)
let schedulingUnit = { ...this.state.schedulingUnit };
schedulingUnit.scheduling_constraints_template_id = strategyConstraint ? strategyConstraint.id : '';
strategyConstraint.ref_resolved_schema.properties.sky.properties.transit_offset.properties.from.default = UnitConversion.getSecsToHHmmssWithSign(strategyConstraint.ref_resolved_schema.properties.sky.properties.transit_offset.properties.from.default);
strategyConstraint.ref_resolved_schema.properties.sky.properties.transit_offset.properties.to.default = UnitConversion.getSecsToHHmmssWithSign(strategyConstraint.ref_resolved_schema.properties.sky.properties.transit_offset.properties.to.default);
strategyConstraint.ref_resolved_schema.properties.sky.properties.min_distance.properties.sun.propertyOrder = 1;
strategyConstraint.ref_resolved_schema.properties.sky.properties.min_distance.properties.moon.propertyOrder = 2;
strategyConstraint.ref_resolved_schema.properties.sky.properties.min_distance.properties.jupiter.propertyOrder = 3;
this.setState({ constraintSchema: strategyConstraint, schedulingUnit });
}
/**
* Reset function to be called when user wants to create new SU
*/
reset() {
const schedulingSets = this.state.schedulingSets;
this.nameInput.focus();
const selectedProject = this.props.match?.params?.project;
let multiCreateCount = this.state.multiCreateCount;
this.setState({
isDirty: false,
dialog: { header: '', detail: '' },
errors: [],
schedulingSets: this.props.match?.params?.project ? schedulingSets : [],
schedulingUnit: {
name: '',
description: '',
project: selectedProject || null,
scheduling_constraints_template_id: this.constraintTemplates[0].id,
rank: '',
output_pinned: this.props.match?.params?.project ? ((_.find(this.projects, { 'name': selectedProject }))?.auto_pin) : null
},
projectDisabled: (this.props.match?.params?.project ? true : false),
observStrategy: {},
paramsSchema: null,
paramsOutput: null,
validEditor: false,
validFields: {},
constraintSchema: null,
constraintInitParams: null,
selectedStations: null,
selectedStationsOfTask: {},
customStationsOfTask: {},
touched: false,
stationGroups: {},
multiCreateCount: ++multiCreateCount
});
this.state.editorFunction();
}
onUpdateStations = (state, selectedStations, missing_StationFieldsErrors, customSelectedStations, taskName) => {
let selectedStationsOfTask = this.state.selectedStationsOfTask;
let taskStations = selectedStationsOfTask[taskName] || [];
let customStationsOfTask = this.state.customStationsOfTask;
let taskCustomStations = customStationsOfTask[taskName] || [];
let missingStationsofTask = this.state.missingStationsofTask;
let missingStnErorsofTask = this.state.missingStnErorsofTask;
selectedStationsOfTask[taskName] = selectedStations;
customStationsOfTask[taskName] = customSelectedStations;
missingStationsofTask[taskName] = state;
missingStnErorsofTask[taskName] = missing_StationFieldsErrors;
if (!this.state.isDirty) {
if (!_.isEqual(taskStations, selectedStations) || !_.isEqual(taskCustomStations, customSelectedStations)) {
this.setState({
missingStationsofTask,
selectedStationsOfTask,
missingStnErorsofTask,
customStationsOfTask
}, () => {
this.setState({ validForm: this.validateForm(), isDirty: true });
if (!this.state.isDirty) {
publish('edit-dirty', true);
}
});
} else {
this.setState({
missingStationsofTask,
selectedStationsOfTask,
missingStnErorsofTask,
customStationsOfTask
}, () => {
this.setState({ validForm: this.validateForm() });
});
}
} else {
this.setState({
missingStationsofTask,
selectedStationsOfTask,
missingStnErorsofTask,
customStationsOfTask
}, () => {
this.setState({ validForm: this.validateForm() });
});
}
};
async refreshSchedulingSet() {
let tmpSU = _.cloneDeep(this.state.schedulingUnit);
const schedulingSets = await ScheduleService.getSchedulingSets(`project=${encodeURIComponent(this.state.schedulingUnit.project)}`);
if (this.state.newSet) {
tmpSU.scheduling_set_id = this.state.newSet.id;
this.setState({
schedulingUnit: tmpSU,
newSet: null
});
}
this.setState({ saveDialogVisible: false, showAddSet: false, schedulingSets: schedulingSets });
}
/**
* Set newly created as current SU Set
* @param {SU Set Name} suSet
*/
setSUSet(suSet) {
this.setState({ newSet: suSet });
}
/**
* Select/Unselect the create another check box
* @param {*} e
*/
setCreateAnother(e) {
this.setState({ 'createAnother': e.target.checked })
}
render() {
let isSaveEnabled = (this.state.constraintValidEditor && this.state.validEditor && this.state.validForm && this.state.validConstraints && this.state.validSpecification)
if (this.isDebuggLoggingEnabled) console.log("isSaveEnabled: " + isSaveEnabled + " constraintValidEditor: " + this.state.constraintValidEditor + " ,vvalidEditor:" + this.state.validEditor + " ,validForm:" + this.state.validForm + " ,validConstraints:" + this.state.validConstraints + " ,validSpecification:" + this.state.validSpecification);
if (this.state.redirect) {
return <Redirect to={{ pathname: this.state.redirect }}></Redirect>
}
const schema = this.state.paramsSchema;
const { scheduleunit_draft } = this.state.userrole;
let jeditor = null;
if (schema) {
jeditor = React.createElement(Jeditor, {
title: "Task Parameters",
schema: schema,
initValue: this.state.paramsOutput,
callback: this.setEditorOutput,
parentFunction: this.setEditorFunction,
bandPassFilter: this.state.bandPassFilter,
observationType: this.state.observationType,
theme: 'bootstrap4'
});
}
return (
<React.Fragment>
<Toast ref={(el) => this.growl = el} />
<PageHeader location={this.props.location} className="SchedulingUnit-PageHeader" title={'Scheduling Unit - Add'}
actions={[{
icon: 'fa-window-close', title: 'Click to close Scheduling Unit creation',
type: 'button', actOn: 'click', props: { callback: this.checkIsDirty }
}]} />
{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" data-testid="namediv">
<InputText
className={(this.state.errors.name && this.state.touched.name) ? 'input-error' : ''}
id="name" data-testid="name"
tooltip="Enter name of the Scheduling Unit"
tooltipOptions={this.tooltipOptions} maxLength="128"
ref={input => {
this.nameInput = input;
}}
value={this.state.schedulingUnit.name} autoFocus
onChange={(e) => this.setSchedUnitParams('name', e.target.value)}
onBlur={(e) => this.setSchedUnitParams('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 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 && this.state.touched.description) ? "error" : "info"}>
{(this.state.errors.description && this.state.touched.description) ? this.state.errors.description : "Max 255 characters"}
</label>
</div>
</div>
{<ProjectScheduleSetSelector
selectedProject={this.state.schedulingUnit.project}
projectDisabled={this.state.projectDisabled}
allProjects={this.projects}
projectErrors={this.state.errors.project}
touchedProject={this.state.touched.project}
changeProject={this.changeProject}
selectedSchedulingSet={this.state.schedulingUnit.scheduling_set_id}
allSchedulingSets={this.state.schedulingSets}
schedulingSetErrors={this.state.errors.scheduling_set_id}
touchedSchedulingSet={this.state.touched.scheduling_set_id}
setSchedUnitParams={this.setSchedUnitParams}
scheduleUnitDraft={scheduleunit_draft}
setState={this.setState}
/>}
<div className="p-field p-grid">
<label htmlFor="rank" className="col-lg-2 col-md-2 col-sm-12">Rank</label>
<div className="col-lg-3 col-md-3 col-sm-12" data-testid="rank">
<input type="number"
data-for="reacttooltip"
data-iscapture="true"
data-tip="Priority of this scheduling unit w.r.t. other scheduling units within the same queue and project.. Min-0.0000, Max-1.0000"
id="proj-rank" name="rank" data-testid="rank"
className="p-inputtext p-component p-filled"
value={this.state.schedulingUnit.rank}
step="0.0001"
lang="en"
onChange={(e) => this.setSchedUnitParams('rank', e.target.value)} />
</div>
<div className="col-lg-1 col-md-1 col-sm-12"></div>
<label htmlFor="priority_queue" className="col-lg-2 col-md-2 col-sm-12">Priority
Queue</label>
<div className="col-lg-3 col-md-3 col-sm-10">
<Dropdown data-testid="priority_queue" id="priority_queue" optionLabel="value"
optionValue="value"
tooltip="Priority queue of this scheduling unit. Queues provide a strict ordering between scheduling units."
tooltipOptions={this.tooltipOptions}
value={this.state.schedulingUnit.priority_queue}
options={this.priorityQueueTypes}
onChange={(e) => {
this.setSchedUnitParams('priority_queue', e.value)
}}
placeholder="Select Priority Queue" />
</div>
</div>
<div className="p-field p-grid">
<label htmlFor="observStrategy" className="col-lg-2 col-md-2 col-sm-12">
Observation Strategy <span style={{ color: 'red' }}>*</span>
</label>
{<ObservationStrategySelector
selectedPurpose={this.state.observStrategyFilters.purpose}
allPurposes={this.templatePurposes}
selectedState={this.state.observStrategyFilters.states}
allStates={this.templateState}
selectedObservationStrategy={this.state.observStrategy}
allObservationStrategies={this.state.observStrategies}
observationIdSet={this.state.observationIdSet}
changeStrategy={this.changeStrategy}
localStoreKey={"strategyFilterSUCreate"}
setState={this.setState}
/>}
<div className="col-lg-1 col-md-1 col-sm-12"></div>
<label htmlFor="autodeletion" className="col-lg-2 col-md-2 col-sm-12">Pin
Data</label>
<div className="col-lg-3 col-md-3 col-sm-12" data-testid="autodeletion">
<Checkbox inputId="trigger" role="trigger"
tooltip="Do not delete data automatically after successful ingest"
tooltipOptions={this.tooltipOptions}
checked={this.state.schedulingUnit.output_pinned}
onChange={(e) => this.setSchedUnitParams('output_pinned', e.target.checked)}
></Checkbox>
</div>
</div>
{_.keys(this.state.stationGroups).length > 0 &&
<div className='grouping'>
<fieldset>
<h3 className="card-title level-1 je-object__title"
style={{ display: "inline-block" }}>
<button type="button"
title={this.state.stationsCollapsed ? "Expand" : "Collapse"}
className="btn btn-secondary btn-sm json-editor-btn-collapse json-editor-btntype-toggle"
onClick={() => this.setState({ stationsCollapsed: !this.state.stationsCollapsed })}>
<i className={this.state.stationsCollapsed ? "fas fa-caret-right" : "fas fa-caret-down"}></i>
</button>
<label>Station specification</label>
</h3>
{!this.state.stationsCollapsed &&
<>
{_.keys(this.state.stationGroups).map((stnGroup) => {
return <Stations
taskName={stnGroup}
stationGroup={this.state.stationGroups[stnGroup]}
onUpdateStations={this.onUpdateStations.bind(this)}
height={'auto'}
key={stnGroup}
/>
})}
</>
}
</fieldset>
</div>
}
</div>
{this.state.constraintSchema && <div className="p-fluid">
<fieldset className="border-style">
<div className="p-col-12" style={{ width: "100%" }}>
<SchedulingConstraint
constraintTemplate={this.state.constraintSchema}
// initValue={this.state.constraintInitParams} // No need to pass this value as the values are set with callback
callback={this.setConstraintsEditorOutput}
parentFunction={this.setConstraintEditorFun} />
</div>
</fieldset>
<div className="p-col-12"
style={{ marginLeft: '0.4em', marginTop: '-10px', paddingTop: '0px' }}>
<label className={this.state.validConstraints ? "info" : "error"}>
{this.state.validConstraintMessage ? this.state.validConstraintMessage : ""}
</label>
</div>
</div>}
<div></div>
{this.state.paramsSchema &&
<div className="p-fluid">
<fieldset className="border-style">
<div style={{ marginLeft: '-0.5em' }}>
<div className="p-col-12" style={{ width: "100%" }}>
{jeditor}
</div>
</div>
</fieldset>
<div className="p-col-12" style={{
marginLeft: '0.4em',
marginTop: '-10px',
paddingTop: '0px',
marginBottom: "2em"
}}>
<label className={this.state.validSpecification ? "info" : "error"} data-testid="validation-spec-msg">
{this.state.validSpecificationMessage}
</label>
</div>
</div>
}
</div>
<div className="p-breadcrumb footer-long-div actions" style={{ marginLeft: '2em !important' }}>
<FormActionbar createAnotherCallBack={this.setCreateAnother}
createAnother={this.state.createAnother}
tooltip="Select checkbox to create another Scheduling Unit after saving this Scheduling Unit"
submitTitle={(!this.state.constraintValidEditor || !this.state.validEditor || !this.state.validForm || !this.state.validConstraints || !this.state.validSpecification) ? "" : "Save Scheduling Unit"}
onSubmit={this.saveSchedulingUnit}
disableSaveBtn={!isSaveEnabled}
onCancel={this.checkIsDirty} />
</div>
</>
}
{/* Dialog component to show messages and get input */}
<div className="p-grid" data-testid="confirm_dialog">
{this.state.showAddSet &&
<SchedulingSet callbackFunction={this.setSUSet} project={this.state.selectedProject[0]}
onCancel={this.refreshSchedulingSet} />
}
<CustomDialog type="confirmation" visible={this.state.showDialog} width="40vw"
header={'Add Scheduling Unit'}
message={'Do you want to discard your changes? A new Scheduling Unit will not be created'}
content={''} onClose={this.close} actions={[{
id: "yes",
title: 'Discard',
callback: this.cancelCreate,
className: 'act-btn-dispose'
},
{ id: "no", title: 'Cancel', className: 'act-btn-cancel', callback: this.close }]}
>
</CustomDialog>
</div>
</React.Fragment>
);
}
}
SchedulingUnitCreate.propTypes = {
match: PropTypes.object,
history: PropTypes.object,
location: PropTypes.oneOfType([PropTypes.object, PropTypes.string])
}
export default SchedulingUnitCreate;