diff --git a/SAS/TMSS/frontend/tmss_webapp/src/authenticate/login.js b/SAS/TMSS/frontend/tmss_webapp/src/authenticate/login.js index fd5b4b041eccc5f155117b6e072092358b35657e..dc664ca93ed31e31f1a3240ecad5698848b5ce62 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/authenticate/login.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/authenticate/login.js @@ -20,6 +20,7 @@ export class Login extends Component { }; this.login = this.login.bind(this); this.setCredentials = this.setCredentials.bind(this); + this.formSubmit = this.formSubmit.bind(this); } /** @@ -45,6 +46,16 @@ export class Login extends Component { this.setState(state); } + /** + * Function to call login function on Enter key press. + * @param {React.KeyboardEvent} event + */ + formSubmit(event) { + if (event.key === "Enter" && this.state.username && this.state.password) { + this.login(); + } + } + /** * Login function called on click of 'Login' button. * If authenticated, callback parent component function. @@ -88,7 +99,8 @@ export class Login extends Component { <div className="form-field"> <span className="p-float-label"> <InputText id="" className={`${this.state.errors.username?"input-error ":""} form-control`} - value={this.state.username} onChange={(e) => this.setCredentials('username', e.target.value)} /> + value={this.state.username} onChange={(e) => this.setCredentials('username', e.target.value)} + onKeyUp={this.formSubmit} /> <label htmlFor="username"><i className="fa fa-user"></i>Enter Username</label> </span> <label className={this.state.errors.username?"error":""}> @@ -98,7 +110,8 @@ export class Login extends Component { <div className="form-field"> <span className="p-float-label"> <InputText id="password" className={`${this.state.errors.password?"input-error ":""} form-control`} - type="password" value={this.state.password} onChange={(e) => this.setCredentials('password', e.target.value )} /> + type="password" value={this.state.password} onChange={(e) => this.setCredentials('password', e.target.value )} + onKeyUp={this.formSubmit} /> <label htmlFor="password"><i className="fa fa-key"></i>Enter Password</label> </span> <label className={this.state.errors.password?"error":""}> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/layout/components/PageHeader.js b/SAS/TMSS/frontend/tmss_webapp/src/layout/components/PageHeader.js index bc835979e1806deb5d4df907004effcde76e032a..06521dd203b56cbe59f48143d249bf0fcca8581b 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/layout/components/PageHeader.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/layout/components/PageHeader.js @@ -3,7 +3,6 @@ import { routes } from '../../routes'; import {matchPath, Link} from 'react-router-dom'; export default ({ title, subTitle, actions, ...props}) => { - debugger; const [page, setPage] = useState({}); useEffect(() => { diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/SchedulingUnitList.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/SchedulingUnitList.js index ca9d501d3d3fda31040dc2a22d8341cf41ef9917..c8740a6360c5acfd00d664ef044a02afb7c503a7 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/SchedulingUnitList.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/SchedulingUnitList.js @@ -17,7 +17,10 @@ import UtilService from '../../services/util.service'; class SchedulingUnitList extends Component{ lsKeySortColumn = "SchedulingUnitListSortData"; - defaultSortColumn= [{id: "Name", desc: false}]; + defaultSortColumn= [{id: "Name", desc: false}]; + SU_NOT_STARTED_STATUSES = ['defined', 'schedulable', 'scheduled']; + SU_ACTIVE_STATUSES = ['started', 'observing', 'observed', 'processing', 'processed', 'ingesting']; + SU_END_STATUSES = ['finished', 'error', 'cancelled']; constructor(props){ super(props); this. setToggleBySorting(); @@ -155,6 +158,10 @@ class SchedulingUnitList extends Component{ this.onRowSelection = this.onRowSelection.bind(this); this.reloadData = this.reloadData.bind(this); this.addTargetColumns = this.addTargetColumns.bind(this); + this.confirmCancelSchedulingUnit = this.confirmCancelSchedulingUnit.bind(this); + this.cancelSchedulingUnit = this.cancelSchedulingUnit.bind(this); + this.getSUCancelConfirmContent = this.getSUCancelConfirmContent.bind(this); + this.getSUCancelStatusContent = this.getSUCancelStatusContent.bind(this); } /** @@ -587,6 +594,125 @@ class SchedulingUnitList extends Component{ this.componentDidMount(); } + /** + * Prepare Scheduling Unit(s) details to show on confirmation dialog before cancelling + */ + getSUCancelConfirmContent() { + let selectedSUs = [], ignoredSUs = []; + for (const obj of this.selectedRows) { + if (obj.type === "Blueprint" && this.SU_END_STATUSES.indexOf(obj.status) < 0) { + selectedSUs.push({ + suId: obj.id, suName: obj.name, + suType: obj.type, status: obj.status + }); + } else { + ignoredSUs.push({ + suId: obj.id, suName: obj.name, + suType: obj.type, status: obj.status + }); + } + } + return <> + <div style={{marginTop: '1em'}}> + <b>Scheduling Unit(s) that can be cancelled</b> + <DataTable value={selectedSUs} resizableColumns columnResizeMode="expand" className="card" style={{ paddingLeft: '0em' }}> + <Column field="suId" header="Scheduling Unit Id"></Column> + <Column field="suName" header="Scheduling Unit Name"></Column> + <Column field="suType" header="Type"></Column> + <Column field="status" header="Status"></Column> + </DataTable> + </div> + {ignoredSUs.length > 0 && + <div style={{marginTop: '1em'}}> + <b>Scheduling Unit(s) that will be ignored</b> + <DataTable value={ignoredSUs} resizableColumns columnResizeMode="expand" className="card" style={{ paddingLeft: '0em' }}> + <Column field="suId" header="Scheduling Unit Id"></Column> + <Column field="suName" header="Scheduling Unit Name"></Column> + <Column field="suType" header="Type"></Column> + <Column field="status" header="Status"></Column> + </DataTable> + </div> + } + </> + } + + /** + * Prepare Scheduling Unit(s) details to show status of cancellationn + */ + getSUCancelStatusContent() { + let cancelledSchedulingUnits = this.state.cancelledSchedulingUnits; + return <> + <DataTable value={cancelledSchedulingUnits} resizableColumns columnResizeMode="expand" className="card" style={{ paddingLeft: '0em' }}> + <Column field="suId" header="Scheduling Unit Id"></Column> + <Column field="suName" header="Scheduling Unit Name"></Column> + <Column field="type" header="Type"></Column> + <Column field="status" header="Status"></Column> + </DataTable> + </> + } + + /** + * Function to get confirmation before cancelling all selected scheduling unit blueprints if the status is + * not one of the end statuses. + * If no selected scheduling unit is cancellable, show info to select a cancellable scheduling unit. + * + */ + confirmCancelSchedulingUnit() { + let selectedBlueprints = this.selectedRows.filter(schedulingUnit => { + return schedulingUnit.type === 'Blueprint' && + this.SU_END_STATUSES.indexOf(schedulingUnit.status)<0}); + if (selectedBlueprints.length === 0) { + appGrowl.show({ severity: 'info', summary: 'Select Row', + detail: 'Select atleast one cancellable Scheduling Unit Blueprint to cancel.' }); + } else { + let dialog = this.state.dialog; + dialog.type = "confirmation"; + dialog.header = "Confirm to Cancel Scheduling Unit(s)"; + dialog.actions = [{ id: 'yes', title: 'Yes', callback: this.cancelSchedulingUnit, className:(this.props.project)?"dialog-btn": "" }, + { id: 'no', title: 'No', callback: this.closeDialog, className:(this.props.project)?"dialog-btn": "" }]; + dialog.detail = "Cancelling the scheduling unit means it will no longer be executed / will be aborted. This action cannot be undone. Already finished/cancelled scheduling unit(s) will be ignored. Do you want to proceed?"; + dialog.content = this.getSUCancelConfirmContent; + dialog.submit = this.cancelSchedulingUnit; + dialog.width = '55vw'; + dialog.showIcon = false; + this.setState({ dialog: dialog, dialogVisible: true }); + } + } + + /** + * Function to cancel all selected Scheduling Unit blueprints if its status is not one of the end statuses + * and update their status on successful cancellation. + */ + async cancelSchedulingUnit() { + let schedulingUnits = this.state.scheduleunit; + let selectedBlueprints = this.selectedRows.filter(su => {return su.type === 'Blueprint'}); + let cancelledSchedulingUnits = [] + for (const selectedSU of selectedBlueprints) { + if (this.SU_END_STATUSES.indexOf(selectedSU.status) < 0) { + const cancelledSU = await ScheduleService.cancelSchedulingUnit(selectedSU.id); + let schedulingUnit = _.find(schedulingUnits, {'id': selectedSU.id, type: 'Blueprint'}); + if (cancelledSU) { + schedulingUnit.status = cancelledSU.status; + } + cancelledSchedulingUnits.push({ + suId: schedulingUnit.id, suName: schedulingUnit.name, type: schedulingUnit.type, + status: schedulingUnit.status.toLowerCase()==='cancelled'?'Cancelled': 'Error Occured' + }); + } + } + let dialog = this.state.dialog; + dialog.type = "confirmation"; + dialog.header = "Cancel Scheduling Unit(s) Status"; + dialog.actions = [{ id: 'no', title: 'Ok', callback: this.closeDialog, className:(this.props.project)?"dialog-btn": "" }]; + dialog.detail = "" + dialog.content = this.getSUCancelStatusContent; + dialog.submit = this.closeDialog; + dialog.width = '55vw'; + dialog.showIcon = false; + this.selectedRows = []; + this.setState({ scheduleunit: schedulingUnits, cancelledSchedulingUnits: cancelledSchedulingUnits, dialog: dialog, dialogVisible: true }); + } + /** * Callback function to close the dialog prompted. */ @@ -616,9 +742,14 @@ class SchedulingUnitList extends Component{ <div > <span className="p-float-label"> {this.state.scheduleunit && this.state.scheduleunit.length > 0 && + <> + <a href="#" onClick={this.confirmCancelSchedulingUnit} title="Cancel selected Scheduling Unit(s)"> + <i class="fa fa-ban" aria-hidden="true" ></i> + </a> <a href="#" onClick={this.checkAndDeleteSchedulingUnit} title="Delete selected Scheduling Unit(s)"> <i class="fa fa-trash" aria-hidden="true" ></i> </a> + </> } </span> </div> 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 5bd338d371491e9d414c05a521374235f113afc5..d476f34454dd12c2749043281a47aad07806f378 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/ViewSchedulingUnit.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/ViewSchedulingUnit.js @@ -30,12 +30,16 @@ class ViewSchedulingUnit extends Component { lsKeySortColumn = 'SortDataViewSchedulingUnit'; defaultSortColumn = []; ignoreSorting = ['status logs']; + SU_NOT_STARTED_STATUSES = ['defined', 'schedulable', 'scheduled']; + SU_ACTIVE_STATUSES = ['started', 'observing', 'observed', 'processing', 'processed', 'ingesting']; + SU_END_STATUSES = ['finished', 'error', 'cancelled']; + TASK_END_STATUSES = ['finished', 'error', 'cancelled']; constructor(props) { super(props); this.setToggleBySorting(); this.state = { scheduleunit: null, - schedule_unit_task: [], + schedulingUnitTasks: [], isLoading: true, showStatusLogs: false, showTaskRelationDialog: false, @@ -152,16 +156,22 @@ class ViewSchedulingUnit extends Component { this.selectedRows = []; this.confirmDeleteTasks = this.confirmDeleteTasks.bind(this); + this.confirmCancelTasks = this.confirmCancelTasks.bind(this); this.onRowSelection = this.onRowSelection.bind(this); this.deleteTasks = this.deleteTasks.bind(this); this.deleteSchedulingUnit = this.deleteSchedulingUnit.bind(this); - this.getTaskDialogContent = this.getTaskDialogContent.bind(this); + this.getTaskDeleteDialogContent = this.getTaskDeleteDialogContent.bind(this); + this.getTaskCancelConfirmContent = this.getTaskCancelConfirmContent.bind(this); + this.getTaskCancelStatusContent = this.getTaskCancelStatusContent.bind(this); this.getSUDialogContent = this.getSUDialogContent.bind(this); this.checkAndCreateBlueprint = this.checkAndCreateBlueprint.bind(this); this.createBlueprintTree = this.createBlueprintTree.bind(this); this.closeDialog = this.closeDialog.bind(this); this.showTaskRelationDialog = this.showTaskRelationDialog.bind(this); this.showDeleteSUConfirmation = this.showDeleteSUConfirmation.bind(this); + this.showCancelSUConfirmation = this.showCancelSUConfirmation.bind(this); + this.cancelSchedulingUnit = this.cancelSchedulingUnit.bind(this); + this.cancelTasks = this.cancelTasks.bind(this); } componentDidUpdate(prevProps, prevState) { @@ -263,7 +273,7 @@ class ViewSchedulingUnit extends Component { scheduleunitId: schedule_id, scheduleunit: schedulingUnit, scheduleunitType: schedule_type, - schedule_unit_task: tasks, + schedulingUnitTasks: tasks, isLoading: false, stationGroup: targetObservation ? targetObservation.specifications_doc.station_groups : [], redirect: null, @@ -308,6 +318,12 @@ class ViewSchedulingUnit extends Component { actOn: 'click', props: { callback: this.checkAndCreateBlueprint }, }); } else { + this.actions.unshift({ + icon: 'fa-ban', type: 'button', actOn: 'click', + title: this.SU_END_STATUSES.indexOf(this.state.scheduleunit.status.toLowerCase())>=0?'Cannot Cancel Scheduling Unit':'Cancel Scheduling Unit', + disabled:this.SU_END_STATUSES.indexOf(this.state.scheduleunit.status.toLowerCase())>=0, + props: { callback: this.showCancelSUConfirmation } + }); 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' }); } @@ -458,7 +474,7 @@ class ViewSchedulingUnit extends Component { * Callback function to close the dialog prompted. */ closeDialog() { - this.setState({ dialogVisible: false }); + this.setState({ dialogVisible: false, cancelledTasks: [] }); } onRowSelection(selectedRows) { @@ -476,7 +492,7 @@ class ViewSchedulingUnit extends Component { dialog.type = "confirmation"; dialog.header = "Confirm to Delete Task(s)"; dialog.detail = "Do you want to delete the selected Task(s)?"; - dialog.content = this.getTaskDialogContent; + dialog.content = this.getTaskDeleteDialogContent; dialog.actions = [{ id: 'yes', title: 'Yes', callback: this.deleteTasks }, { id: 'no', title: 'No', callback: this.closeDialog }]; dialog.onSubmit = this.deleteTasks; @@ -501,9 +517,9 @@ class ViewSchedulingUnit extends Component { } /** - * Prepare Task(s) details to show on confirmation dialog + * Prepare Task(s) details to show on confirmation dialog before deleting */ - getTaskDialogContent() { + getTaskDeleteDialogContent() { let selectedTasks = []; for (const obj of this.selectedRows) { selectedTasks.push({ @@ -576,6 +592,158 @@ class ViewSchedulingUnit extends Component { } } + /** + * Show confirmation dialog before cancelling the scheduling unit. + */ + showCancelSUConfirmation() { + let dialog = this.state.dialog; + dialog.type = "confirmation"; + dialog.header = "Confirm to Cancel Scheduling Unit"; + dialog.actions = [{ id: 'yes', title: 'Yes', callback: this.cancelSchedulingUnit }, + { id: 'no', title: 'No', callback: this.closeDialog }]; + if (this.SU_NOT_STARTED_STATUSES.indexOf(this.state.scheduleunit.status) >= 0) { + dialog.detail = "Cancelling this scheduling unit means it will no longer be executed. This action cannot be undone. Do you want to proceed?"; + } else if (this.SU_ACTIVE_STATUSES.indexOf(this.state.scheduleunit.status) >= 0) { + dialog.detail = "Cancelling this scheduling unit means it will be aborted. This action cannot be undone. Do you want to proceed?"; + } + dialog.submit = this.cancelSchedulingUnit; + dialog.width = '40vw'; + dialog.showIcon = true; + this.setState({ dialog: dialog, dialogVisible: true }); + } + + /** + * Function to cancel the scheduling unit and update its status and status of tasks if succeeeded. + */ + async cancelSchedulingUnit() { + let schedulingUnit = this.state.scheduleunit; + let cancelledSU = await ScheduleService.cancelSchedulingUnit(schedulingUnit.id); + if (!cancelledSU) { + appGrowl.show({ severity: 'error', summary: 'error', detail: 'Error while cancelling Scheduling Unit' }); + this.setState({ dialogVisible: false }); + } else { + schedulingUnit.status = cancelledSU.status; + let actions = this.state.actions; + let cancelAction = _.find(actions, ['icon', 'fa-ban']); + cancelAction.disabled = true; + const cancelActionIndex = _.findIndex(actions, {'icon': 'fa-ban'}); + actions.splice(cancelActionIndex, 1, cancelAction); + const cancelledSUTasks = cancelledSU.task_blueprints; + let suTasks = this.state.schedulingUnitTasks; + for (let suTask of suTasks) { + const cancelledSUTask = _.find(cancelledSUTasks, {'id': suTask.id}); + suTask.status = cancelledSUTask.status; + } + appGrowl.show({ severity: 'success', summary: 'Success', detail: 'Scheduling Unit is cancelled successfully' }); + this.setState({ dialogVisible: false, scheduleunit: schedulingUnit, + schedulingUnitTasks:suTasks, actions: actions}); + } + } + + /** + * Prepare Task(s) details to show on confirmation dialog before cancelling + */ + getTaskCancelConfirmContent() { + let selectedTasks = []; + for (const obj of this.selectedRows) { + if (this.TASK_END_STATUSES.indexOf(obj.status) < 0) { + selectedTasks.push({ + id: obj.id, suId: this.state.scheduleunit.id, suName: this.state.scheduleunit.name, + taskId: obj.id, controlId: obj.subTaskID, taskName: obj.name, status: obj.status + }); + } + } + return <> + <DataTable value={selectedTasks} resizableColumns columnResizeMode="expand" className="card" style={{ paddingLeft: '0em' }}> + <Column field="suId" header="Scheduling Unit Id"></Column> + <Column field="suName" header="Scheduling Unit Name"></Column> + <Column field="taskId" header="Task Id"></Column> + <Column field="controlId" header="Control Id"></Column> + <Column field="taskName" header="Task Name"></Column> + <Column field="status" header="Status"></Column> + </DataTable> + </> + } + + /** + * Prepare Task(s) details to show status of Task cancellationn + */ + getTaskCancelStatusContent() { + let cancelledTasks = this.state.cancelledTasks; + return <> + <DataTable value={cancelledTasks} resizableColumns columnResizeMode="expand" className="card" style={{ paddingLeft: '0em' }}> + <Column field="suId" header="Scheduling Unit Id"></Column> + <Column field="suName" header="Scheduling Unit Name"></Column> + <Column field="taskId" header="Task Id"></Column> + <Column field="controlId" header="Control Id"></Column> + <Column field="taskName" header="Task Name"></Column> + <Column field="status" header="Status"></Column> + </DataTable> + </> + } + + /** + * Function to get confirmation before cancelling all selected task blueprints if the task status is + * not one of the end statuses. If no selected task is cancellable, show info to select a cancellable task. + * + */ + confirmCancelTasks() { + let selectedBlueprints = this.selectedRows.filter(task => { + return task.tasktype === 'Blueprint' && + this.TASK_END_STATUSES.indexOf(task.status)<0}); + if (selectedBlueprints.length === 0) { + appGrowl.show({ severity: 'info', summary: 'Select Row', + detail: 'Select atleast one cancellable Task Blueprint to cancel.' }); + } else { + let dialog = this.state.dialog; + dialog.type = "confirmation"; + dialog.header = "Confirm to Cancel Task(s)"; + dialog.actions = [{ id: 'yes', title: 'Yes', callback: this.cancelTasks }, + { id: 'no', title: 'No', callback: this.closeDialog }]; + dialog.detail = "Cancelling the task means it will no longer be executed / will be aborted. This action cannot be undone. Do you want to proceed?"; + dialog.content = this.getTaskCancelConfirmContent; + dialog.submit = this.cancelTasks; + dialog.width = '55vw'; + dialog.showIcon = false; + this.setState({ dialog: dialog, dialogVisible: true }); + } + } + + /** + * Function to cancel all selected task blueprints if the task status is not one of the end statuses + * and update their status on successful cancellation. + */ + async cancelTasks() { + let schedulingUnitTasks = this.state.schedulingUnitTasks; + let selectedBlueprints = this.selectedRows.filter(task => {return task.tasktype === 'Blueprint'}); + let cancelledTasks = [] + for (const selectedTask of selectedBlueprints) { + if (this.TASK_END_STATUSES.indexOf(selectedTask.status) < 0) { + const cancelledTask = await TaskService.cancelTask(selectedTask.id); + let task = _.find(schedulingUnitTasks, {'id': selectedTask.id, tasktype: 'Blueprint'}); + if (cancelledTask) { + task.status = cancelledTask.status; + } + cancelledTasks.push({ + id: task.id, suId: this.state.scheduleunit.id, suName: this.state.scheduleunit.name, + taskId: task.id, controlId: task.subTaskID, taskName: task.name, + status: task.status.toLowerCase()==='cancelled'?'Cancelled': 'Error Occured' + }); + } + } + let dialog = this.state.dialog; + dialog.type = "confirmation"; + dialog.header = "Cancel Task(s) Status"; + dialog.actions = [{ id: 'no', title: 'Ok', callback: this.closeDialog }]; + dialog.detail = "" + dialog.content = this.getTaskCancelStatusContent; + dialog.submit = this.closeDialog; + dialog.width = '55vw'; + dialog.showIcon = false; + this.selectedRows = []; + this.setState({ schedulingUnitTasks: schedulingUnitTasks, cancelledTasks: cancelledTasks, dialog: dialog, dialogVisible: true }); + } + render() { if (this.state.redirect) { return <Redirect to={{ pathname: this.state.redirect }}></Redirect> @@ -670,17 +838,22 @@ class ViewSchedulingUnit extends Component { <div className="delete-option"> <div > <span className="p-float-label"> - {this.state.schedule_unit_task && this.state.schedule_unit_task.length > 0 && - <a href="#" onClick={this.confirmDeleteTasks} title="Delete selected Task(s)"> - <i class="fa fa-trash" aria-hidden="true" ></i> - </a> + {this.state.schedulingUnitTasks && this.state.schedulingUnitTasks.length > 0 && + <> + <a href="#" onClick={this.confirmCancelTasks} title="Cancel selected Task(s)"> + <i class="fa fa-ban" aria-hidden="true" ></i> + </a> + <a href="#" onClick={this.confirmDeleteTasks} title="Delete selected Task(s)"> + <i class="fa fa-trash" aria-hidden="true" ></i> + </a> + </> } </span> </div> </div> - {this.state.isLoading ? <AppLoader /> : (this.state.schedule_unit_task.length > 0) ? + {this.state.isLoading ? <AppLoader /> : (this.state.schedulingUnitTasks.length > 0) ? <ViewTable - data={this.state.schedule_unit_task} + data={this.state.schedulingUnitTasks} defaultcolumns={this.state.defaultcolumns} optionalcolumns={this.state.optionalcolumns} columnclassname={this.state.columnclassname} diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/list.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/list.js index 4e217041ca0662dc7a9522dbcd78e08808ac481e..3b7d0f0060f6eb6a7e89f54253bdc8a2efdc82bb 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/list.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/list.js @@ -21,6 +21,7 @@ export class TaskList extends Component { lsKeySortColumn = "TaskListSortData"; // The following values should be lower case ignoreSorting = ['status logs']; + TASK_END_STATUSES = ['finished', 'error', 'cancelled']; constructor(props) { super(props); this.state = { @@ -129,15 +130,20 @@ export class TaskList extends Component { "Data size on Disk": "filter-input-50", "Subtask Content": "filter-input-75", "BluePrint / Task Draft link": "filter-input-50", - }] + }], + actions: [] }; this.selectedRows = []; this.subtaskTemplates = []; this.confirmDeleteTasks = this.confirmDeleteTasks.bind(this); + this.confirmCancelTasks = this.confirmCancelTasks.bind(this); this.onRowSelection = this.onRowSelection.bind(this); this.deleteTasks = this.deleteTasks.bind(this); + this.cancelTasks = this.cancelTasks.bind(this); this.closeDialog = this.closeDialog.bind(this); - this.getTaskDialogContent = this.getTaskDialogContent.bind(this); + this.getTaskDeleteDialogContent = this.getTaskDeleteDialogContent.bind(this); + this.getTaskCancelConfirmContent = this.getTaskCancelConfirmContent.bind(this); + this.getTaskCancelStatusContent = this.getTaskCancelStatusContent.bind(this); } subtaskComponent = (task) => { @@ -260,7 +266,12 @@ export class TaskList extends Component { tasks = await this.formatDataProduct(tasks); allTasks = [...allTasks, ...tasks]; } - this.setState({ tasks: allTasks, isLoading: false }); + const actions = [{icon: 'fa fa-ban', title: 'Cancel Task(s)', + type: 'button', actOn: 'click', props: { callback: this.confirmCancelTasks }}, + {icon: 'fa fa-trash', title: 'Delete Task(s)', + type: 'button', actOn: 'click', props: { callback: this.confirmDeleteTasks }} + ]; + this.setState({ tasks: allTasks, isLoading: false, actions: actions }); }); } @@ -282,9 +293,9 @@ export class TaskList extends Component { } /** - * Prepare Task(s) details to show on confirmation dialog + * Prepare Task(s) details to show on confirmation dialog before deleting */ - getTaskDialogContent() { + getTaskDeleteDialogContent() { let selectedTasks = []; for (const obj of this.selectedRows) { selectedTasks.push({ @@ -310,7 +321,7 @@ export class TaskList extends Component { dialog.type = "confirmation"; dialog.header = "Confirm to Delete Task(s)"; dialog.detail = "Do you want to delete the selected Task(s)?"; - dialog.content = this.getTaskDialogContent; + dialog.content = this.getTaskDeleteDialogContent; dialog.actions = [{ id: 'yes', title: 'Yes', callback: this.deleteTasks }, { id: 'no', title: 'No', callback: this.closeDialog }]; dialog.onSubmit = this.deleteTasks; @@ -341,11 +352,136 @@ export class TaskList extends Component { } } + /** + * Prepare Task(s) details to show on confirmation dialog before cancelling + */ + getTaskCancelConfirmContent() { + let selectedTasks = [], ignoredTasks = []; + for (const obj of this.selectedRows) { + if (this.TASK_END_STATUSES.indexOf(obj.status) < 0) { + selectedTasks.push({ + id: obj.id, suId: obj.schedulingUnitId, suName: obj.schedulingUnitName, + taskId: obj.id, controlId: obj.subTaskID, taskName: obj.name, status: obj.status + }); + } else { + ignoredTasks.push({ + id: obj.id, suId: obj.schedulingUnitId, suName: obj.schedulingUnitName, + taskId: obj.id, controlId: obj.subTaskID, taskName: obj.name, status: obj.status + }); + } + } + return <> + <div style={{marginTop: '1em'}}> + <b>Task(s) that can be cancelled</b> + <DataTable value={selectedTasks} resizableColumns columnResizeMode="expand" className="card" style={{ paddingLeft: '0em' }}> + <Column field="suId" header="Scheduling Unit Id"></Column> + <Column field="suName" header="Scheduling Unit Name"></Column> + <Column field="taskId" header="Task Id"></Column> + <Column field="controlId" header="Control Id"></Column> + <Column field="taskName" header="Task Name"></Column> + <Column field="status" header="Status"></Column> + </DataTable> + </div> + {ignoredTasks.length > 0 && + <div style={{marginTop: '1em'}}> + <b>Task(s) that will be ignored</b> + <DataTable value={ignoredTasks} resizableColumns columnResizeMode="expand" className="card" style={{ paddingLeft: '0em' }}> + <Column field="suId" header="Scheduling Unit Id"></Column> + <Column field="suName" header="Scheduling Unit Name"></Column> + <Column field="taskId" header="Task Id"></Column> + <Column field="controlId" header="Control Id"></Column> + <Column field="taskName" header="Task Name"></Column> + <Column field="status" header="Status"></Column> + </DataTable> + </div> + } + </> + } + + /** + * Prepare Task(s) details to show status of Task cancellationn + */ + getTaskCancelStatusContent() { + let cancelledTasks = this.state.cancelledTasks; + return <> + <DataTable value={cancelledTasks} resizableColumns columnResizeMode="expand" className="card" style={{ paddingLeft: '0em' }}> + <Column field="suId" header="Scheduling Unit Id"></Column> + <Column field="suName" header="Scheduling Unit Name"></Column> + <Column field="taskId" header="Task Id"></Column> + <Column field="controlId" header="Control Id"></Column> + <Column field="taskName" header="Task Name"></Column> + <Column field="status" header="Status"></Column> + </DataTable> + </> + } + + /** + * Function to get confirmation before cancelling all selected task blueprints if the task status is + * not one of the end statuses. If no selected task is cancellable, show info to select a cancellable task. + * + */ + confirmCancelTasks() { + let selectedBlueprints = this.selectedRows.filter(task => { + return task.tasktype === 'Blueprint' && + this.TASK_END_STATUSES.indexOf(task.status)<0}); + if (selectedBlueprints.length === 0) { + appGrowl.show({ severity: 'info', summary: 'Select Row', + detail: 'Select atleast one cancellable Task Blueprint to cancel.' }); + } else { + let dialog = this.state.dialog; + dialog.type = "confirmation"; + dialog.header = "Confirm to Cancel Task(s)"; + dialog.actions = [{ id: 'yes', title: 'Yes', callback: this.cancelTasks }, + { id: 'no', title: 'No', callback: this.closeDialog }]; + dialog.detail = "Cancelling the task means, it will no longer be executed / will be aborted. This action cannot be undone. Already finished/cancelled task(s) will be ignored. Do you want to proceed?"; + dialog.content = this.getTaskCancelConfirmContent; + dialog.submit = this.cancelTasks; + dialog.width = '55vw'; + dialog.showIcon = false; + this.setState({ dialog: dialog, dialogVisible: true }); + } + } + + /** + * Function to cancel all selected task blueprints if the task status is not one of the end statuses + * and update their status on successful cancellation. + */ + async cancelTasks() { + let tasks = this.state.tasks; + let selectedBlueprints = this.selectedRows.filter(task => {return task.tasktype === 'Blueprint'}); + let cancelledTasks = [] + for (const selectedTask of selectedBlueprints) { + if (this.TASK_END_STATUSES.indexOf(selectedTask.status) < 0) { + const cancelledTask = await TaskService.cancelTask(selectedTask.id); + let task = _.find(tasks, {'id': selectedTask.id, tasktype: 'Blueprint'}); + if (cancelledTask) { + task.status = cancelledTask.status; + } + cancelledTasks.push({ + id: task.id, suId: task.schedulingUnitId, suName: task.schedulingUnitName, + taskId: task.id, controlId: task.subTaskID, taskName: task.name, + status: task.status.toLowerCase()==='cancelled'?'Cancelled': 'Error Occured' + }); + } + } + let dialog = this.state.dialog; + dialog.type = "confirmation"; + dialog.header = "Cancel Task(s) Status"; + dialog.actions = [{ id: 'no', title: 'Ok', callback: this.closeDialog }]; + dialog.detail = "" + dialog.content = this.getTaskCancelStatusContent; + dialog.submit = this.closeDialog; + dialog.width = '55vw'; + dialog.showIcon = false; + this.selectedRows = []; + this.setState({ tasks: tasks, cancelledTasks: cancelledTasks, dialog: dialog, dialogVisible: true }); + } + /** * Callback function to close the dialog prompted. */ closeDialog() { - this.setState({ dialogVisible: false }); + this.setState({ dialogVisible: false, cancelledTasks: [] }); } onRowSelection(selectedRows) { @@ -360,18 +496,9 @@ export class TaskList extends Component { return ( <React.Fragment> - <PageHeader location={this.props.location} title={'Task - List'} /> + <PageHeader location={this.props.location} title={'Task - List'} actions={this.state.actions}/> {this.state.isLoading ? <AppLoader /> : <> - <div className="delete-option"> - <div > - <span className="p-float-label"> - <a href="#" onClick={this.confirmDeleteTasks} title="Delete selected Task(s)"> - <i class="fa fa-trash" aria-hidden="true" ></i> - </a> - </span> - </div> - </div> <ViewTable data={this.state.tasks} defaultcolumns={this.state.defaultcolumns} diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/view.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/view.js index bde2f9d803f8bb2cc98f7fa7bb4a3bbe2aa11a1b..6427470c7d4e01cde703a45ac8e644467c7cf764 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/view.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/view.js @@ -17,28 +17,27 @@ import { Column } from 'primereact/column'; export class TaskView extends Component { // DATE_FORMAT = 'YYYY-MMM-DD HH:mm:ss'; + TASK_NOT_STARTED_STATUSES = ['defined', 'schedulable', 'scheduled']; + TASK_ACTIVE_STATUSES = ['started', 'observing', 'observed', 'processing', 'processed', 'ingesting']; + TASK_END_STATUSES = ['finished', 'error', 'cancelled']; + constructor(props) { super(props); this.state = { isLoading: true, confirmDialogVisible: false, - hasBlueprint: true + hasBlueprint: true, + dialog: {} }; - this.showIcon = false; - this.dialogType = "confirmation"; - this.dialogHeader = ""; - this.dialogMsg = ""; - this.dialogContent = ""; - this.callBackFunction = ""; - this.dialogWidth = '40vw'; - this.onClose = this.close; - this.onCancel =this.close; this.setEditorFunction = this.setEditorFunction.bind(this); this.deleteTask = this.deleteTask.bind(this); - this.showConfirmation = this.showConfirmation.bind(this); - this.close = this.close.bind(this); - this.getDialogContent = this.getDialogContent.bind(this); + this.showDeleteConfirmation = this.showDeleteConfirmation.bind(this); + this.closeDialog = this.closeDialog.bind(this); + this.getTaskDeleteDialogContent = this.getTaskDeleteDialogContent.bind(this); + this.showCancelConfirmation = this.showCancelConfirmation.bind(this); + this.cancelTask = this.cancelTask.bind(this); + if (this.props.match.params.id) { this.state.taskId = this.props.match.params.id; @@ -127,23 +126,22 @@ export class TaskView extends Component { /** * Show confirmation dialog */ - showConfirmation() { - this.dialogType = "confirmation"; - this.dialogHeader = "Confirm to Delete Task"; - this.showIcon = false; - this.dialogMsg = "Do you want to delete this Task?"; - this.dialogWidth = '55vw'; - this.dialogContent = this.getDialogContent; - this.callBackFunction = this.deleteTask; - this.onClose = this.close; - this.onCancel =this.close; - this.setState({confirmDialogVisible: true}); + showDeleteConfirmation() { + let dialog = this.state.dialog; + dialog.type = "confirmation"; + dialog.header = "Confirm to Delete Task"; + dialog.showIcon = false; + dialog.detail = "Do you want to delete this Task?"; + dialog.width = '55vw'; + dialog.content = this.getTaskDeleteDialogContent; + dialog.onSubmit = this.deleteTask; + this.setState({dialog: dialog, confirmDialogVisible: true}); } /** * Prepare Task details to show on confirmation dialog */ - getDialogContent() { + getTaskDeleteDialogContent() { let selectedTasks = [{suId: this.state.schedulingUnit.id, suName: this.state.schedulingUnit.name, taskId: this.state.task.id, controlId: this.state.task.subTaskID, taskName: this.state.task.name, status: this.state.task.status}]; return <> @@ -158,7 +156,7 @@ export class TaskView extends Component { </> } - close() { + closeDialog() { this.setState({confirmDialogVisible: false}); } @@ -185,6 +183,43 @@ export class TaskView extends Component { } } + /** + * Show confirmation dialog before cancelling the task. + */ + showCancelConfirmation() { + let dialog = this.state.dialog; + dialog.type = "confirmation"; + dialog.header = "Confirm to Cancel Task"; + dialog.actions = [{ id: 'yes', title: 'Yes', callback: this.cancelTask }, + { id: 'no', title: 'No', callback: this.closeDialog }]; + if (this.TASK_NOT_STARTED_STATUSES.indexOf(this.state.task.status) >= 0) { + dialog.detail = "Cancelling this task means it will no longer be executed. This action cannot be undone. Do you want to proceed?"; + } else if (this.TASK_ACTIVE_STATUSES.indexOf(this.state.task.status) >= 0) { + dialog.detail = "Cancelling this task means it will be aborted. This action cannot be undone. Do you want to proceed?"; + } + dialog.submit = this.cancelTask; + dialog.width = '40vw'; + dialog.showIcon = true; + this.setState({ dialog: dialog, confirmDialogVisible: true }); + } + + /** + * Function to cancel the task and update its status. + */ + async cancelTask() { + let task = this.state.task; + let cancelledTask = await TaskService.cancelTask(task.id); + if (!cancelledTask) { + appGrowl.show({ severity: 'error', summary: 'error', detail: 'Error while cancelling Scheduling Unit' }); + this.setState({ dialogVisible: false }); + } else { + task.status = cancelledTask.status; + let actions = this.state.actions; + appGrowl.show({ severity: 'success', summary: 'Success', detail: 'Scheduling Unit is cancelled successfully' }); + this.setState({ confirmDialogVisible: false, task: task, actions: actions}); + } + } + render() { if (this.state.redirect) { return <Redirect to={ {pathname: this.state.redirect} }></Redirect> @@ -212,9 +247,16 @@ export class TaskView extends Component { } else { actions = [{ icon: 'fa-lock', title: 'Cannot edit blueprint'}]; + if (this.state.task) { + actions.push({icon: 'fa-ban', type: 'button', actOn: 'click', + title: this.TASK_END_STATUSES.indexOf(this.state.task.status.toLowerCase())>=0?'Cannot Cancel Task':'Cancel Task', + disabled:this.TASK_END_STATUSES.indexOf(this.state.task.status.toLowerCase())>=0, + props: { callback: this.showCancelConfirmation } + }); + } } actions.push({icon: 'fa fa-trash',title:this.state.hasBlueprint? 'Cannot delete Draft when Blueprint exists':'Delete Task', - type: 'button', disabled: this.state.hasBlueprint, actOn: 'click', props:{ callback: this.showConfirmation}}); + type: 'button', disabled: this.state.hasBlueprint, actOn: 'click', props:{ callback: this.showDeleteConfirmation}}); actions.push({ icon: 'fa-window-close', link: this.props.history.goBack, title:'Click to Close Task', props : { pathname:'/schedulingunit' }}); @@ -283,8 +325,10 @@ export class TaskView extends Component { <span className="col-lg-4 col-md-4 col-sm-12">{this.state.task.end_time?moment(this.state.task.end_time,moment.ISO_8601).format(UIConstants.CALENDAR_DATETIME_FORMAT):""}</span> </div> <div className="p-grid"> - <label className="col-lg-2 col-md-2 col-sm-12">Tags</label> - <Chips className="col-lg-4 col-md-4 col-sm-12 chips-readonly" disabled value={this.state.task.tags}></Chips> + {/* <label className="col-lg-2 col-md-2 col-sm-12">Tags</label> + <Chips className="col-lg-4 col-md-4 col-sm-12 chips-readonly" disabled value={this.state.task.tags}></Chips> */} + <label className="col-lg-2 col-md-2 col-sm-12">Status</label> + <span className="col-lg-4 col-md-4 col-sm-12">{this.state.task.status}</span> {this.state.schedulingUnit && <> <label className="col-lg-2 col-md-2 col-sm-12">Scheduling Unit</label> @@ -339,10 +383,10 @@ export class TaskView extends Component { </div> </React.Fragment> } - <CustomDialog type={this.dialogType} visible={this.state.confirmDialogVisible} width={this.dialogWidth} - header={this.dialogHeader} message={this.dialogMsg} - content={this.dialogContent} onClose={this.onClose} onCancel={this.onCancel} onSubmit={this.callBackFunction} - showIcon={this.showIcon} actions={this.actions}> + <CustomDialog type="confirmation" visible={this.state.confirmDialogVisible} width={this.state.dialog.width} + header={this.state.dialog.header} message={this.state.dialog.detail} + content={this.state.dialog.content} onClose={this.closeDialog} onCancel={this.closeDialog} onSubmit={this.callBackFunction} + showIcon={this.state.dialog.showIcon} actions={this.state.dialog.actions}> </CustomDialog> </React.Fragment> ); 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 6314ec54e7d3c3ed3952451453f255df559605d0..019b5c4b0595e58f4ab8b0223455f8f6d9871085 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/services/schedule.service.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/services/schedule.service.js @@ -693,6 +693,16 @@ const ScheduleService = { console.error(error); return false; } + }, + cancelSchedulingUnit: async(id) => { + let cancelledSU = null; + try { + const url = `/api/scheduling_unit_blueprint_extended/${id}/cancel/`; + cancelledSU = (await axios.post(url, {})).data; + } catch(error) { + console.error(error); + } + return cancelledSU; } } diff --git a/SAS/TMSS/frontend/tmss_webapp/src/services/task.service.js b/SAS/TMSS/frontend/tmss_webapp/src/services/task.service.js index fd4b6d769ecc53b022be3da580317ebbae11a24d..e1f3d57b8cc4a2e92d3d717c5e68c1915042e87c 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/services/task.service.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/services/task.service.js @@ -266,6 +266,21 @@ const TaskService = { console.error(error); return false; } + }, + /** + * Cancel task + * @param {*} type + * @param {*} id + */ + cancelTask: async function(id) { + try { + const url = `/api/task_blueprint/${id}/cancel`; + await axios.post(url, {}); + return true; + } catch(error) { + console.error(error); + return false; + } } }