diff --git a/SAS/TMSS/frontend/tmss_webapp/src/components/ViewTable.js b/SAS/TMSS/frontend/tmss_webapp/src/components/ViewTable.js index 1817c879c51a685360e6286afc687fc25e74a996..a9bd822854a2a0899fa51477c09df4873bc2bea2 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/components/ViewTable.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/components/ViewTable.js @@ -1,5 +1,5 @@ import React, { useRef, useState } from "react"; -import { useSortBy, useTable, useFilters, useGlobalFilter, useAsyncDebounce, usePagination, useRowSelect, useColumnOrder } from 'react-table' +import { useFlexLayout , useResizeColumns, useSortBy, useTable, useFilters, useGlobalFilter, useAsyncDebounce, usePagination, useRowSelect, useColumnOrder } from 'react-table' import matchSorter from 'match-sorter' import _, { filter } from 'lodash'; import moment from 'moment'; @@ -65,6 +65,7 @@ let dragged = null; let reorder = []; //let confirmDatePlugin = new confirmDatePlugin(); // Define a default UI for filtering +let fixedColumns = ['Select', 'Action', 'Status Logs', 'View Summary']; const getItemStyle = ({ isDragging, isDropAnimating }, draggableStyle) => ({ ...draggableStyle, @@ -290,7 +291,7 @@ function SelectColumnFilter({ <div onClick={e => { e.stopPropagation() }}> <select title={(tableToolTipsState[Header])?tableToolTipsState[Header]:"Select a value from list to search"} - className= {columnclassname[0][Header]} + // className= {columnclassname[0][Header]} style={{ height: '24.2014px', border: '1px solid lightgrey', @@ -428,37 +429,34 @@ function MultiSelectColumnFilter({ <label htmlFor="filtertype2">All</label> </div> <div style={{ position: 'relative', display: 'flex'}} > - <div> - <MultiSelect data-testid="multi-select" id="multi-select" optionLabel="value" optionValue="value" filter={true} - value={value} - options={options} - onChange={e => { - setValue(e.target.value); - setFilter(e.target.value || undefined); - setFiltertype(filtertype); - setFiltered(true); - if(storeFilter) { - if (e.target.value.length > 0) { - TableUtil.saveFilter(currentTableName, Header, e.target.value); - TableUtil.saveFilter(currentTableName, `${Header}-FilterOption`, filtertype); - } else { - TableUtil.clearColumnFilter(currentTableName, Header); - TableUtil.clearColumnFilter(currentTableName, `${Header}-FilterOption`); - } + <MultiSelect data-testid="multi-select" id="multi-select" optionLabel="value" optionValue="value" filter={true} + value={value} + options={options} + onChange={e => { + setValue(e.target.value); + setFilter(e.target.value || undefined); + setFiltertype(filtertype); + setFiltered(true); + if(storeFilter) { + if (e.target.value.length > 0) { + TableUtil.saveFilter(currentTableName, Header, e.target.value); + TableUtil.saveFilter(currentTableName, `${Header}-FilterOption`, filtertype); + } else { + TableUtil.clearColumnFilter(currentTableName, Header); + TableUtil.clearColumnFilter(currentTableName, `${Header}-FilterOption`); } - }} - maxSelectedLabels="1" - selectedItemsLabel="{0} Selected" - className="multi-select" - tooltip={(tableToolTipsState[Header])?tableToolTipsState[Header]:"Select one or more value from list to search"} - /> - </div> + } + }} + maxSelectedLabels="1" + selectedItemsLabel="{0} Selected" + className="multi-select" + tooltip={(tableToolTipsState[Header])?tableToolTipsState[Header]:"Select one or more value from list to search"} + style={{width: '95%'}} + /> {doServersideFilter && - <div> - <button className="p-link" onClick={callSearchFunc} > - <i className="pi pi-search search-btn" /> - </button> - </div> + <button className="p-link" onClick={callSearchFunc} > + <i className="pi pi-search search-btn" /> + </button> } </div> </div> @@ -535,7 +533,6 @@ function MultiSelectFilter({ return ( <div onClick={e => { e.stopPropagation()}}> <div style={{ position: 'relative', display: 'flex'}} > - <diV> <MultiSelect data-testid="multi-select" id="multi-select" optionLabel="name" optionValue="value" filter //={!doServersideFilter} value={value} options={options} @@ -556,9 +553,8 @@ function MultiSelectFilter({ selectedItemsLabel="{0} Selected" className="multi-select" tooltip={(tableToolTipsState[Header])?tableToolTipsState[Header]:"Select one or more value from list and click search icon to search"} - style={{width: '8em'}} + style={{width: '85%'}} /> - </diV> {doServersideFilter && <div> <button className="p-link" onClick={callSearchFunc} > @@ -838,7 +834,7 @@ function FlatpickrRangeColumnFilter({ }; return ( <div className="table-filter" onClick={e => { e.stopPropagation() }}> - <Flatpickr data-enable-time data-input + <Flatpickr data-enable-time data-input className="flatpickr-range-filter" options={{ "inlineHideInput": true, "wrap": true, "enableSeconds": true, @@ -1249,7 +1245,7 @@ function RangeColumnFilter({ <> <div className="filter-slider-label"> <span style={{ float: "left" }}>{filterValue[0]}</span> - <span style={{ float: "right" }}>{min !== max ? filterValue[1] : ""}</span> + <span style={{ float: "right", marginRight: "5px" }}>{min !== max ? filterValue[1] : ""}</span> </div> <Slider value={filterValue} min={min} max={max} className="filter-slider" style={{}} @@ -1457,7 +1453,9 @@ function RankRangeFilter({ } }} style={{ - width: '75px', + minWidth: '48px', + maxWidth: '85px', + width:'100%', height: '25px' }} /> @@ -1497,7 +1495,9 @@ function RankRangeFilter({ } }} style={{ - width: '75px', + minWidth: '48px', + maxWidth: '85px', + width:'100%', height: '25px' }} /> @@ -1583,7 +1583,9 @@ function DurationRangeFilter({ } }} style={{ - width: '85px', + minWidth: '48px', + maxWidth: '85px', + width:'100%', height: '25px' }} /> @@ -1609,7 +1611,9 @@ function DurationRangeFilter({ } }} style={{ - width: '85px', + minWidth: '48px', + maxWidth: '85px', + width: '100%', height: '25px' }} /> @@ -2012,6 +2016,8 @@ function Table(props) { () => ({ // Let's set up our default Filter UI Filter: DefaultColumnFilter, + minWidth: 60, + maxWidth: 250 }), [] ) @@ -2069,7 +2075,9 @@ function Table(props) { usePagination, useRowSelect, useColumnOrder, - useExportData + useExportData, + useFlexLayout, + useResizeColumns ); const currentColOrder = React.useRef(); let pageCount = doServersideFilter?controlledPageCount:data.length; @@ -2354,15 +2362,21 @@ function Table(props) { key={column.id} draggableId={column.id} index={index} - isDragDisabled={_.includes(['Select', 'Action', 'Status Logs'], column.id)? true: false}> + isDragDisabled={_.includes(fixedColumns, column.id)? true: false}> {(provided, snapshot) => { + if (column.id !== 'actionpath') { return ( - <th onClick={() => { + <th className={column.id} className={_.includes(fixedColumns, column.id)?'fixed-column-td':''} style={{display: 'flex'}} + onClick={() => { if(!doServersideFilter) { - toggleBySorting({ 'id': column.id, desc: (column.isSortedDesc != undefined ? !column.isSortedDesc : false) }); + if(!column.disableSortBy) { + toggleBySorting({ 'id': column.id, desc: (column.isSortedDesc != undefined ? !column.isSortedDesc : false) }); + } } }}> - <div {...column.getHeaderProps(column.getSortByToggleProps())} > + <div style={{display:'flex'}}> + <div style={{display: 'grid',verticalAlign:'bottom'}}> + <div {...column.getHeaderProps(column.getSortByToggleProps())} className={_.includes(fixedColumns, column.id)?'fixed-column':''}> <div {...provided.draggableProps} {...provided.dragHandleProps} @@ -2384,12 +2398,26 @@ function Table(props) { </div> {/* Render the columns filter UI */} {column.Header !== 'actionpath' && - <div className={columnclassname[0][column.Header]}> + // <div className={columnclassname[0][column.Header]}> + <div {...column.getHeaderProps(column.getSortByToggleProps())} > {column.canFilter && column.Header !== 'Action' ? column.render('Filter') : null} </div> } + </div> + </div> + {_.includes(fixedColumns, column.id)?<></>: + <div {...column.getResizerProps()} + className={`resizer ${ + column.isResizing ? 'isResizing' : '' + }`} + > + </div> + } </th> ); + } else { + return ""; + } }} </Draggable> ))} @@ -2412,8 +2440,9 @@ function Table(props) { <tr {...row.getRowProps()}> {row.cells.map(cell => { if (cell.column.id !== 'actionpath') { - return <td {...cell.getCellProps()}> - {(cell.row.original.links || []).includes(cell.column.id) ? <a href={cell.row.original.linksURL[cell.column.id]}>{cell.render('Cell')}</a> : cell.render('Cell')} + // return <td {...cell.getCellProps()} className={cell.column.id+'_body'}> + return <td className={cell.column.id+'_body'} className={_.includes(fixedColumns, cell.column.id)?'fixed-column-td':''}> + {(cell.row.original.links || []).includes(cell.column.id) ? <a href={cell.row.original.linksURL[cell.column.id]}>{cell.render('Cell', cell.getCellProps())}</a> : cell.render('Cell', cell.getCellProps())} </td> } else { @@ -2554,7 +2583,7 @@ function ViewTable(props) { Header: 'Action', id: 'Action', accessor: props.keyaccessor, - Cell: props => <Link to={{pathname: props.cell.row.values['actionpath']}}className='p-link' onClick={(e) => navigateTo(e, props)} ><i className="fa fa-eye" style={{ cursor: 'pointer' }}></i></Link>, + Cell: props => <Link to={{pathname: props.cell.row.values['actionpath']}} className='p-link' onClick={(e) => navigateTo(e, props)} ><i className="fa fa-eye" style={{ cursor: 'pointer' }}></i></Link>, disableFilters: true, disableSortBy: true, //isVisible: defaultdataheader.includes(props.keyaccessor), @@ -2562,7 +2591,7 @@ function ViewTable(props) { }) } - const navigateTo = (cellProps) => () => { + const navigateTo = ( cellProps) =>() => { if (cellProps.cell.row.values['actionpath']) { if (!props.viewInNewWindow) { return history.push({ @@ -2575,7 +2604,9 @@ function ViewTable(props) { window.open(cellProps.cell.row.values['actionpath'], '_blank'); } } - // Object.entries(props.paths[0]).map(([key,value]) =>{}) + else { + props.actionCallback(cellProps.cell.row.values); + } } //Default Columns @@ -2591,14 +2622,14 @@ function ViewTable(props) { accessor: header, filter: filtertype, Filter: filterFn, - disableSortBy: doServersideFilter?typeof disableSortBy !== 'undefined' ? disableSortBy : true:false, + disableSortBy: doServersideFilter?(typeof disableSortBy !== 'undefined' ? disableSortBy : true):(disableSortBy || false), disableFilters: doServersideFilter?typeof disableFilter !== 'undefined' ? disableFilter : true:false, //minResizeWidth: 50, //*** TO REMOVE - INCOMING CHANGE */ // filter: (showColumnFilter?((!isString && defaultheader[0][header].filter=== 'date') ? 'includes' : 'fuzzyText'):""), // Filter: (showColumnFilter?(isString ? DefaultColumnFilter : (filterTypes[defaultheader[0][header].filter] ? filterTypes[defaultheader[0][header].filter] : DefaultColumnFilter)):""), isVisible: true, - Cell: props => <div> {updatedCellvalue(header, props.value, defaultheader[0][header])} </div>, + Cell: props => { return <div style={{...props.style}}> {updatedCellvalue(header, props.value, defaultheader[0][header])} </div>}, }) }) @@ -2618,7 +2649,7 @@ function ViewTable(props) { disableSortBy: doServersideFilter?typeof disableSortBy !== 'undefined' ? disableSortBy : true:false, disableFilters: doServersideFilter?typeof disableFilter !== 'undefined' ? disableFilter : true:false, isVisible: false, - Cell: props => <div> {updatedCellvalue(header, props.value, optionalheader[0][header])} </div>, + Cell: props => <div style={{...props.style}}> {updatedCellvalue(header, props.value, optionalheader[0][header])} </div>, }) }); diff --git a/SAS/TMSS/frontend/tmss_webapp/src/layout/_overrides.scss b/SAS/TMSS/frontend/tmss_webapp/src/layout/_overrides.scss index b41bf3b7ab750dc1d13f79c60bbb9309b11c05fa..f05db634874e71d162dbc225b5da90e93211ddf8 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/layout/_overrides.scss +++ b/SAS/TMSS/frontend/tmss_webapp/src/layout/_overrides.scss @@ -63,9 +63,10 @@ .table-filter span { margin-right: 20px; + width: 100%; } .table-filter span input{ - width: 120px; + width: 70%; height: 25px; } diff --git a/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_viewtable.scss b/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_viewtable.scss index c0112826632ed7b79a4249e411793adea771bb04..30838ccd3d33beefc178c2241a2d7740a6cf494c 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_viewtable.scss +++ b/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_viewtable.scss @@ -22,17 +22,24 @@ border-top: 1px solid lightgray; padding: .65rem; vertical-align:bottom; + padding-right: 0px; + word-break: break-all; } .viewtable th>div { vertical-align: text-bottom; } +.viewtable tr { + border-bottom: 1px solid lightgray; +} + .viewtable td { font-size: 14px; padding: .65rem; - border-bottom: 1px solid lightgray; + // border-bottom: 1px solid lightgray; overflow-wrap: anywhere; + padding-right: 13.4px; } .pagination { @@ -101,7 +108,7 @@ body .p-paginator { } .filter-input input, .p-slider { - max-width: 175px; + // max-width: 175px; } .filter-input-0 input{ @@ -138,11 +145,13 @@ body .p-paginator { .filter-slider { margin: 25px 5px 10px 5px; + width : 90% !important; } .filter-slider-label { font-size: 12px; font-weight: 600; + margin-top: 25px; } .filter-slider-label span { @@ -205,4 +214,51 @@ body .p-paginator { top: 6px; color: dimgray; left: 0.25em; +} + +.resizer { + display: flex; + // background: lightgray; + width: 3px; + min-height: 3em; + position: relative; + right: 0; + top: 0; + // transform: translateX(50%); + // z-index: 1; + touch-action:none; + margin-left: 10px; + border-right: 2px solid lightgray; + &.isResizing { + // background: rgb(99, 97, 97); + border-right: 2px solid darkgray; + } +} + +.Action > div, .Action_body { + width: 50px !important; +} + +.Select > div, .Select_body { + width: 35px !important; +} + +.fixed-column { + width: 50px !important; + min-width: 50px !important; + word-break: break-word; +} + +.fixed-column-td { + // display: block !important; + width: 50px !important; + flex: none !important; +} + +.table-filter > input,select { + width: 100% !important; +} + +.flatpickr-range-filter { + display: flex; } \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/list.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/list.js index 2e00b8e11008078d4c0d5c21f1139b855519184a..275c586ae65c5e263bdd03714ae9d0df788f9b85 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/list.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/list.js @@ -8,6 +8,13 @@ import UtilService from '../../services/util.service'; import AuthUtil from '../../utils/auth.util'; import AccessDenied from '../../layout/components/AccessDenied'; import _ from 'lodash'; +import { Dialog } from 'primereact/dialog'; +import { RadioButton } from 'primereact/radiobutton'; +import { Button } from 'primereact/button'; +import { CustomDialog } from '../../layout/components/CustomDialog'; +import { Column } from 'primereact/column'; +import { DataTable } from 'primereact/datatable'; +import { appGrowl } from '../../layout/components/AppGrowl'; /* eslint-disable no-unused-expressions */ @@ -22,9 +29,10 @@ export class ProjectList extends Component { userRolePermission: {} }, projectlist: [], + showStatusUpdateDialog: false, defaultcolumns: [{ name: "Name / Project Code", - status: { + project_state_value: { name: "Status", filter: "select" }, @@ -117,10 +125,18 @@ export class ProjectList extends Component { isprocessed: false, isLoading: true } + this.selectedRows = []; + this.statusOptions = []; this.pageUpdated = true; this.getPopulatedProjectList = this.getPopulatedProjectList.bind(this); this.toggleBySorting = this.toggleBySorting.bind(this); this.setToggleBySorting = this.setToggleBySorting.bind(this); + this.onRowSelection = this.onRowSelection.bind(this); + this.showStatusChangeDialog = this.showStatusChangeDialog.bind(this); + this.confirmStatusChange = this.confirmStatusChange.bind(this); + this.closeDialog = this.closeDialog.bind(this); + this.submitStatusChange = this.submitStatusChange.bind(this); + this.getFilterOptions = this.getFilterOptions.bind(this); this.setToggleBySorting(); } @@ -165,13 +181,14 @@ export class ProjectList extends Component { }); } - componentDidMount() { + async componentDidMount() { //this.getUserRolePermission(); // Show Project for the Cycle, This request will be coming from Cycle View. Otherwise it is consider as normal Project List. this.pageUpdated = true; let cycle = this.props.cycle; this.getPopulatedProjectList(cycle); this.setToggleBySorting(); + this.statusOptions = await ProjectService.getProjectStates(); } setToggleBySorting() { @@ -194,6 +211,163 @@ export class ProjectList extends Component { UtilService.localStore({ type: 'set', key: this.lsKeySortColumn, value: sortData }); } + /** + * Content for confirmation dialog + * @returns Content to be dispalyed in the confirmation dialog + */ + getSUDialogContent = () => { + let changedData = []; + for (const row of this.selectedRows) { + let changedRow = {} + changedRow.project_state_value = this.state.changedStatus.value; + changedRow.name = row.name; + changedData.push(changedRow); + } + return ( + <> + <div style={{ marginTop: '1em' }}> + <b>Do you want to change the status of selected Project(s)?</b> + <div style={{ marginTop: '1em' }}> + <DataTable value={changedData} resizableColumns columnResizeMode="expand" className="card" style={{ paddingLeft: '0em' }}> + <Column field="name" header="Project Name"></Column> + <Column field="project_state_value" header="Status"></Column> + </DataTable> + </div> + </div> + </> + ) + } + + /** + * + * @returns Content to be displayed in dialog after updating the staus in backend + */ + getSUResponseDialogContent = () => { + let changedData = []; + for (const row of this.state.statusChangeResponse) { + changedData.push(row); + } + return ( + <> + <div style={{ marginTop: '1em' }}> + <b></b> + <div style={{ marginTop: '1em' }}> + <DataTable value={this.state.statusChangeResponse} resizableColumns columnResizeMode="expand" className="card" style={{ paddingLeft: '0em' }}> + <Column field="name" header="Project Name"></Column> + <Column field="status" header="Project Status"></Column> + <Column field="message" header="Update Status"></Column> + </DataTable> + </div> + </div> + </> + ) + } + + onRowSelection(selectedRows) { + this.selectedRows = selectedRows; + } + + /** + * Show the available status options to change + */ + showStatusChangeDialog() { + if(this.selectedRows.length>0) { + this.setState({ showStatusUpdateDialog: !this.state.showStatusUpdateDialog, changedStatus: null }); + } + else { + appGrowl.show({severity: 'info',summary: 'Select Row', detail: 'Please select one or more Project(s)'}); + } + } + + /** + * Show confirmation dialog to update status change + */ + confirmStatusChange () { + let dialog = {}; + dialog.type = "confirmation"; + dialog.header = `Confirm Project(s) Status Change`; + dialog.content = this.getSUDialogContent; + dialog.actions = [{ + id: 'yes', title: 'Yes', callback: () => { + this.submitStatusChange(); + } + }, + { id: 'no', title: 'No', callback: () => { + this.closeDialog() + } }]; + dialog.onSubmit = () => {}; + dialog.width = '25vw'; + dialog.showIcon = false; + dialog.dialogVisible = true; + this.setState({dialog: dialog}) + } + + closeDialog () { + let dialog = this.state.dialog; + dialog.dialogVisible = false + this.setState({dialog: dialog}); + return false; + } + + /** + * Updating status in backend on confirmation and displaying response of the updation + */ + async submitStatusChange () { + let statusChangeResponse = [] + for (const row of this.selectedRows) { + row.project_state = this.state.changedStatus.url + row.project_state_value = this.state.changedStatus.value + let response = await ProjectService.updateProject(row.name, row) + if(response.isUpdated) { + statusChangeResponse.push({ + name: response.name, + status: response.project_state_value, + message: 'Success' + }) + } + else { + statusChangeResponse.push({ + name: response.name, + status: response.project_state_value, + message: 'Failed' + }) + appGrowl.show({ severity: 'error', summary: 'Failed to update project status'}); + } + } + this.setState({statusChangeResponse: statusChangeResponse}) + let dialog = this.state.dialog; + dialog.type = "confirmation"; + dialog.header = `Project(s) Status Update Info`; + dialog.content = this.getSUResponseDialogContent; + dialog.actions = [{ + id: 'yes', title: 'Close', callback: () => { + this.closeDialog(); + this.setState({showStatusUpdateDialog: false}); + } + }]; + dialog.onSubmit = () => { + this.closeDialog; + this.setState({showStatusUpdateDialog: false}); + } + dialog.width = '30vw'; + dialog.showIcon = false; + dialog.dialogVisible = true; + this.setState({dialog: dialog, changedStatus: {}}) + } + + /** + * Get Option-list values for Select Dropdown filter in 'Viewtable' + * @param {String} id : Column id + * @returns + */ + getFilterOptions(id) { + let options = []; + if(id && id === 'Status') { + options = (_.map(this.statusOptions, status => status.value)).sort(); + } + return options; + } + /** * Get current user role permission for Project list */ @@ -203,7 +377,12 @@ export class ProjectList extends Component { // } render() { - const {project} = this.state.userrole.userRolePermission + const footer = ( + <div > + <Button label="Save" className="p-button-primary p-mr-2" icon="pi pi-check" disabled={!this.state.changedStatus} onClick={this.confirmStatusChange } data-testid="save-btn" /> + <Button label="Cancel" className="p-button-danger mr-0" icon="pi pi-times" onClick={() => this.setState({showStatusUpdateDialog: false})} /> + </div> + ); return ( <> {/*<div className="p-grid"> @@ -221,8 +400,11 @@ export class ProjectList extends Component { </> : <PageHeader location={this.props.location} title={'Project - List'} - actions={[{ icon: 'fa-plus-square', title: this.state.userrole && this.state.userrole.userRolePermission.project && this.state.userrole.userRolePermission.project.create?'Click to Add Project':"Don't have permission to add new Project", - disabled: this.state.userrole && this.state.userrole.userRolePermission.project?!this.state.userrole.userRolePermission.project.create:true, props: { pathname: '/project/create' } }]} + actions={[{icon: 'fa fa-tag',title: this.state.userrole && this.state.userrole.userRolePermission.project && this.state.userrole.userRolePermission.project.edit?'Update Project(s) Status':"Don't have permission to Update Status", + disabled: this.state.userrole && this.state.userrole.userRolePermission.project?!this.state.userrole.userRolePermission.project.edit:true, + type: 'button', actOn: 'click', props: { callback: this.showStatusChangeDialog }}, + { icon: 'fa-plus-square', title: this.state.userrole && this.state.userrole.userRolePermission.project && this.state.userrole.userRolePermission.project.create?'Click to Add Project':"Don't have permission to add new Project", + disabled: this.state.userrole && this.state.userrole.userRolePermission.project?!this.state.userrole.userRolePermission.project.create:true, props: { pathname: '/project/create' }}]} /> } {this.state.isLoading ? <AppLoader /> : (this.state.isprocessed && this.state.projectlist.length > 0) ? @@ -238,15 +420,52 @@ export class ProjectList extends Component { keyaccessor="name" unittest={this.state.unittest} tablename={this.lsTableName} + allowRowSelection={true} + onRowSelection={this.onRowSelection} toggleBySorting={(sortData) => this.toggleBySorting(sortData)} lsKeySortColumn={this.lsKeySortColumn} pageUpdated={this.pageUpdated} storeFilter={false} + showFilterOption={this.getFilterOptions} //Callback function to provide inputs for option-list in Select Dropdown filter /> : <div>No project found </div> } + {this.state.showStatusUpdateDialog && + <div> + <Dialog header={`Update Project(s) Status`} + footer={footer} maximizable= {false} + visible={this.state.showStatusUpdateDialog} maximized={false} position="center" style={{ width: '20vw' }} + onHide={() => { this.setState({ showStatusUpdateDialog: false }) }} + className="content_dlg"> + <div style={{ width: '100%' }}> + <div class="p-fluid"> + <div class="p-grid p-field" style={{ paddingLeft: '15px' }}>Select the status and click 'Save' to update.</div> + {this.statusOptions.map(option => { + return ( + <div className="p-col-12"> + <RadioButton + inputId={option.value} + name={option.value} + value={option.value} + onChange={e => this.setState({ changedStatus: option })} + checked={this.state.changedStatus && this.state.changedStatus.value === option.value} + /> + <label htmlFor={option.value} className='p-radiobutton-label' style={{textTransform: 'capitalize'}}> + {option.value} + </label> + </div> + ) + })} + </div> + </div> + </Dialog> + <CustomDialog type="confirmation" visible={this.state.dialog && this.state.dialog.dialogVisible} + header={this.state.dialog && this.state.dialog.header} message={this.state.dialog && this.state.dialog.detail} actions={this.state.dialog && this.state.dialog.actions} + content={this.state.dialog && this.state.dialog.content} width={this.state.dialog && this.state.dialog.width} showIcon={this.state.dialog && this.state.dialog.showIcon} + onClose={this.closeDialog} onCancel={this.closeDialog}/> + </div> + } </> - ) } diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/view.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/view.js index c9fd1a28b7197a1d6337aeb7706741a9b5b1f8bc..bdbe7240fe2b5ac85e6bb22ea6f90d36b0da1318 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/view.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/view.js @@ -15,6 +15,13 @@ import SchedulingUnitList from './../Scheduling/SchedulingUnitList'; import UIConstants from '../../utils/ui.constants'; import AuthUtil from '../../utils/auth.util'; import AccessDenied from '../../layout/components/AccessDenied'; +import { DataTable } from 'primereact/datatable'; +import { Column } from 'primereact/column'; +import { appGrowl } from '../../layout/components/AppGrowl'; +import { Button } from 'primereact/button'; +import { Dialog } from 'primereact/dialog'; +import { RadioButton } from 'primereact/radiobutton'; +import { CustomDialog } from '../../layout/components/CustomDialog'; /** * Component to view the details of a project @@ -26,6 +33,7 @@ export class ProjectView extends Component { ltaStorage: [], isLoading: true, project:'', + showStatusUpdateDialog: false }; if (this.props.match.params.id) { this.state.projectId = this.props.match.params.id; @@ -33,11 +41,15 @@ export class ProjectView extends Component { this.state.projectId = this.props.location.state.id; } this.cancelView = this.cancelView.bind(this); + this.showStatusChangeDialog = this.showStatusChangeDialog.bind(this); + this.confirmStatusChange = this.confirmStatusChange.bind(this); + this.closeDialog = this.closeDialog.bind(this); + this.submitStatusChange = this.submitStatusChange.bind(this); this.state.redirect = this.state.projectId?"":'/project' // If no project id is passed, redirect to Project list page this.resourceUnitMap = UnitConverter.resourceUnitMap; // Resource unit conversion factor and constraints } - componentDidMount() { + async componentDidMount() { const projectId = this.state.projectId; if (projectId) { AuthUtil.getUserPermissionByModuleId('project', projectId) @@ -60,6 +72,7 @@ export class ProjectView extends Component { }); } }); + this.statusOptions = await ProjectService.getProjectStates(); } else { this.setState({redirect: "/not-found"}); } @@ -100,6 +113,137 @@ export class ProjectView extends Component { } } + /** + * Content for confirmation dialog + * @returns Content to be dispalyed in the confirmation dialog + */ + getSUDialogContent = () => { + let changedData = []; + changedData.push({ + name: this.state.project.name, + project_state_value: this.state.changedStatus.value + }); + + return ( + <> + <div style={{ marginTop: '1em' }}> + <b>Do you want to change the status of this project?</b> + <div style={{ marginTop: '1em' }}> + <DataTable value={changedData} resizableColumns columnResizeMode="expand" className="card" style={{ paddingLeft: '0em' }}> + <Column field="name" header="Project Name"></Column> + <Column field="project_state_value" header="Status"></Column> + </DataTable> + </div> + </div> + </> + ) + } + + /** + * + * @returns Content to be displayed in dialog after updating the staus in backend + */ + getSUResponseDialogContent = () => { + let changedData = []; + for (const row of this.state.statusChangeResponse) { + changedData.push(row); + } + return ( + <> + <div style={{ marginTop: '1em' }}> + <b></b> + <div style={{ marginTop: '1em' }}> + <DataTable value={this.state.statusChangeResponse} resizableColumns columnResizeMode="expand" className="card" style={{ paddingLeft: '0em' }}> + <Column field="name" header="Project Name"></Column> + <Column field="status" header="Project Status"></Column> + <Column field="message" header="Update Status"></Column> + </DataTable> + </div> + </div> + </> + ) + } + + /** + * Show dialog with all available status options + */ + showStatusChangeDialog() { + this.setState({ showStatusUpdateDialog: !this.state.showStatusUpdateDialog, changedStatus: null }); + } + + /** + * Show confirmation dialog for status change + */ + confirmStatusChange () { + let dialog = {}; + dialog.type = "confirmation"; + dialog.header = `Confirm Project Status change`; + dialog.content = this.getSUDialogContent; + dialog.actions = [{ + id: 'yes', title: 'Yes', callback: () => { + this.submitStatusChange(); + } + }, + { id: 'no', title: 'No', callback: this.closeDialog }]; + dialog.onSubmit = () => {} + dialog.width = '25vw'; + dialog.showIcon = false; + dialog.dialogVisible = true; + this.setState({dialog: dialog}) + } + + closeDialog () { + let dialog = this.state.dialog; + dialog.dialogVisible = false + this.setState({dialog: dialog, showStatusChangeDialog: false}); + return false; + } + + /** + * Updating status in backend on confirmation and displaying response of the updation + */ + async submitStatusChange () { + let statusChangeResponse = [] + let project = this.state.project; + project.project_state = this.state.changedStatus.url + project.project_state_value = this.state.changedStatus.value + let response = await ProjectService.updateProject(project.name, project) + if(response.isUpdated) { + statusChangeResponse.push({ + name: response.name, + status: response.project_state_value, + message: 'Success' + }) + } + else { + statusChangeResponse.push({ + name: response.name, + status: response.project_state_value, + message: 'Failed' + }) + appGrowl.show({ severity: 'error', summary: 'Failed to update project status'}); + } + this.setState({statusChangeResponse: statusChangeResponse}) + let dialog = this.state.dialog; + dialog.type = "confirmation"; + dialog.header = `Project Status Update Info`; + dialog.content = this.getSUResponseDialogContent; + dialog.actions = [{ + id: 'yes', title: 'Close', callback: () => { + this.closeDialog(); + this.setState({showStatusUpdateDialog: false}); + } + }]; + dialog.onSubmit = () => { + this.closeDialog(); + this.setState({showStatusUpdateDialog: false}); + } + dialog.width = '30vw'; + dialog.showIcon = false; + dialog.dialogVisible = true; + this.setState({dialog: dialog, changedStatus: {}}) + } + /** * Get current user role permission for selected Project */ @@ -109,6 +253,12 @@ export class ProjectView extends Component { // } render() { + const footer = ( + <div> + <Button label="Save" className="p-button-primary p-mr-2" icon="pi pi-check" disabled={!this.state.changedStatus} onClick={this.confirmStatusChange } data-testid="save-btn" /> + <Button label="Cancel" className="p-button-danger mr-0" icon="pi pi-times" onClick={() => this.setState({showStatusUpdateDialog: false, changedStatus: {}})} /> + </div> + ); if (this.state.redirect) { return <Redirect to={ {pathname: this.state.redirect} }></Redirect> } @@ -120,7 +270,11 @@ export class ProjectView extends Component { <> <TieredMenu className="app-header-menu" model={this.menuOptions} popup ref={el => this.optionsMenu = el} /> <PageHeader location={this.props.location} title={'Project - Details'} - actions={[ {icon: 'fa-edit', + actions={[{icon: 'fa fa-tag', + title: this.state.permissionById[this.state.projectId].edit?'Update Project Status':"Don't have permission to update status", + disabled: this.state.permissionById[this.state.projectId].edit?!this.state.permissionById[this.state.projectId].edit:true, + type: 'button', actOn: 'click', props: { callback: this.showStatusChangeDialog }}, + {icon: 'fa-edit', title: this.state.permissionById[this.state.projectId].edit?'Click to Edit Project':"Don't have permission to edit", type:'link', disabled: this.state.permissionById[this.state.projectId].edit?!this.state.permissionById[this.state.projectId].edit:true, @@ -162,6 +316,8 @@ export class ProjectView extends Component { <span className="col-lg-4 col-md-4 col-sm-12">{this.state.project.priority_rank}</span> </div> <div className="p-grid"> + <label className="col-lg-2 col-md-2 col-sm-12">Project Status</label> + <span className="col-lg-4 col-md-4 col-sm-12">{this.state.project.project_state_value}</span> <label className="col-lg-2 col-md-2 col-sm-12">LTA Storage Location</label> <span className="col-lg-4 col-md-4 col-sm-12">{this.state.ltaStorage[this.state.project.archive_location]}</span> </div> @@ -201,6 +357,41 @@ export class ProjectView extends Component { </div> </React.Fragment> } + {this.state.showStatusUpdateDialog && + <div> + <Dialog header={`Update Project Status`} + footer={footer} maximizable={false} + visible={this.state.showStatusUpdateDialog} maximized={false} position="center" style={{ width: '20vw' }} + onHide={() => { this.setState({ showStatusUpdateDialog: false, changedStatus: {} }) }} + className="content_dlg"> + <div style={{ width: '100%' }}> + <div class="p-fluid"> + <div class="p-grid p-field" style={{ paddingLeft: '15px' }}>Select the status and click 'Save' to update.</div> + {this.statusOptions.map(option => { + return ( + <div className="p-col-12"> + <RadioButton + inputId={option.value} + name={option.value} + value={option.value} + onChange={e => this.setState({ changedStatus: option })} + checked={this.state.changedStatus && this.state.changedStatus.value === option.value} + /> + <label htmlFor={option.value} className='p-radiobutton-label'> + {_.startCase(option.value)} + </label> + </div> + ) + })} + </div> + </div> + </Dialog> + <CustomDialog type="confirmation" visible={this.state.dialog && this.state.dialog.dialogVisible} + header={this.state.dialog && this.state.dialog.header} message={this.state.dialog && this.state.dialog.detail} actions={this.state.dialog && this.state.dialog.actions} + content={this.state.dialog && this.state.dialog.content} width={this.state.dialog && this.state.dialog.width} showIcon={this.state.dialog && this.state.dialog.showIcon} + onClose={this.closeDialog} onCancel={this.closeDialog} onSubmit={this.submitStatusChange} /> + </div> + } </>: <AccessDenied/>} </>} </React.Fragment> 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 72ce758af08ab032531e8171001976040a5b9db4..986fd03769bc1ab468f6a5c6701259bd6894fbc7 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/ViewSchedulingUnit.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/ViewSchedulingUnit.js @@ -253,7 +253,7 @@ class ViewSchedulingUnit extends Component { this.setToggleBySorting(); let schedule_id = this.props.match.params.id; let schedule_type = this.props.match.params.type; - const permissionById = await AuthUtil.getUserPermissionByModuleId('scheduling_unit_draft', schedule_id) + const permissionById = await AuthUtil.getUserPermissionByModuleId(schedule_type === 'blueprint'? 'scheduling_unit_blueprint': 'scheduling_unit_draft', schedule_id); this.setState({userPermission: permission, permissionById: permissionById, schedule_id: schedule_id}); if (schedule_type && schedule_id) { this.stations = await ScheduleService.getStationGroup(); diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/list.tabs.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/list.tabs.js index d912e1ed14be8889456b2dd5fc74c7df68f68678..2d86ba533db68ede29739b91719f2fffbfcf0d2e 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/list.tabs.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/list.tabs.js @@ -20,6 +20,7 @@ class TimelineListTabs extends Component { super(props); this.state = { listTabIndex: 0, + unschedulableList: [], showStatusLogs: false, } this.pageUpdated = true; @@ -31,6 +32,10 @@ class TimelineListTabs extends Component { this.getTaskList = this.getTaskList.bind(this); this.getSUFilterOptions = this.getSUFilterOptions.bind(this); this.getTaskFilterOptions = this.getTaskFilterOptions.bind(this); + this.showSummary = this.showSummary.bind(this); + } + + componentDidMount() { } /** @@ -64,6 +69,32 @@ class TimelineListTabs extends Component { } return taskList; } + + /** + * Formatting data for UNscedulable SU list + * @returns formatted data of unschedulable list + */ + getUnschedulableList () { + let unschedulableList = []; + for(let unschedulableSU of this.props.unschedulableList ) { + unschedulableSU['viewSummaryAction'] = this.showUnschedulableSummary(unschedulableSU) + unschedulableList.push(unschedulableSU) + } + return unschedulableList; + } + + /** + * + * @param {Object} unschedulableSU - Object of item clicked + * @returns HTML to show action button for viewing summary + */ + showUnschedulableSummary = (unschedulableSU) => { + return ( + <button className="p-link" onClick={(e) => { this.showSummary(unschedulableSU) }}> + <i className="fa fa-history"></i> + </button> + ); + }; /** * Callback function passed to SU List table and which in turn call the parent(View or Weekview) callback function to update the timeline. @@ -162,6 +193,17 @@ class TimelineListTabs extends Component { return options; } + /** + * Callback function passed to SU summary table and which in turn call the parent(View or Weekview) callback function to show summary of the SU. + * @param {Object} item - object of item clicked + */ + showSummary(item) { + if (this.props.showSummary ) { + item.type = "UNSCHEDULABLE" + this.props.showSummary(item); + } + } + render() { const taskList = this.getTaskList(); return( @@ -229,6 +271,27 @@ class TimelineListTabs extends Component { pageUpdated={this.pageUpdated} /> </TabPanel> + <TabPanel header="Unschedulable"> + <ViewTable + viewInNewWindow + data={this.getUnschedulableList()} + defaultcolumns={TimelineConstants.UNSCEDULABLE_SU_LIST_DEFAULT_COLUMNS} + optionalcolumns={TimelineConstants.SU_LIST_OPTIONAL_COLUMNS} + columnclassname={TimelineConstants.SU_LIST_COLUMN_CLASSES} + columnOrders={TimelineConstants.SU_LIST_COLUMN_ORDER} + defaultSortColumn={this.getSortingColumn("SUListSortColumn")} + showaction="true" + tablename={`timeline_scheduleunit_list`} + //showFilterOption={this.getSUFilterOptions} //Callback function to provide inputs for option-list in Select Dropdown filter + showTopTotal={false} + showGlobalFilter={true} + showColumnFilter={true} + // filterCallback={this.suListFilterCallback} + lsKeySortColumn={"SUListSortColumn"} + toggleBySorting={(sortData) => this.storeSortingColumn("SUListSortColumn", sortData)} + pageUpdated={this.pageUpdated} + /> + </TabPanel> </TabView> {this.state.showStatusLogs && <Dialog header={`Status change logs - ${this.state.task ? this.state.task.name : ""}`} diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/view.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/view.js index acfc903326428e02b389d04247d1652e9c75e723..8e709b1c4bc94a5c394a8f25f8637585988c77a9 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/view.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/view.js @@ -73,6 +73,7 @@ export class TimelineView extends Component { items: [], // Timeline items from scheduling unit blueprints grouped by scheduling unit draft isSUDetsVisible: false, isTaskDetsVisible: false, + isUnscheduledDetVisible: false, canExtendSUList: this.timelineUIAttributes.canExtendSUList===undefined?true:this.timelineUIAttributes.canExtendSUList, canShrinkSUList: this.timelineUIAttributes.canShrinkSUList || false, isSUListVisible: this.timelineUIAttributes.isSUListVisible===undefined?true:this.timelineUIAttributes.isSUListVisible, @@ -94,6 +95,8 @@ export class TimelineView extends Component { userrole: AuthStore.getState(), suStatusList: [], taskStatusList: [], + unschedulableList: [], + unschedulableBlueprint: {}, datasetStartTime: null, datasetEndTime:null, showDialog: false, popPosition: {display: 'none'} @@ -195,8 +198,9 @@ export class TimelineView extends Component { loader: false, taskTypes: taskTypes, currentUTC: currentUTC, isLoading: false, currentStartTime: defaultStartTime, currentEndTime: defaultEndTime, - selectedStationGroup: selectedStationGroup + selectedStationGroup: selectedStationGroup, }); + this.getUnschedulableUnits().then(unschedulableList => this.setState({unschedulableList: unschedulableList})); this.dateRangeCallback(defaultStartTime, defaultEndTime, true); }); } @@ -330,6 +334,37 @@ export class TimelineView extends Component { return {original: suBlueprints, formatted: suList}; } + /** + * Formatting data for the table + * @returns list of formatted unschedulable SU for table + */ + + async getUnschedulableUnits () { + let unschedulableBlueprints = await ScheduleService.getSchedulingListByStatus('unschedulable'); + for (let unschedulableBlueprint of unschedulableBlueprints){ + unschedulableBlueprint['actionpath'] = `/schedulingunit/view/blueprint/${unschedulableBlueprint.id}`; + unschedulableBlueprint.suDraft = unschedulableBlueprint.draft; + unschedulableBlueprint.project = unschedulableBlueprint.draft.scheduling_set.project.name; + unschedulableBlueprint.suSet = unschedulableBlueprint.draft.scheduling_set; + unschedulableBlueprint.durationInSec = unschedulableBlueprint.duration; + unschedulableBlueprint.duration = UnitConverter.getSecsToHHmmss(unschedulableBlueprint.duration); + unschedulableBlueprint.workflowStatus = this.timelineCommonUtils.getWorkflowStatus(unschedulableBlueprint); + unschedulableBlueprint.task_content = ""; + unschedulableBlueprint.observ_template_name = unschedulableBlueprint.draft.observation_strategy_template?unschedulableBlueprint.draft.observation_strategy_template.name:null; + unschedulableBlueprint.tasks = unschedulableBlueprint.task_blueprints; + unschedulableBlueprint.stationGroupCount = this.timelineCommonUtils.getSUStationGroupCount(unschedulableBlueprint).counts; + for (let task of unschedulableBlueprint.tasks) { + const controlTask = _.find(task.subtasks, subtask => { return subtask.primary}); + task.controlId = controlTask?controlTask.id:""; + if (task.task_type === "observation") { + task.antenna_set = task.specifications_doc.antenna_set; + task.band = task.specifications_doc.filter; + } + } + } + return unschedulableBlueprints; + } + setSelectedStationGroup(value) { // By default all stations groups are selected. // In that case no need to store the selected group otherwise store the selected groups in local storage @@ -448,10 +483,12 @@ export class TimelineView extends Component { * @param {Object} item */ onItemClick(item) { - if (item.type === "SCHEDULE") { - this.showSUSummary(item); + if (item.type === "UNSCHEDULABLE") { + this.showSUSummary(item, this.state.unschedulableList); } else if (item.type === "RESERVATION") { this.showReservationSummary(item); + } else if (item.type === "SCHEDULE") { + this.showSUSummary(item, this.state.suBlueprintList); } else { this.showTaskSummary(item); } @@ -459,9 +496,10 @@ export class TimelineView extends Component { /** * To load SU summary and show - * @param {Object} item - Timeline SU item object. + * @param {Object} item - SU item object. + * @param {Array} blueprintList - List of blueprints based on the item type clicked */ - showSUSummary(item) { + showSUSummary(item, blueprintList) { if (this.state.isSUDetsVisible && item.id === this.state.selectedItem.id) { this.closeSummaryPanel(); } else { @@ -473,7 +511,7 @@ export class TimelineView extends Component { canExtendSUList: false, canShrinkSUList: false }); if (fetchDetails) { - const suBlueprint = _.find(this.state.suBlueprints, { id: (this.state.stationView ? parseInt(item.id.split('-')[0]) : item.id) }); + const suBlueprint = _.find(blueprintList, { id: (this.state.stationView ? parseInt(item.id.split('-')[0]) : item.id) }); const suConstraintTemplate = _.find(this.suConstraintTemplates, { id: suBlueprint.suDraft.scheduling_constraints_template_id }); /* If tasks are not loaded on component mounting fetch from API */ if (suBlueprint.tasks) { @@ -543,7 +581,7 @@ export class TimelineView extends Component { // If the stored previous position available, restore it else keep the default positions const canExtendSUList = this.timelineUIAttributes.canExtendSUList!=undefined?this.timelineUIAttributes.canExtendSUList:true; const canShrinkSUList = this.timelineUIAttributes.canShrinkSUList || false; - this.setState({ isSUDetsVisible: false, isReservDetsVisible: false, isTaskDetsVisible: false, + this.setState({ isSUDetsVisible: false, isReservDetsVisible: false, isTaskDetsVisible: false, isUnscheduledDetVisible: false, canExtendSUList: canExtendSUList, canShrinkSUList: canShrinkSUList }); } @@ -1296,23 +1334,24 @@ export class TimelineView extends Component { { this.state.isLoading ? <AppLoader /> : <div className="p-grid"> {/* SU List Panel */} - <div className={isSUListVisible && (isSUDetsVisible || isReservDetsVisible || isTaskDetsVisible || + <div className={isSUListVisible && (isSUDetsVisible || isReservDetsVisible || isTaskDetsVisible || (canExtendSUList && !canShrinkSUList) ? "col-lg-4 col-md-4 col-sm-12" : ((canExtendSUList && canShrinkSUList) ? "col-lg-5 col-md-5 col-sm-12" : "col-lg-6 col-md-6 col-sm-12"))} style={isSUListVisible ? { position: "inherit", borderRight: "3px solid #efefef", paddingTop: "10px" } : { display: 'none' }}> <TimelineListTabs suBlueprintList={this.state.suBlueprintList} suListFilterCallback={this.suListFilterCallback} reservationList={this.getReservationList()} + showSummary={this.onItemClick} suStatusList={this.state.suStatusList} taskStatusList={this.state.taskStatusList} + unschedulableList={this.state.unschedulableList} ></TimelineListTabs> </div> {/* Timeline Panel */} - <div className={isSUListVisible ? ((isSUDetsVisible || isReservDetsVisible || isTaskDetsVisible) ? "col-lg-5 col-md-5 col-sm-12" : + <div className={isSUListVisible ? ((isSUDetsVisible || isReservDetsVisible || isTaskDetsVisible || this.state.isUnscheduledDetVisible) ? "col-lg-5 col-md-5 col-sm-12" : (!canExtendSUList && canShrinkSUList) ? "col-lg-6 col-md-6 col-sm-12" : ((canExtendSUList && canShrinkSUList) ? "col-lg-7 col-md-7 col-sm-12" : "col-lg-8 col-md-8 col-sm-12")) : ((isSUDetsVisible || isReservDetsVisible || isTaskDetsVisible) ? "col-lg-9 col-md-9 col-sm-12" : "col-lg-12 col-md-12 col-sm-12")} - // style={{borderLeft: "3px solid #efefef"}} > {/* Panel Resize buttons */} {isSUListVisible && diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/week.view.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/week.view.js index ab192791713e44f92548559c48a891b558bbf9ac..a8aca7e145674ffcb6bd3d5884c0ca4221700f08 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/week.view.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/week.view.js @@ -62,6 +62,7 @@ export class WeekTimelineView extends Component { group: [], // Timeline group from scheduling unit draft name items: [], // Timeline items from scheduling unit blueprints grouped by scheduling unit draft isSUDetsVisible: false, + isUnscheduledDetVisible: false, canExtendSUList: this.timelineUIAttributes.canExtendSUList===undefined?true:this.timelineUIAttributes.canExtendSUList, canShrinkSUList: this.timelineUIAttributes.canShrinkSUList || false, isSUListVisible: this.timelineUIAttributes.isSUListVisible===undefined?true:this.timelineUIAttributes.isSUListVisible, @@ -72,6 +73,8 @@ export class WeekTimelineView extends Component { reservationEnabled: this.timelineUIAttributes.reservationEnabled===undefined?true:this.timelineUIAttributes.reservationEnabled, suStatusList: [], taskStatusList: [], + unschedulableList: [], + unschedulableBlueprint: {}, datasetStartTime: null, datasetEndTime:null, showDialog: false, userrole: AuthStore.getState(), @@ -101,7 +104,6 @@ export class WeekTimelineView extends Component { this.updateSchedulingUnit = this.updateSchedulingUnit.bind(this); this.showDymanicSchedulerPopup = this.showDymanicSchedulerPopup.bind(this); this.close = this.close.bind(this); - } async componentDidMount() { @@ -139,6 +141,7 @@ export class WeekTimelineView extends Component { const groupDate = defaultStartTime.clone().add(count, 'days'); group.push({ 'id': groupDate.format("MMM DD ddd"), title: groupDate.format("MMM DD - ddd"), value: groupDate }); } + const unschedulableList = await this.getUnschedulableUnits(); // Get all scheduling constraint templates ScheduleService.getSchedulingConstraintTemplates() .then(suConstraintTemplates => { @@ -146,6 +149,7 @@ export class WeekTimelineView extends Component { }); this.setState({ suBlueprints: [], suBlueprintList: [], + unschedulableList: unschedulableList, group: _.sortBy(group, ['value']), items: items, currentUTC: currentUTC, isLoading: false, startTime: defaultStartTime, endTime: defaultEndTime @@ -280,7 +284,35 @@ export class WeekTimelineView extends Component { } return {original: suBlueprints, formatted: suList}; } - + /** + * Formatting data for the table + * @returns list of formatted unschedulable SU for table + */ + async getUnschedulableUnits () { + let unschedulableBlueprints = await ScheduleService.getSchedulingListByStatus('unschedulable'); + for (let unschedulableBlueprint of unschedulableBlueprints) { + unschedulableBlueprint['actionpath'] = `/schedulingunit/view/blueprint/${unschedulableBlueprint.id}`; + unschedulableBlueprint.suDraft = unschedulableBlueprint.draft; + unschedulableBlueprint.project = unschedulableBlueprint.draft.scheduling_set.project.name; + unschedulableBlueprint.suSet = unschedulableBlueprint.draft.scheduling_set; + unschedulableBlueprint.durationInSec = unschedulableBlueprint.duration; + unschedulableBlueprint.duration = UnitConverter.getSecsToHHmmss(unschedulableBlueprint.duration); + unschedulableBlueprint.workflowStatus = this.timelineCommonUtils.getWorkflowStatus(unschedulableBlueprint); + unschedulableBlueprint.task_content = ""; + unschedulableBlueprint.observ_template_name = unschedulableBlueprint.draft.observation_strategy_template?unschedulableBlueprint.draft.observation_strategy_template.name:null; + unschedulableBlueprint.tasks = unschedulableBlueprint.task_blueprints; + unschedulableBlueprint.stationGroupCount = this.timelineCommonUtils.getSUStationGroupCount(unschedulableBlueprint).counts; + for (let task of unschedulableBlueprint.tasks) { + const controlTask = _.find(task.subtasks, subtask => { return subtask.primary}); + task.controlId = controlTask?controlTask.id:""; + if (task.task_type === "observation") { + task.antenna_set = task.specifications_doc.antenna_set; + task.band = task.specifications_doc.filter; + } + } + } + return unschedulableBlueprints; + } /** * Function to get/prepare Item object to be passed to Timeline component * @param {Object} suBlueprint @@ -318,19 +350,26 @@ export class WeekTimelineView extends Component { * Callback function to pass to Timeline component for item click. * @param {Object} item */ - onItemClick(item) { - if (item.type === "SCHEDULE") { - this.showSUSummary(item); + onItemClick(item) { + if (item.type === "UNSCHEDULABLE") { + let itemClicked =_.clone(item) + itemClicked.id = item.id + '-' + this.showSUSummary(itemClicked, this.state.unschedulableList); } else if (item.type === "RESERVATION") { this.showReservationSummary(item); + } else if (item.type === "SCHEDULE") { + this.showSUSummary(item, this.state.suBlueprintList); + } else { + this.showTaskSummary(item); } } /** * To load SU summary and show - * @param {Object} item - Timeline SU item object. + * @param {Object} item - SU item object. + * @param {Array} blueprintList - List of blueprints based on the item type clicked */ - showSUSummary(item) { + showSUSummary(item, blueprintList) { if (this.state.isSUDetsVisible && item.id === this.state.selectedItem.id) { this.closeSUDets(); } else { @@ -342,7 +381,7 @@ export class WeekTimelineView extends Component { canExtendSUList: false, canShrinkSUList: false }); if (fetchDetails) { - const suBlueprint = _.find(this.state.suBlueprints, { id: parseInt(item.id.split('-')[0]) }); + const suBlueprint = _.find(blueprintList, { id: parseInt(item.id.split('-')[0])}); const suConstraintTemplate = _.find(this.suConstraintTemplates, { id: suBlueprint.draft.scheduling_constraints_template_id }); /* If tasks are not loaded on component mounting fetch from API */ if (suBlueprint.tasks) { @@ -392,7 +431,7 @@ export class WeekTimelineView extends Component { closeSUDets() { const canExtendSUList = this.timelineUIAttributes.canExtendSUList!=undefined?this.timelineUIAttributes.canExtendSUList:true; const canShrinkSUList = this.timelineUIAttributes.canShrinkSUList || false; - this.setState({ isSUDetsVisible: false, isReservDetsVisible: false, + this.setState({ isSUDetsVisible: false, isReservDetsVisible: false, isUnscheduledDetVisible: false, canExtendSUList: canExtendSUList, canShrinkSUList: canShrinkSUList }); } @@ -1023,7 +1062,7 @@ export class WeekTimelineView extends Component { </div> */} <div className="p-grid"> {/* SU List Panel */} - <div className={isSUListVisible && (isSUDetsVisible || isReservDetsVisible || + <div className={isSUListVisible && (isSUDetsVisible || isReservDetsVisible || (canExtendSUList && !canShrinkSUList) ? "col-lg-4 col-md-4 col-sm-12" : ((canExtendSUList && canShrinkSUList) ? "col-lg-5 col-md-5 col-sm-12" : "col-lg-6 col-md-6 col-sm-12"))} style={isSUListVisible ? { position: "inherit", borderRight: "5px solid #efefef", paddingTop: "10px" } : { display: "none" }}> @@ -1031,11 +1070,13 @@ export class WeekTimelineView extends Component { suListFilterCallback={this.suListFilterCallback} reservationList={this.getReservationList()} suStatusList={this.state.suStatusList} + showSummary={this.onItemClick} + unschedulableList={this.state.unschedulableList}moz taskStatusList={this.state.taskStatusList} ></TimelineListTabs> </div> {/* Timeline Panel */} - <div className={isSUListVisible ? ((isSUDetsVisible || isReservDetsVisible) ? "col-lg-5 col-md-5 col-sm-12" : + <div className={isSUListVisible ? ((isSUDetsVisible || isReservDetsVisible ||this.state.isUnscheduledDetVisible) ? "col-lg-5 col-md-5 col-sm-12" : (!canExtendSUList && canShrinkSUList) ? "col-lg-6 col-md-6 col-sm-12" : ((canExtendSUList && canShrinkSUList) ? "col-lg-7 col-md-7 col-sm-12" : "col-lg-8 col-md-8 col-sm-12")) : ((isSUDetsVisible || isReservDetsVisible) ? "col-lg-9 col-md-9 col-sm-12" : "col-lg-12 col-md-12 col-sm-12")} diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/decide.acceptance.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/decide.acceptance.js index c26c41b85b2dfd9eb11ac3b667313e7dfdd01519..fbb9de2fc2b928f656084772cda7ca4e70a96bc2 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/decide.acceptance.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/decide.acceptance.js @@ -17,8 +17,10 @@ class DecideAcceptance extends Component { showEditor: false, sos_accept_after_pi: true, pi_accept: true, - operator_accept: true, + operator_accept: true, + currentWorkflowTask:{}, }; + this.user =JSON.parse(localStorage.getItem("user")); this.Next = this.Next.bind(this); this.handleChange = this.handleChange.bind(this); this.onChangePIComment = this.onChangePIComment.bind(this); @@ -28,10 +30,12 @@ class DecideAcceptance extends Component { async componentDidMount() { let currentWorkflowTask = null; + let processPermission = null; if (this.props.readOnly) { this.getQADecideAcceptance(); } else { currentWorkflowTask = await this.props.getCurrentTaskDetails(); + processPermission = await this.props.processPermission(); } const qaReportingResponse = await WorkflowService.getQAReportingTo(this.props.process.qa_reporting_to); const qaSOSResponse = await WorkflowService.getQAReportingSOS(this.props.process.qa_reporting_sos); @@ -44,8 +48,9 @@ class DecideAcceptance extends Component { quality_within_policy: qaSOSResponse.quality_within_policy, sos_accept_show_pi: qaSOSResponse.sos_accept_show_pi, sos_accept_after_pi: piVerificationResponse.pi_accept, - assignTo: this.props.readOnly?this.props.workflowTask.owner:(currentWorkflowTask?currentWorkflowTask.fields.owner:null), - currentWorkflowTask: currentWorkflowTask + assignTo: this.props.readOnly?this.props.workflowTask.owner:(currentWorkflowTask?currentWorkflowTask.owner_id:null), + currentWorkflowTask: currentWorkflowTask, + processPermission: processPermission }); } @@ -71,16 +76,13 @@ class DecideAcceptance extends Component { async Next() { let currentWorkflowTask = await this.props.getCurrentTaskDetails(); const promise = []; - if (currentWorkflowTask && !currentWorkflowTask.fields.owner) { - promise.push(WorkflowService.updateAssignTo(currentWorkflowTask.pk, { owner: this.state.assignTo })); - } promise.push(WorkflowService.updateQA_Perform(this.props.id, {"sos_accept_after_pi":this.state.sos_accept_after_pi})); Promise.all(promise).then(async(responses) => { if (responses.indexOf(null)<0) { // After completing the current task, get the next task triggered in the workflow and assign project role user for the next task. currentWorkflowTask = await this.props.getCurrentTaskDetails(); - if (!currentWorkflowTask.fields.owner) { - await WorkflowService.updateAssignTo(currentWorkflowTask.pk, + if (!currentWorkflowTask.owner_id) { + await WorkflowService.updateAssignTo(currentWorkflowTask.id, `project_role=${this.props.project.project_category_value==='user_shared_support'? 'shared_support_user':'friend_of_project_primary'}`, { owner: this.state.assignTo }); @@ -109,7 +111,7 @@ class DecideAcceptance extends Component { async assignTaskCB() { let currentWorkflowTask = await this.props.getCurrentTaskDetails(); this.setState({currentWorkflowTask: currentWorkflowTask, - assignTo: currentWorkflowTask.fields.owner, reassign: false}); + assignTo: currentWorkflowTask.owner_id, reassign: false}); } render() { @@ -121,17 +123,25 @@ class DecideAcceptance extends Component { <div className="col-lg-3 col-md-3 col-sm-12" data-testid="assignTo" > {/* Display the assigned owner of the task */} {this.state.assignTo && - <>{this.state.assignTo} + <>{!this.props.readOnly ? this.state.currentWorkflowTask.owner_username: this.props.workflowTask.owner_username } {/* Allow to edit the owner if the task is not yet completed */} {!this.props.readOnly && !this.state.reassign && - <Link onClick={e => this.setState({reassign: true})} style={{marginLeft: '10px'}}> - <i className="pi pi-pencil"></i> - </Link>} + <> + {this.state.currentWorkflowTask.editPermissions + ? <Link onClick={e => this.setState({reassign: true})} style={{marginLeft: '10px'}}> + <i className="pi pi-pencil"></i> + </Link> + : <Link style={{ cursor: 'default', pointerEvents: "none", color: 'grey', marginLeft: '10px'}} onClick={e => this.setState({reassign: true})}> + <i className="pi pi-pencil"></i> + </Link> + } + </> + } </> } {/* Display the task assigner if owner is not assigned or to reassign to other user */} {(!this.state.assignTo || this.state.reassign) && this.state.currentWorkflowTask && - <TaskAssigner currentWorkflowTask={this.state.currentWorkflowTask} + <TaskAssigner currentWorkflowTask={this.state.currentWorkflowTask} disableAssignToMe = {this.state.currentWorkflowTask.owner_username === this.user.name? true: false} projectRoles={this.props.projectRoles} callback={this.assignTaskCB} /> } </div> @@ -186,13 +196,20 @@ class DecideAcceptance extends Component { </div> {!this.props.readOnly && <div className="p-grid" style={{ marginTop: '20px' }}> <div className="btn-bar"> - <Button label="Next" className="p-button-primary" icon="pi pi-check" onClick = { this.Next } disabled={!this.state.content || this.props.readOnly}/> + <Button disabled={this.state.content && this.state.processPermission && this.state.currentWorkflowTask.editPermissions && this.state.currentWorkflowTask.owner_username === this.user.name? false: true } + label="Next" className="p-button-primary" icon="pi pi-check" onClick={ this.Next } /> </div> <div className="btn-bar"> <Button label="Cancel" className="p-button-danger" icon="pi pi-times" style={{ width : '90px' }} onClick={(e) => { this.props.onCancel()}} /> </div> </div>} + {!this.props.readOnly && + <> + {this.state.currentWorkflowTask.editPermissions && this.state.currentWorkflowTask.owner_username === this.user.name? + <span></span>: <span style={{color: 'red'}}>* You can only save and proceed next if you are assigned to this workflow step</span>} + </> + } </> ) }; diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/index.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/index.js index 43a99a7955af30d8cdc478e9eb59ece2b0454646..80bbed6eef8fc597835969e5733409979a50942e 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/index.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/index.js @@ -21,6 +21,7 @@ import DataProductService from '../../services/data.product.service'; import TaskService from '../../services/task.service'; import AuthUtil from '../../utils/auth.util'; import UtilService from '../../services/util.service'; +import NotFound from '../../layout/components/NotFound'; const RedirectionMap = { 'wait scheduled': 1, @@ -72,6 +73,7 @@ export default (props) => { const [schedulingUnit, setSchedulingUnit] = useState(); const [userrole, setPermissions] = useState({}) const [projectRoles, setProjectRoles] = useState(); + const [notFound, setNotFound] = useState(); // const [ingestTask, setInjestTask] = useState({}); // const [QASchedulingTask, setQASchdulingTask] = useState([]); @@ -158,31 +160,37 @@ export default (props) => { // setQASchdulingTask(suQAProcessTasks); // const workflowLastTask = responses[1].find(task => task.process === suQAProcess.id); const workflowLastTask = (_.orderBy(suQAProcessTasks, ['id'], ['desc']))[0]; - let currView = RedirectionMap[workflowLastTask.flow_task.toLowerCase()]; - let currStep = RedirectionMap[workflowLastTask.flow_task.toLowerCase()]; - // Need to cross check below if condition if it fails in next click - if (workflowLastTask.status === 'NEW') { - currView = RedirectionMap[workflowLastTask.flow_task.toLowerCase()]; - currStep = RedirectionMap[workflowLastTask.flow_task.toLowerCase()]; - } //else { - // setCurrentView(3); - // } - else if (workflowLastTask.status.toLowerCase() === 'done' || workflowLastTask.status.toLowerCase() === 'finished') { - // setDisableNextButton(true); - currView = 9; - currStep = 9; - } - if (currView > 7) { - const subtaskTemplates = await TaskService.getSubtaskTemplates(); - await getDataProductDetails(taskList, subtaskTemplates); - if (!ingestTabVisible) { - currView--; - currStep--; + if(workflowLastTask) { + let currView = RedirectionMap[workflowLastTask.flow_task.toLowerCase()]; + let currStep = RedirectionMap[workflowLastTask.flow_task.toLowerCase()]; + // Need to cross check below if condition if it fails in next click + if (workflowLastTask.status === 'NEW') { + currView = RedirectionMap[workflowLastTask.flow_task.toLowerCase()]; + currStep = RedirectionMap[workflowLastTask.flow_task.toLowerCase()]; + } //else { + // setCurrentView(3); + // } + else if (workflowLastTask.status.toLowerCase() === 'done' || workflowLastTask.status.toLowerCase() === 'finished') { + // setDisableNextButton(true); + currView = 9; + currStep = 9; + } + if (currView > 7) { + const subtaskTemplates = await TaskService.getSubtaskTemplates(); + await getDataProductDetails(taskList, subtaskTemplates); + if (!ingestTabVisible) { + currView--; + currStep--; + } } + setCurrentView(currView); + setCurrentStep(currStep); + setLoader(false); + } else { + setNotFound(true); + setLoader(false); } - setCurrentView(currView); - setCurrentStep(currStep); - setLoader(false); + }); } @@ -194,10 +202,19 @@ export default (props) => { return tasks.find(task => task.specifications_template.type_value==='ingest') } + const getProcessPermission = async () => { + const permissions = await WorkflowService.getWorkflowProcessPermission(QASUProcess.id); + let processPermission = permissions?(_.includes(permissions, 'PUT', 'PATCH')):false; + return processPermission; + } + const getCurrentTaskDetails = async () => { // const response = await WorkflowService.getCurrentTask(props.match.params.id); - const response = await WorkflowService.getCurrentTask(QASUProcess.id); - return response; + const currentTask = await WorkflowService.getCurrentTask(QASUProcess.id); + const currentTaskpermissions = await WorkflowService.getWorkflowTaskPermission(currentTask.id); + currentTask.owner_username = currentTaskpermissions.user_name + currentTask.editPermissions = currentTaskpermissions?(_.includes(currentTaskpermissions.headers, 'PUT', 'PATCH')):false; + return currentTask; }; const getProject = () => { @@ -245,95 +262,100 @@ export default (props) => { return ( <> - <Growl ref={(el) => growl = el} /> - {currentStep && - <PageHeader location={props.location} title={getTitle()} - actions={[{type:'ext_link', icon:'', label: 'SDC Helpdesk', title: 'Report major issues here', props: { pathname: 'https://support.astron.nl/sdchelpdesk' } }, - {icon: 'fa-window-close', title: 'Click to Close Workflow', type: 'button', actOn: 'click', props:{ callback: cancelView }}, - ]} />} - {loader && <AppLoader />} - {!loader && schedulingUnit && - <> - <div className="p-fluid"> - {currentView && <div className="p-field p-grid"> - <label htmlFor="suName" 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/${schedulingUnit.id}` }}>{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"> - <span>{schedulingUnit.status}</span> + { notFound ? + <NotFound/> + :<> + <Growl ref={(el) => growl = el} /> + {currentStep && + <PageHeader location={props.location} title={getTitle()} + actions={[{type:'ext_link', icon:'', label: 'SDC Helpdesk', title: 'Report major issues here', props: { pathname: 'https://support.astron.nl/sdchelpdesk' } }, + {icon: 'fa-window-close', title: 'Click to Close Workflow', type: 'button', actOn: 'click', props:{ callback: cancelView }}, + ]} />} + {loader && <AppLoader />} + {!loader && schedulingUnit && + <> + <div className="p-fluid"> + {currentView && <div className="p-field p-grid"> + <label htmlFor="suName" 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/${schedulingUnit.id}` }}>{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"> + <span>{schedulingUnit.status}</span> + </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 rel="noopener noreferrer" href="https://proxy.lofar.eu/inspect/HTML/" target="_blank">Inspection plots</a> + </label> + <label className="col-sm-10 "> + <a rel="noopener noreferrer" 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={`step-header-${currentStep}`}> + + <Steps model={getStepItems()} activeIndex={currentView - 1} readOnly={false} + onSelect={(e) => e.index<currentStep?setCurrentView(e.index+1):setCurrentView(currentView)} /> + </div> + <div className="step-content"> + {currentView === 1 && + <Scheduled onNext={onNext} onCancel={onCancel} readOnly={ currentStep !== 1 } + schedulingUnit={schedulingUnit} /> + } + {currentView === 2 && + <ProcessingDone onNext={onNext} onCancel={onCancel} readOnly={ currentStep !== 2 } + schedulingUnit={schedulingUnit} /> + } + {} + {currentView === 3 && userrole.workflow.qa_reporting_to && + <QAreporting onNext={onNext} onCancel={onCancel} id={QASUProcess.id} readOnly={ currentStep !== 3 } + process={QASUProcess} getCurrentTaskDetails={getCurrentTaskDetails} processPermission={getProcessPermission} + workflowTask={_.find(workflowTasks, ['flow_task', 'Qa Reporting To'])} + onError={showMessage} projectRoles={getProjectRoles('qa reporting to')} /> + } + {currentView === 4 && userrole.workflow.qa_reporting_sos && + <QAsos onNext={onNext} onCancel={onCancel} id={QASUProcess.id} readOnly={ currentStep !== 4 } + process={QASUProcess} getCurrentTaskDetails={getCurrentTaskDetails} processPermission={getProcessPermission} + workflowTask={_.find(workflowTasks, ['flow_task', 'Qa Reporting Sos'])} + onError={showMessage} projectRoles={getProjectRoles('qa reporting sos')} /> + } + {currentView === 5 && userrole.workflow.pi_verification && + <PIverification onNext={onNext} onCancel={onCancel} id={QASUProcess.id} readOnly={ currentStep !== 5 } + process={QASUProcess} getCurrentTaskDetails={getCurrentTaskDetails} processPermission={getProcessPermission} + workflowTask={_.find(workflowTasks, ['flow_task', 'Pi Verification'])} + onError={showMessage} projectRoles={getProjectRoles('pi verification')} /> + } + {currentView === 6 && userrole.workflow.decide_acceptance && + <DecideAcceptance onNext={onNext} onCancel={onCancel} id={QASUProcess.id} readOnly={ currentStep !== 6 } + process={QASUProcess} getCurrentTaskDetails={getCurrentTaskDetails} processPermission={getProcessPermission} + workflowTask={_.find(workflowTasks, ['flow_task', 'Decide Acceptance'])} + onError={showMessage} project={getProject()} + projectRoles={getProjectRoles('decide acceptance')} /> + } + {(showIngestTab && currentView === 7) && userrole.workflow.unpin_data && + <Ingesting onNext={onNext} onCancel={onCancel} id={QASUProcess.id} readOnly={ currentStep !== 7 } + onError={showMessage} task={getIngestTask()} /> + } + {currentView === (showIngestTab?8:7) && + <DataProduct onNext={onNext} onCancel={onCancel} id={QASUProcess.id} onError={showMessage} readOnly={ currentStep !== (showIngestTab?8:7) } + tasks={tasks} getCurrentTaskDetails={getCurrentTaskDetails} processPermission={getProcessPermission} + schedulingUnit={schedulingUnit} projectRoles={getProjectRoles('unpin data')} + workflowTask={_.find(workflowTasks, ['flow_task', 'Unpin Data'])} /> + } + {currentView === (showIngestTab?9:8) && + <Done onNext={onNext} onCancel={onCancel} onError={showMessage} + reportingPage={qaReporting} readOnly={ currentStep !== 9 } /> + } + </div> </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 rel="noopener noreferrer" href="https://proxy.lofar.eu/inspect/HTML/" target="_blank">Inspection plots</a> - </label> - <label className="col-sm-10 "> - <a rel="noopener noreferrer" 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={`step-header-${currentStep}`}> - - <Steps model={getStepItems()} activeIndex={currentView - 1} readOnly={false} - onSelect={(e) => e.index<currentStep?setCurrentView(e.index+1):setCurrentView(currentView)} /> - </div> - <div className="step-content"> - {currentView === 1 && - <Scheduled onNext={onNext} onCancel={onCancel} readOnly={ currentStep !== 1 } - schedulingUnit={schedulingUnit} /> - } - {currentView === 2 && - <ProcessingDone onNext={onNext} onCancel={onCancel} readOnly={ currentStep !== 2 } - schedulingUnit={schedulingUnit} /> - } - {} - {currentView === 3 && userrole.workflow.qa_reporting_to && - <QAreporting onNext={onNext} onCancel={onCancel} id={QASUProcess.id} readOnly={ currentStep !== 3 } - process={QASUProcess} getCurrentTaskDetails={getCurrentTaskDetails} - workflowTask={_.find(workflowTasks, ['flow_task', 'Qa Reporting To'])} - onError={showMessage} projectRoles={getProjectRoles('qa reporting to')} /> - } - {currentView === 4 && userrole.workflow.qa_reporting_sos && - <QAsos onNext={onNext} onCancel={onCancel} id={QASUProcess.id} readOnly={ currentStep !== 4 } - process={QASUProcess} getCurrentTaskDetails={getCurrentTaskDetails} - workflowTask={_.find(workflowTasks, ['flow_task', 'Qa Reporting Sos'])} - onError={showMessage} projectRoles={getProjectRoles('qa reporting sos')} /> - } - {currentView === 5 && userrole.workflow.pi_verification && - <PIverification onNext={onNext} onCancel={onCancel} id={QASUProcess.id} readOnly={ currentStep !== 5 } - process={QASUProcess} getCurrentTaskDetails={getCurrentTaskDetails} - workflowTask={_.find(workflowTasks, ['flow_task', 'Pi Verification'])} - onError={showMessage} projectRoles={getProjectRoles('pi verification')} /> - } - {currentView === 6 && userrole.workflow.decide_acceptance && - <DecideAcceptance onNext={onNext} onCancel={onCancel} id={QASUProcess.id} readOnly={ currentStep !== 6 } - process={QASUProcess} getCurrentTaskDetails={getCurrentTaskDetails} - workflowTask={_.find(workflowTasks, ['flow_task', 'Decide Acceptance'])} - onError={showMessage} project={getProject()} - projectRoles={getProjectRoles('decide acceptance')} /> - } - {(showIngestTab && currentView === 7) && userrole.workflow.unpin_data && - <Ingesting onNext={onNext} onCancel={onCancel} id={QASUProcess.id} readOnly={ currentStep !== 7 } - onError={showMessage} task={getIngestTask()} /> - } - {currentView === (showIngestTab?8:7) && - <DataProduct onNext={onNext} onCancel={onCancel} id={QASUProcess.id} onError={showMessage} readOnly={ currentStep !== (showIngestTab?8:7) } - tasks={tasks} getCurrentTaskDetails={getCurrentTaskDetails} - schedulingUnit={schedulingUnit} projectRoles={getProjectRoles('unpin data')} - workflowTask={_.find(workflowTasks, ['flow_task', 'Unpin Data'])} /> - } - {currentView === (showIngestTab?9:8) && - <Done onNext={onNext} onCancel={onCancel} onError={showMessage} - reportingPage={qaReporting} readOnly={ currentStep !== 9 } /> - } - </div> - </div> + </> + } </> } </> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/pi.verification.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/pi.verification.js index 535290a2b922f86b51add8bbb32859a615f5a3d5..50f8200c97e24e4360f508878f1f299fe4f8a1d2 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/pi.verification.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/pi.verification.js @@ -17,8 +17,10 @@ class PIverification extends Component { pi_accept: true, operator_accept: true, quality_within_policy: true, - sos_accept_show_pi: true + sos_accept_show_pi: true, + currentWorkflowTask:{}, }; + this.user =JSON.parse(localStorage.getItem("user")); this.Next = this.Next.bind(this); this.handleChange = this.handleChange.bind(this); this.onChangePIComment = this.onChangePIComment.bind(this); @@ -28,11 +30,13 @@ class PIverification extends Component { async componentDidMount() { let currentWorkflowTask = null; + let processPermission = null; if (this.props.readOnly) { this.getPIVerificationDetails(); } else { currentWorkflowTask = await this.props.getCurrentTaskDetails(); + processPermission = await this.props.processPermission(); } const operatorResponse = await WorkflowService.getQAReportingTo(this.props.process.qa_reporting_to); const sosResponse = await WorkflowService.getQAReportingSOS(this.props.process.qa_reporting_sos); @@ -41,8 +45,9 @@ class PIverification extends Component { content: sosResponse.sos_report, quality_within_policy: sosResponse.quality_within_policy, sos_accept_show_pi: sosResponse.sos_accept_show_pi, - assignTo: this.props.readOnly?this.props.workflowTask.owner:(currentWorkflowTask?currentWorkflowTask.fields.owner:null), + assignTo: this.props.readOnly?this.props.workflowTask.owner:(currentWorkflowTask?currentWorkflowTask.owner_id:null), currentWorkflowTask: currentWorkflowTask, + processPermission: processPermission, }); } @@ -76,16 +81,13 @@ class PIverification extends Component { async Next() { let currentWorkflowTask = await this.props.getCurrentTaskDetails(); const promise = []; - if (currentWorkflowTask && !currentWorkflowTask.fields.owner) { - promise.push(WorkflowService.updateAssignTo(currentWorkflowTask.pk),{ owner: this.state.assignTo }); - } promise.push(WorkflowService.updateQA_Perform(this.props.id,{"pi_report": this.state.comment, "pi_accept": this.state.pi_accept})); Promise.all(promise).then(async(responses) => { if (responses.indexOf(null)<0) { // After completing the current task, get the next task triggered in the workflow and assign project role user for the next task. currentWorkflowTask = await this.props.getCurrentTaskDetails(); - if (!currentWorkflowTask.fields.owner) { - await WorkflowService.updateAssignTo(currentWorkflowTask.pk, "project_role=friend_of_project", { owner: this.state.assignTo }) + if (!currentWorkflowTask.owner_id) { + await WorkflowService.updateAssignTo(currentWorkflowTask.id, "project_role=friend_of_project", { owner: this.state.assignTo }); } this.props.onNext({ report:this.state.content, pireport: this.state.comment, pi_accept: this.state.pi_accept}); } else { @@ -111,7 +113,7 @@ class PIverification extends Component { async assignTaskCB() { let currentWorkflowTask = await this.props.getCurrentTaskDetails(); this.setState({currentWorkflowTask: currentWorkflowTask, - assignTo: currentWorkflowTask.fields.owner, reassign: false}); + assignTo: currentWorkflowTask.owner_id, reassign: false}); } render() { @@ -124,17 +126,25 @@ class PIverification extends Component { <div className="col-lg-3 col-md-3 col-sm-12" data-testid="assignTo" > {/* Display the assigned owner of the task */} {this.state.assignTo && - <>{this.state.assignTo} + <>{!this.props.readOnly ? this.state.currentWorkflowTask.owner_username: this.props.workflowTask.owner_username } {/* Allow to edit the owner if the task is not yet completed */} {!this.props.readOnly && !this.state.reassign && - <Link onClick={e => this.setState({reassign: true})} style={{marginLeft: '10px'}}> - <i className="pi pi-pencil"></i> - </Link>} + <> + {this.state.currentWorkflowTask.editPermissions + ? <Link onClick={e => this.setState({reassign: true})} style={{marginLeft: '10px'}}> + <i className="pi pi-pencil"></i> + </Link> + : <Link style={{ cursor: 'default', pointerEvents: "none", color: 'grey', marginLeft: '10px'}} onClick={e => this.setState({reassign: true})}> + <i className="pi pi-pencil"></i> + </Link> + } + </> + } </> } {/* Display the task assigner if owner is not assigned or to reassign to other user */} {(!this.state.assignTo || this.state.reassign) && this.state.currentWorkflowTask && - <TaskAssigner currentWorkflowTask={this.state.currentWorkflowTask} + <TaskAssigner currentWorkflowTask={this.state.currentWorkflowTask} disableAssignToMe = {this.state.currentWorkflowTask.owner_username === this.user.name? true: false} projectRoles={this.props.projectRoles} callback={this.assignTaskCB} /> } </div> @@ -181,13 +191,26 @@ class PIverification extends Component { <label htmlFor="piAccept" style={{paddingLeft:"5px"}} >As PI / contact author I accept this data</label> {!this.props.readOnly && <div className="p-grid" style={{ marginTop: '20px' }}> <div className="btn-bar"> - <Button disabled={!this.state.content || this.props.readOnly || !this.state.comment} label="Next" className="p-button-primary" icon="pi pi-check" onClick={ this.Next } /> + <Button disabled={this.state.content && this.state.comment && this.state.processPermission && this.state.currentWorkflowTask.editPermissions && this.state.currentWorkflowTask.owner_username === this.user.name? false: true } + label="Next" className="p-button-primary" icon="pi pi-check" onClick={ this.Next } /> </div> <div className="btn-bar"> <Button label="Cancel" className="p-button-danger" icon="pi pi-times" style={{ width : '90px' }} onClick={(e) => { this.props.onCancel()}} /> </div> </div>} + {!this.props.readOnly && + <> + {this.state.currentWorkflowTask.editPermissions && this.state.currentWorkflowTask.owner_username === this.user.name? + <span></span>: <span style={{color: 'red'}}>* You can only save and proceed next if you are assigned to this workflow step</span> + } + </> + } + <div> + {this.state.comment ? + <span></span>: <span style={{color: 'red'}}>* Comments required</span> + } + </div> </div> </div> </> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/qa.reporting.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/qa.reporting.js index e336d31e95fc0ae8bc0136c8d4843ddec77e8d2e..4d7b7c5f093b7f3cf2bf89a7806152cd3a0b77c0 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/qa.reporting.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/qa.reporting.js @@ -19,9 +19,11 @@ class QAreporting extends Component{ this.state={ content: '', assignTo: '', + currentWorkflowTask:{}, operator_accept: true, templateLoading: true }; + this.user =JSON.parse(localStorage.getItem("user")); this.Next = this.Next.bind(this); this.handleChange = this.handleChange.bind(this); this.assignTaskCB = this.assignTaskCB.bind(this); @@ -38,8 +40,9 @@ class QAreporting extends Component{ } else { const defaultTemplate = await QaReportingTemplate.getReportTemplate(this.props.process.su) const currentWorkflowTask = await this.props.getCurrentTaskDetails(); + const processPermission = await this.props.processPermission(); this.setState({defaultTemplate: defaultTemplate, templateLoading: false, - assignTo: currentWorkflowTask.fields.owner, + assignTo: currentWorkflowTask.owner_id, processPermission: processPermission, currentWorkflowTask: currentWorkflowTask}); } } @@ -51,17 +54,13 @@ class QAreporting extends Component{ async Next() { let currentWorkflowTask = await this.props.getCurrentTaskDetails(); const promise = []; - // If no task owner is assigned, assign to the current logged in user - if (currentWorkflowTask && !currentWorkflowTask.fields.owner) { - promise.push(WorkflowService.updateAssignTo(currentWorkflowTask.pk, "", { owner: this.state.assignTo })); - } promise.push(WorkflowService.updateQA_Perform(this.props.id, {"operator_report": this.state.content, "operator_accept": this.state.operator_accept})); Promise.all(promise).then(async(responses) => { if (responses.indexOf(null)<0) { // After completing the current task, get the next task triggered in the workflow and assign project role user for the next task. currentWorkflowTask = await this.props.getCurrentTaskDetails(); - if (!currentWorkflowTask.fields.owner) { - await WorkflowService.updateAssignTo(currentWorkflowTask.pk, "project_role=friend_of_project", { owner: this.state.assignTo }) + if (!currentWorkflowTask.owner_id) { + await WorkflowService.updateAssignTo(currentWorkflowTask.id, "project_role=friend_of_project", { owner: this.state.assignTo }); } this.props.onNext({ report: this.state.content }); } else { @@ -91,7 +90,7 @@ class QAreporting extends Component{ async assignTaskCB() { let currentWorkflowTask = await this.props.getCurrentTaskDetails(); this.setState({currentWorkflowTask: currentWorkflowTask, - assignTo: currentWorkflowTask.fields.owner, reassign: false}); + assignTo: currentWorkflowTask.owner_id, reassign: false}); } render() { @@ -104,17 +103,25 @@ class QAreporting extends Component{ <div className="col-lg-3 col-md-3 col-sm-12" data-testid="assignTo" > {/* Display the assigned owner of the task */} {this.state.assignTo && - <>{this.state.assignTo} + <>{!this.props.readOnly ? this.state.currentWorkflowTask.owner_username: this.props.workflowTask.owner_username } {/* Allow to edit the owner if the task is not yet completed */} {!this.props.readOnly && !this.state.reassign && - <Link onClick={e => this.setState({reassign: true})} style={{marginLeft: '10px'}}> - <i className="pi pi-pencil"></i> - </Link>} + <> + {this.state.currentWorkflowTask.editPermissions + ? <Link onClick={e => this.setState({reassign: true})} style={{marginLeft: '10px'}}> + <i className="pi pi-pencil"></i> + </Link> + : <Link style={{ cursor: 'default', pointerEvents: "none", color: 'grey', marginLeft: '10px'}} onClick={e => this.setState({reassign: true})}> + <i className="pi pi-pencil"></i> + </Link> + } + </> + } </> } {/* Display the task assigner if owner is not assigned or to reassign to other user */} {(!this.state.assignTo || this.state.reassign) && this.state.currentWorkflowTask && - <TaskAssigner currentWorkflowTask={this.state.currentWorkflowTask} + <TaskAssigner currentWorkflowTask={this.state.currentWorkflowTask} disableAssignToMe = {this.state.currentWorkflowTask.owner_username === this.user.name? true: false} projectRoles={this.props.projectRoles} callback={this.assignTaskCB} /> } </div> @@ -152,17 +159,23 @@ class QAreporting extends Component{ </div> {!this.props.readOnly && <div className="p-grid p-justify-start"> <div className="btn-bar"> - <Button disabled={!this.state.content || this.props.readOnly} label="Next" className="p-button-primary" icon="pi pi-check" onClick={ this.Next } /> + <Button disabled={this.state.content && this.state.processPermission && this.state.currentWorkflowTask.editPermissions && this.state.currentWorkflowTask.owner_username === this.user.name? false: true } + label="Next" className="p-button-primary" icon="pi pi-check" onClick={ this.Next } /> </div> <div className="btn-bar"> <Button label="Cancel" className="p-button-danger" icon="pi pi-times" style={{ width : '88px' }} onClick={(e) => { this.props.onCancel()}} /> </div> </div>} + {!this.props.readOnly && + <> + {this.state.currentWorkflowTask.editPermissions && this.state.currentWorkflowTask.owner_username === this.user.name ? + <span></span>: <span style={{color: 'red'}}>* You can only save and proceed next if you are assigned to this workflow step</span>} + </> + } </div> </> ) -}; - +}; } export default QAreporting; \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/qa.reporting.template.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/qa.reporting.template.js index 42f38d150d284dbb4457e0aa9b84b37e8aa79a74..db5aac01a6534552bee06e8df0bf519775073aac 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/qa.reporting.template.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/qa.reporting.template.js @@ -27,13 +27,15 @@ const QaReportingTemplate = { if (task.task_type === "observation") { const taskSuccessors = await TaskService.getTaskSuccessors('blueprint', task.id) let taskDataproducts = _.find(dataproducts.task_blueprints, {'id': task.id}) - for(const successor of taskSuccessors) { - if(successor.task_type === "pipeline") { - successor.subtasks.map(successorSubtask => { - if(successorSubtask.primary === true){ - observation.pipeline.push(successorSubtask.id); - } - }) + if(taskSuccessors) { + for(const successor of taskSuccessors) { + if(successor.task_type === "pipeline") { + successor.subtasks.map(successorSubtask => { + if(successorSubtask.primary === true){ + observation.pipeline.push(successorSubtask.id); + } + }) + } } } let taskDP = []; diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/qa.sos.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/qa.sos.js index 9d9b53db4140e112cc09d6581683c68309540eb7..ba90b53c3f2e0ca244d49419b7fd9f3792125471 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/qa.sos.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/qa.sos.js @@ -15,8 +15,10 @@ class QAreportingSDCO extends Component { showEditor: false, quality_within_policy: true, sos_accept_show_pi: true, - operator_accept: true + operator_accept: true, + currentWorkflowTask:{}, }; + this.user =JSON.parse(localStorage.getItem("user")); this.Next = this.Next.bind(this); this.handleChange = this.handleChange.bind(this); this.getQASOSDetails = this.getQASOSDetails.bind(this); @@ -29,10 +31,11 @@ class QAreportingSDCO extends Component { } else { const currentWorkflowTask = await this.props.getCurrentTaskDetails(); const response = await WorkflowService.getQAReportingTo(this.props.process.qa_reporting_to); + const processPermission = await this.props.processPermission(); this.setState({ content: response.operator_report, - operator_accept: response.operator_accept, - assignTo: currentWorkflowTask.fields.owner, currentWorkflowTask: currentWorkflowTask + operator_accept: response.operator_accept, processPermission: processPermission, + assignTo: currentWorkflowTask.owner_id, currentWorkflowTask: currentWorkflowTask }); } } @@ -65,16 +68,13 @@ class QAreportingSDCO extends Component { async Next() { let currentWorkflowTask = await this.props.getCurrentTaskDetails(); const promise = []; - if (currentWorkflowTask && !currentWorkflowTask.fields.owner) { - promise.push(WorkflowService.updateAssignTo(currentWorkflowTask.pk, { owner: this.state.assignTo })); - } promise.push(WorkflowService.updateQA_Perform(this.props.id, {"sos_report": this.state.content, "sos_accept_show_pi": this.state.sos_accept_show_pi, "quality_within_policy": this.state.quality_within_policy})); Promise.all(promise).then(async(responses) => { if (responses.indexOf(null)<0) { // After completing the current task, get the next task triggered in the workflow and assign project role user for the next task. currentWorkflowTask = await this.props.getCurrentTaskDetails(); - if (!currentWorkflowTask.fields.owner) { - await WorkflowService.updateAssignTo(currentWorkflowTask.pk, "project_role=contact", { owner: this.state.assignTo }) + if (!currentWorkflowTask.owner_id) { + await WorkflowService.updateAssignTo(currentWorkflowTask.id, "project_role=contact", { owner: this.state.assignTo }); } this.props.onNext({ report: this.state.content }); } else { @@ -94,7 +94,7 @@ class QAreportingSDCO extends Component { async assignTaskCB() { let currentWorkflowTask = await this.props.getCurrentTaskDetails(); this.setState({currentWorkflowTask: currentWorkflowTask, - assignTo: currentWorkflowTask.fields.owner, reassign: false}); + assignTo: currentWorkflowTask.owner_id, reassign: false}); } render() { @@ -107,17 +107,25 @@ class QAreportingSDCO extends Component { <div className="col-lg-3 col-md-3 col-sm-12" data-testid="assignTo" > {/* Display the assigned owner of the task */} {this.state.assignTo && - <>{this.state.assignTo} + <>{!this.props.readOnly ? this.state.currentWorkflowTask.owner_username: this.props.workflowTask.owner_username } {/* Allow to edit the owner if the task is not yet completed */} {!this.props.readOnly && !this.state.reassign && - <Link onClick={e => this.setState({reassign: true})} style={{marginLeft: '10px'}}> - <i className="pi pi-pencil"></i> - </Link>} + <> + {this.state.currentWorkflowTask.editPermissions + ? <Link onClick={e => this.setState({reassign: true})} style={{marginLeft: '10px'}}> + <i className="pi pi-pencil"></i> + </Link> + : <Link style={{ cursor: 'default', pointerEvents: "none", color: 'grey', marginLeft: '10px'}} onClick={e => this.setState({reassign: true})}> + <i className="pi pi-pencil"></i> + </Link> + } + </> + } </> } {/* Display the task assigner if owner is not assigned or to reassign to other user */} {(!this.state.assignTo || this.state.reassign) && this.state.currentWorkflowTask && - <TaskAssigner currentWorkflowTask={this.state.currentWorkflowTask} + <TaskAssigner currentWorkflowTask={this.state.currentWorkflowTask} disableAssignToMe = {this.state.currentWorkflowTask.owner_username === this.user.name? true: false} projectRoles={this.props.projectRoles} callback={this.assignTaskCB} /> } </div> @@ -154,13 +162,21 @@ class QAreportingSDCO extends Component { </div> {!this.props.readOnly && <div className="p-grid" style={{ marginTop: '20px' }}> <div className="btn-bar"> - <Button label="Next" disabled={!this.state.content || this.props.readOnly} className="p-button-primary" icon="pi pi-check" onClick={ this.Next } /> + <Button disabled={this.state.content && this.state.processPermission && this.state.currentWorkflowTask.editPermissions && this.state.currentWorkflowTask.owner_username === this.user.name? false: true } + label="Next" className="p-button-primary" icon="pi pi-check" onClick={ this.Next } /> </div> <div className="btn-bar"> <Button label="Cancel" disabled={this.props.readOnly} className="p-button-danger" icon="pi pi-times" style={{ width : '90px' }} onClick={(e) => { this.props.onCancel()}} /> </div> </div>} + {!this.props.readOnly && + <> + {this.state.currentWorkflowTask.editPermissions && this.state.currentWorkflowTask.owner_username === this.user.name? + <span></span>: <span style={{color: 'red'}}>* You can only save and proceed next if you are assigned to this workflow step</span> + } + </> + } </div> </> ) diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/task.assigner.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/task.assigner.js index 2b37bd388b451567a0f6ad6b6bd3270712183a5c..633b34bca47224ecc8ef67da13783585b8f355e8 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/task.assigner.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/task.assigner.js @@ -12,6 +12,7 @@ class TaskAssigner extends Component { this.state = { errors: {} }; + this.user =JSON.parse(localStorage.getItem("user")); this.assignUser = this.assignUser.bind(this); } @@ -41,12 +42,12 @@ class TaskAssigner extends Component { (this.state.assignToRole?`project_role=${this.state.assignToRole}` :(this.state.assignToEmail?`user_email=${this.state.assignToEmail}`:"")); let assignStatus = null; - if (this.props.currentWorkflowTask.fields.owner) { + if (this.props.currentWorkflowTask.owner_id) { if (toUser) { - assignStatus = await WorkflowService.reAssignTo(this.props.currentWorkflowTask.pk, toUser, {}); + assignStatus = await WorkflowService.reAssignTo(this.props.currentWorkflowTask.id, toUser, {}); } } else { - assignStatus = await WorkflowService.updateAssignTo(this.props.currentWorkflowTask.pk, toUser, {}); + assignStatus = await WorkflowService.updateAssignTo(this.props.currentWorkflowTask.id, toUser, {}); } // Callback to the parent component after assigning user to the task. this.props.callback(); @@ -57,17 +58,20 @@ class TaskAssigner extends Component { return ( <> <div> - <Link to="#" onClick={e => {this.assignUser(true)}}> - Assign to me</Link> + { + this.props.currentWorkflowTask.editPermissions && !this.props.disableAssignToMe + ? <Link to="#" className="notDisabled" onClick={e => {this.assignUser(true)}}>Assign to me</Link> + : <Link to="#" style={{ cursor: 'disabled', pointerEvents: "none", color: 'grey'}}>Assign to me</Link> + } </div> - <Dropdown disabled={this.props.readOnly} inputId="assignToValue" + <Dropdown disabled={!this.props.currentWorkflowTask.editPermissions} inputId="assignToValue" tooltip="Assign to user with role (will be assigned to the first user in this role)" value={this.state.assignToRole} optionLabel="displayValue" optionValue="value" onChange={(e) => this.setState({assignToRole: e.value, assignToEmail: null})} options={this.props.projectRoles} placeholder="Project Role" style={{marginBottom: '5px'}} /> <div style={{marginBottom: '5px'}} > - <InputText value={this.state.assignToEmail} placeholder="User Email" + <InputText disable={!this.props.currentWorkflowTask.editPermissions}value={this.state.assignToEmail} placeholder="User Email" tooltip="Assign to user with this email" onChange={e => this.setUserMail(e.target.value)} /> <label className="error"> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/unpin.data.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/unpin.data.js index 6cddb845928e3b4c41114b82a9a2e0eb73d5567b..1197308b498d70f8343f76789af2496b0b8760e4 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/unpin.data.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/unpin.data.js @@ -10,6 +10,7 @@ export default ({ tasks, schedulingUnit, onCancel, ...props }) => { const [showConfirmDialog, setShowConfirmDialog] = useState(false); // const [QASUProcess, setQASUProcess] = useState(); const [currentWorkflowTask, setCurrentWorkflowTask] = useState(); + const [processPermission, setProcessPermission] = useState() const [assignTo, setAssignTo] = useState(); const [reassign, setReassign] = useState(false); const defaultcolumns = [ { @@ -27,6 +28,7 @@ export default ({ tasks, schedulingUnit, onCancel, ...props }) => { const toggleDialog = () => { setShowConfirmDialog(!showConfirmDialog) }; + const user = JSON.parse(localStorage.getItem("user")); /** * Method will trigger on click next buton * here onNext props coming from parent, where will handle redirection to other page @@ -34,9 +36,6 @@ export default ({ tasks, schedulingUnit, onCancel, ...props }) => { const Next = async () => { const currentWorkflowTask = await props.getCurrentTaskDetails(); const promise = []; - if (currentWorkflowTask && !currentWorkflowTask.fields.owner) { - promise.push(WorkflowService.updateAssignTo(currentWorkflowTask.pk, { owner: '' })); - } promise.push(WorkflowService.updateQA_Perform(props.id, {})); Promise.all(promise).then((responses) => { if (responses.indexOf(null)<0) { @@ -54,6 +53,10 @@ export default ({ tasks, schedulingUnit, onCancel, ...props }) => { .then(currentWorkflowTask => { setCurrentWorkflowTask(currentWorkflowTask); }); + props.processPermission() + .then(permission => { + setProcessPermission(permission) + }) } if (props.readOnly && !assignTo) { setAssignTo(props.workflowTask.owner); @@ -66,11 +69,9 @@ export default ({ tasks, schedulingUnit, onCancel, ...props }) => { const assignTaskCB = async() => { let currentWorkflowTask = await props.getCurrentTaskDetails(); setCurrentWorkflowTask(currentWorkflowTask); - setAssignTo(currentWorkflowTask.fields.owner); + setAssignTo(currentWorkflowTask.owner_id); setReassign(false); } - - return ( <div className="p-fluid mt-2"> <div className="p-field p-grid" style={{ paddingLeft: '-10px' }}> @@ -78,17 +79,25 @@ export default ({ tasks, schedulingUnit, onCancel, ...props }) => { <div className="col-lg-3 col-md-3 col-sm-12" data-testid="assignTo" > {/* Display the assigned owner of the task */} {assignTo && - <>{assignTo} + <>{!props.readOnly ? currentWorkflowTask.owner_username: props.workflowTask.owner_username } {/* Allow to edit the owner if the task is not yet completed */} {!props.readOnly && !reassign && - <Link onClick={e => setReassign(true)} style={{marginLeft: '10px'}}> - <i className="pi pi-pencil"></i> - </Link>} + <> + {currentWorkflowTask && currentWorkflowTask.editPermissions + ? <Link onClick={e => setReassign(true)} style={{marginLeft: '10px'}}> + <i className="pi pi-pencil"></i> + </Link> + : <Link style={{ cursor: 'default', pointerEvents: "none", color: 'grey', marginLeft: '10px'}} onClick={e => this.setState({reassign: true})}> + <i className="pi pi-pencil"></i> + </Link> + + } + </>} </> } {/* Display the task assigner if owner is not assigned or to reassign to other user */} {(!assignTo || reassign) && currentWorkflowTask && - <TaskAssigner currentWorkflowTask={currentWorkflowTask} + <TaskAssigner currentWorkflowTask={currentWorkflowTask} disableAssignToMe = {currentWorkflowTask && currentWorkflowTask.owner_username === user.name? true: false} projectRoles={props.projectRoles} callback={assignTaskCB} /> } </div> @@ -112,13 +121,21 @@ export default ({ tasks, schedulingUnit, onCancel, ...props }) => { </div> <div className="p-grid p-justify-start mt-2"> {!props.readOnly && <div className="btn-bar"> - <Button label="Delete" className="p-button-primary" icon="pi pi-trash" onClick={toggleDialog} /> + <Button + disabled={processPermission && currentWorkflowTask && currentWorkflowTask.editPermissions && currentWorkflowTask.owner_username === user.name? false: true } + label="Delete" className="p-button-primary" icon="pi pi-trash" onClick={toggleDialog} /> </div>} {!props.readOnly && <div className="btn-bar"> <Button label="Cancel" className="p-button-danger" icon="pi pi-times" style={{ width: '90px' }} onClick={(e) => { onCancel()}} /> </div>} </div> + {!props.readOnly && + <> + {currentWorkflowTask && currentWorkflowTask.editPermissions && currentWorkflowTask.owner_username === user.name? + <span></span>: <span style={{color: 'red'}}>* You can only delete if you are assigned to this workflow step</span>} + </> + } <div className="p-grid" data-testid="confirm_dialog"> <Dialog header={'Confirm'} visible={showConfirmDialog} style={{ width: '40vw' }} inputId="confirm_dialog" modal={true} onHide={() => setShowConfirmDialog(false)} diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/workflow.list.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/workflow.list.js index 7ea92a0c80ac26671b836799d5a9e971d9970280..a9a7eae35324432f83695fd09f960114e6062f44 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/workflow.list.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/workflow.list.js @@ -310,7 +310,7 @@ class WorkflowList extends Component{ <PageHeader location={this.props.location} title={'Workflow - List'} actions={[]}/> <div style={{marginTop: '15px'}}> - {this.state.isLoading ? <AppLoader/> : (this.state.workflowProcessList.length>0 )? + {this.state.isLoading ? <AppLoader/> : <> <div className="p-select " style={{position: 'relative', marginLeft: '27em', marginTop: '-2em'}}> <div className="p-field p-grid"> @@ -359,7 +359,6 @@ class WorkflowList extends Component{ storeFilter={true} /> </> - :<div>No Workflow Process SU found</div> } </div> </>: <AccessDenied/> } diff --git a/SAS/TMSS/frontend/tmss_webapp/src/services/project.service.js b/SAS/TMSS/frontend/tmss_webapp/src/services/project.service.js index 7140b0edce9e64e5f3e2c1e2f1569afdaa7dafc1..f0f2c47ae7e0233a9df5c234a3858b8b15ffd406 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/services/project.service.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/services/project.service.js @@ -66,6 +66,16 @@ const ProjectService = { console.error(error); } }, + getProjectStates: async function() { + try { + const url = `/api/project_state/`; + const response = await axios.get(url); + return response.data.results; + } catch (error) { + console.error(error); + return []; + } + }, saveProject: async function(project, projectQuota) { let archive_location = project.archive_location; try { 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 754be5119c8b4abe6af04cfbca511db1e577f179..26bcb9e85ed3f883d142772bca11e42ee8b0a169 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/services/schedule.service.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/services/schedule.service.js @@ -791,6 +791,17 @@ const ScheduleService = { console.error('[project.services.getSchedulingListByProject]', error); } }, + getSchedulingListByStatus: async function (status) { + try { + let url = `/api/scheduling_unit_blueprint/?ordering=name&expand=${SU_EXPAND_FIELDS.join()}&fields=${SU_FETCH_FIELDS.join()}` + url = url + `&status=${status}` + const response = await axios.get(url); + return response.data.results; + } catch (error) { + console.error(error); + return []; + }; + }, getSchedulingBySet: async function (id) { try { const response = await axios.get(`/api/scheduling_set/${id}/scheduling_unit_draft/?ordering=id`); diff --git a/SAS/TMSS/frontend/tmss_webapp/src/services/workflow.service.js b/SAS/TMSS/frontend/tmss_webapp/src/services/workflow.service.js index 28b9ba276372bb164d44f0df2c829ce99d0b0d6f..410c70f39c4489fcd59d693bd077f21e75de63d4 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/services/workflow.service.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/services/workflow.service.js @@ -33,6 +33,30 @@ const WorkflowService = { } return data; }, + getWorkflowTaskPermission: async function(id) { + try { + const response = await axios.get(`/workflow_api/scheduling_unit_flow/qa_scheduling_unit_task/${id}`); + let res = {} + res.headers = response.headers['access-control-allow-methods']; + res.user_name = response.data.owner_username; + return res; + + } catch(error) { + console.error('[workflow.services.updateAssignTo]',error); + return null; + } + }, + getWorkflowProcessPermission: async function(id) { + try { + const response = await axios.get(`/workflow_api/scheduling_unit_flow/qa_scheduling_unit_process/${id}`); + const res = response.headers['access-control-allow-methods']; + return res; + + } catch(error) { + console.error('[workflow.services.updateAssignTo]',error); + return null; + } + }, updateAssignTo: async (id, toUser, data) => { try { const response = await axios.post(`/workflow_api/scheduling_unit_flow/qa_scheduling_unit_task/${id}/assign/?${toUser}`, data); diff --git a/SAS/TMSS/frontend/tmss_webapp/src/shared/timeline.constants.js b/SAS/TMSS/frontend/tmss_webapp/src/shared/timeline.constants.js index 9c592d65eb1f14c42f82dc9562a73836edf054d3..0c8bd0f2b0d29e6fe2dba8cc9cbcc15cda51406c 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/shared/timeline.constants.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/shared/timeline.constants.js @@ -40,6 +40,51 @@ const TimelineConstants = { name: "Stations (CS/RS/IS)" } }], + UNSCEDULABLE_SU_LIST_DEFAULT_COLUMNS: [{ + + viewSummaryAction: { + name: "View Summary", + filter: "none", + disableSortBy: true + }, + status: { + name:"Status", + filter: "multiselect-filter", + }, + do_cancel: { + name:"Cancelled" + }, + start_time: + { + name: "Start Time", + format: UIConstants.CALENDAR_DATETIME_FORMAT + }, + stop_time: { + name: "End Time", + format: UIConstants.CALENDAR_DATETIME_FORMAT + }, + duration: { + name: "Duration (HH:mm:ss)" + }, + id: { + name: "Id" + }, + name: { + name: "Name" + }, + workflowStatus: { + name: "Workflow Status" + }, + priority_rank: { + name: "Priority" + }, + project: { + name: "Project" + }, + stationGroupCount: { + name: "Stations (CS/RS/IS)" + } + }], SU_LIST_OPTIONAL_COLUMNS: [{ description: { name: "Description" @@ -57,6 +102,7 @@ const TimelineConstants = { actionpath: "actionpath" }], SU_LIST_COLUMN_ORDER: [ + "View Summary", "Status", "Cancelled", "Start Time", @@ -72,9 +118,9 @@ const TimelineConstants = { "Template name" ], SU_LIST_COLUMN_CLASSES: [{ - "Status": "filter-input-100", "Cancelled": "filter-input-50", + "Cancelled": "filter-input-50", "Status": "filter-input-100", "Start Time": "filter-input-75", "End Time": "filter-input-75", - "Id": "filter-input-50", "Name": "filter-input-150", + "Id": "filter-input-50", "Name": "filter-input-150", "View Summary": "filter-input-0", "Workflow Status": "filter-input-75", "description": "filter-input-125", "Priority": "filter-input-50", "Project": "filter-input-75", "Stations (CS/RS/IS)":"filter-input-75", "Task content": "filter-input-75", @@ -163,7 +209,7 @@ const TimelineConstants = { "Updated At" ], TASK_LIST_COLUMN_CLASSES: [{ - "Status Logs": "filter-input-50", "Type": "filter-input-100", + "Status Logs": "filter-input-0", "Type": "filter-input-100", "Status": "filter-input-100", "Cancelled": "filter-input-50", "Start Time": "filter-input-75", "End Time": "filter-input-75", "Task Id": "filter-input-50", "Name": "filter-input-100",