diff --git a/SAS/TMSS/frontend/tmss_webapp/src/App.js b/SAS/TMSS/frontend/tmss_webapp/src/App.js index 74a6f8c2bd0dc57fea26971a4d83c1bf3d076c4b..f7800c0f6da31b54be802cfb909310de8fc8a4f3 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/App.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/App.js @@ -34,7 +34,8 @@ class App extends Component { overlayMenuActive: localStorage.getItem('overlayMenuActive') === 'true' ? true : false, mobileMenuActive: localStorage.getItem('mobileMenuActive') === 'true' ? true : false, authenticated: Auth.isAuthenticated(), - redirect: (Auth.isAuthenticated() && window.location.pathname === "/login")?"/":window.location.pathname + redirect: (Auth.isAuthenticated() && window.location.pathname === "/login")?"/":window.location.pathname, + findObjectPlaceholder: 'Sub Task', }; this.onWrapperClick = this.onWrapperClick.bind(this); this.onToggleMenu = this.onToggleMenu.bind(this); @@ -43,13 +44,15 @@ class App extends Component { this.setPageTitle = this.setPageTitle.bind(this); this.loggedIn = this.loggedIn.bind(this); this.logout = this.logout.bind(this); + this.setSearchField = this.setSearchField.bind(this); this.menu = [ {label: 'Dashboard', icon: 'pi pi-fw pi-home', to:'/dashboard',section: 'dashboard'}, {label: 'Cycle', icon:'pi pi-fw pi-spinner', to:'/cycle',section: 'cycle'}, {label: 'Project', icon: 'fab fa-fw fa-wpexplorer', to:'/project',section: 'project'}, {label: 'Scheduling Units', icon: 'pi pi-fw pi-calendar', to:'/schedulingunit',section: 'schedulingunit'}, + {label: 'Tasks', icon: 'pi pi-fw pi-check-square', to:'/task'}, {label: 'Timeline', icon: 'pi pi-fw pi-clock', to:'/su/timelineview',section: 'su/timelineview'}, - // {label: 'Tasks', icon: 'pi pi-fw pi-check-square', to:'/task'}, + ]; } @@ -130,6 +133,19 @@ class App extends Component { this.setState({authenticated: false, redirect:"/"}); } + /** + * Set search param + * @param {*} key + * @param {*} value + */ + setSearchField(key, value) { + this.setState({ + objectType: key, + findObjectId: value, + redirect:"/find/object/"+key+"/"+value + }); + } + render() { const wrapperClass = classNames('layout-wrapper', { 'layout-overlay': this.state.layoutMode === 'overlay', @@ -150,12 +166,17 @@ class App extends Component { {/* Load main routes and application only if the application is authenticated */} {this.state.authenticated && <> - <AppTopbar onToggleMenu={this.onToggleMenu} isLoggedIn={this.state.authenticated} onLogout={this.logout}></AppTopbar> + <AppTopbar + onToggleMenu={this.onToggleMenu} + isLoggedIn={this.state.authenticated} + onLogout={this.logout} + setSearchField={this.setSearchField} + /> <Router basename={ this.state.currentPath }> <AppMenu model={this.menu} onMenuItemClick={this.onMenuItemClick} layoutMode={this.state.la} active={this.state.menuActive}/> <div className="layout-main"> {this.state.redirect && - <Redirect to={{pathname: this.state.redirect}} />} + <Redirect to={{pathname: this.state.redirect }}/> } <AppBreadCrumbWithRouter setPageTitle={this.setPageTitle} /> <RoutedContent /> </div> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/components/Timeline/CalendarTimeline.js b/SAS/TMSS/frontend/tmss_webapp/src/components/Timeline/CalendarTimeline.js index 19ee68919a9d2397e8e4691152bc713246427d73..237eefd86136b5d7ee9fcd14b08528f5413962bf 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/components/Timeline/CalendarTimeline.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/components/Timeline/CalendarTimeline.js @@ -170,6 +170,10 @@ export class CalendarTimeline extends Component { return true; } + componentWillUnmount() { + this.componentUnmounting = true; // Variable to check and terminate any API calls in loop + } + /** * Sets current UTC and LST time either from the server or locally. * @param {boolean} systemClock - to differetiate whether tosync with server or local update @@ -719,7 +723,7 @@ export class CalendarTimeline extends Component { zIndex: item.type==="SUNTIME"?79:80 }, onMouseDown: () => { - if (item.type !== "SUNTIME" && item.type !== "RESERVATION") { + if (item.type !== "SUNTIME") { this.onItemClick(item); } else { @@ -814,7 +818,9 @@ export class CalendarTimeline extends Component { * @param {Object} item */ onItemMouseOver(evt, item) { - if ((item.type==="SCHEDULE" || item.type==="TASK") && this.props.itemMouseOverCallback) { + if ((item.type==="SCHEDULE" || item.type==="TASK" || item.type==="RESERVATION") + && this.props.itemMouseOverCallback) { + this.setState({mouseEvent: true}); this.props.itemMouseOverCallback(evt, item); } } @@ -824,7 +830,9 @@ export class CalendarTimeline extends Component { * @param {Object} item */ onItemMouseOut(evt, item) { - if ((item.type==="SCHEDULE" || item.type==="TASK") && this.props.itemMouseOutCallback) { + if ((item.type==="SCHEDULE" || item.type==="TASK"|| item.type==="RESERVATION") + && this.props.itemMouseOutCallback) { + this.setState({mouseEvent: true}); this.props.itemMouseOutCallback(evt); } } @@ -835,13 +843,14 @@ export class CalendarTimeline extends Component { * @param {moment} endTime */ async changeDateRange(startTime, endTime, refreshData) { - if (this.props.showSunTimings && this.state.viewType===UIConstants.timeline.types.NORMAL) { + if (this.props.showSunTimings && this.state.viewType===UIConstants.timeline.types.NORMAL && !this.loadingNormalSuntimes) { this.setNormalSuntimings(startTime, endTime); } const result = await this.props.dateRangeCallback(startTime, endTime, refreshData); - if (!this.props.showSunTimings && this.state.viewType === UIConstants.timeline.types.NORMAL) { + if (!this.props.showSunTimings && this.state.viewType === UIConstants.timeline.types.NORMAL && !this.loadingStationSunTimes) { result.items = await this.addStationSunTimes(startTime, endTime, result.group, result.items); - } else if (this.state.viewType === UIConstants.timeline.types.WEEKVIEW) { + result.items = _.orderBy(result.items, ['type'], ['desc']); + } else if (this.state.viewType === UIConstants.timeline.types.WEEKVIEW && !this.loadingWeekSunTimes) { let group = DEFAULT_GROUP.concat(result.group); result.items = await this.addWeekSunTimes(startTime, endTime, group, result.items); } @@ -853,14 +862,16 @@ export class CalendarTimeline extends Component { * @param {moment} startTime * @param {moment} endTime */ - setNormalSuntimings(startTime, endTime) { + async setNormalSuntimings(startTime, endTime) { let sunRiseTimings = [], sunSetTimings = [], sunTimeMap={}; const noOfDays = endTime.diff(startTime, 'days'); - for (const number of _.range(noOfDays+3)) { // Added 3 to have suntimes of day before start time and day after end time so that for small time duration also, suntimes will be available. - let prevStartTime = startTime.clone().add(-1, 'days'); - const date = prevStartTime.clone().add(number, 'days').hours(12).minutes(0).seconds(0); - const formattedDate = date.format("YYYY-MM-DD"); - UtilService.getSunTimings(formattedDate).then(timings => { + if (!this.loadingNormalSuntimes) { + this.loadingNormalSuntimes = true; + for (const number of _.range(noOfDays+3)) { // Added 3 to have suntimes of day before start time and day after end time so that for small time duration also, suntimes will be available. + let prevStartTime = startTime.clone().add(-1, 'days'); + const date = prevStartTime.clone().add(number, 'days').hours(12).minutes(0).seconds(0); + const formattedDate = date.format("YYYY-MM-DD"); + let timings = await UtilService.getSunTimings(formattedDate); if (timings) { const sunriseStartTime = moment.utc(timings.sun_rise.start.split('.')[0]); const sunriseEndTime = moment.utc(timings.sun_rise.end.split('.')[0]); @@ -877,7 +888,10 @@ export class CalendarTimeline extends Component { sunTimeMap[formattedDate] = {sunrise: sunriseTime, sunset: sunsetTime}; this.setState({sunRiseTimings: sunRiseTimings, sunSetTimings: sunSetTimings, sunTimeMap: sunTimeMap}); } - }); + if (number === (noOfDays+2)) { + this.loadingNormalSuntimes = false; + } + } } } @@ -891,78 +905,84 @@ export class CalendarTimeline extends Component { async addStationSunTimes(startTime, endTime, stationGroup, items) { const noOfDays = endTime.diff(startTime, 'days'); let sunItems = _.cloneDeep(items); + this.loadingStationSunTimes = true; for (const number of _.range(noOfDays+1)) { for (const station of stationGroup) { - const date = startTime.clone().add(number, 'days').hours(12).minutes(0).seconds(0); - const timings = await UtilService.getSunTimings(date.format("YYYY-MM-DD"), station.id); - if (timings) { - let sunriseItem = { id: `sunrise-${number}-${station.id}`, - group: station.id, - // title: `${timings.sun_rise.start} to ${timings.sun_rise.end}`, - title: "", - project: "", - name: "", - duration: "", - start_time: moment.utc(timings.sun_rise.start), - end_time: moment.utc(timings.sun_rise.end), - bgColor: "yellow", - selectedBgColor: "yellow", - type: "SUNTIME"}; - sunItems.push(sunriseItem); - let sunsetItem = _.cloneDeep(sunriseItem); - sunsetItem.id = `sunset-${number}-${station.id}`; - // sunsetItem.title = `${timings.sun_set.start} to ${timings.sun_set.end}`; - sunsetItem.title = ""; - sunsetItem.start_time = moment.utc(timings.sun_set.start); - sunsetItem.end_time = moment.utc(timings.sun_set.end); - sunsetItem.bgColor = "orange"; - sunsetItem.selectedBgColor = "orange"; - sunItems.push(sunsetItem); - let befSunriseItem = _.cloneDeep(sunriseItem); - befSunriseItem.id = `bef-sunrise-${number}-${station.id}`; - // sunsetItem.title = `${timings.sun_set.start} to ${timings.sun_set.end}`; - befSunriseItem.title = ""; - befSunriseItem.start_time = moment.utc(timings.sun_rise.start).hours(0).minutes(0).seconds(0); - befSunriseItem.end_time = moment.utc(timings.sun_rise.start); - befSunriseItem.bgColor = "grey"; - befSunriseItem.selectedBgColor = "grey"; - sunItems.push(befSunriseItem); - let afterSunsetItem = _.cloneDeep(sunriseItem); - afterSunsetItem.id = `aft-sunset-${number}-${station.id}`; - // sunsetItem.title = `${timings.sun_set.start} to ${timings.sun_set.end}`; - afterSunsetItem.title = ""; - afterSunsetItem.start_time = moment.utc(timings.sun_set.end); - afterSunsetItem.end_time = moment.utc(timings.sun_set.end).hours(23).minutes(59).seconds(59); - afterSunsetItem.bgColor = "grey"; - afterSunsetItem.selectedBgColor = "grey"; - sunItems.push(afterSunsetItem); - let dayItem = _.cloneDeep(sunriseItem); - dayItem.id = `day-${number}-${station.id}`; - // sunsetItem.title = `${timings.sun_set.start} to ${timings.sun_set.end}`; - dayItem.title = ""; - dayItem.start_time = moment.utc(timings.sun_rise.end); - dayItem.end_time = moment.utc(timings.sun_set.start); - dayItem.bgColor = "white"; - dayItem.selectedBgColor = "white"; - sunItems.push(dayItem); + if (!this.componentUnmounting) { + const date = startTime.clone().add(number, 'days').hours(12).minutes(0).seconds(0); + const timings = await UtilService.getSunTimings(date.format("YYYY-MM-DD"), station.id); + if (timings) { + let sunriseItem = { id: `sunrise-${number}-${station.id}`, + group: station.id, + // title: `${timings.sun_rise.start} to ${timings.sun_rise.end}`, + title: "", + project: "", + name: "", + duration: "", + start_time: moment.utc(timings.sun_rise.start), + end_time: moment.utc(timings.sun_rise.end), + bgColor: "yellow", + selectedBgColor: "yellow", + type: "SUNTIME"}; + sunItems.push(sunriseItem); + let sunsetItem = _.cloneDeep(sunriseItem); + sunsetItem.id = `sunset-${number}-${station.id}`; + // sunsetItem.title = `${timings.sun_set.start} to ${timings.sun_set.end}`; + sunsetItem.title = ""; + sunsetItem.start_time = moment.utc(timings.sun_set.start); + sunsetItem.end_time = moment.utc(timings.sun_set.end); + sunsetItem.bgColor = "orange"; + sunsetItem.selectedBgColor = "orange"; + sunItems.push(sunsetItem); + let befSunriseItem = _.cloneDeep(sunriseItem); + befSunriseItem.id = `bef-sunrise-${number}-${station.id}`; + // sunsetItem.title = `${timings.sun_set.start} to ${timings.sun_set.end}`; + befSunriseItem.title = ""; + befSunriseItem.start_time = moment.utc(timings.sun_rise.start).hours(0).minutes(0).seconds(0); + befSunriseItem.end_time = moment.utc(timings.sun_rise.start); + befSunriseItem.bgColor = "grey"; + befSunriseItem.selectedBgColor = "grey"; + sunItems.push(befSunriseItem); + let afterSunsetItem = _.cloneDeep(sunriseItem); + afterSunsetItem.id = `aft-sunset-${number}-${station.id}`; + // sunsetItem.title = `${timings.sun_set.start} to ${timings.sun_set.end}`; + afterSunsetItem.title = ""; + afterSunsetItem.start_time = moment.utc(timings.sun_set.end); + afterSunsetItem.end_time = moment.utc(timings.sun_set.end).hours(23).minutes(59).seconds(59); + afterSunsetItem.bgColor = "grey"; + afterSunsetItem.selectedBgColor = "grey"; + sunItems.push(afterSunsetItem); + let dayItem = _.cloneDeep(sunriseItem); + dayItem.id = `day-${number}-${station.id}`; + // sunsetItem.title = `${timings.sun_set.start} to ${timings.sun_set.end}`; + dayItem.title = ""; + dayItem.start_time = moment.utc(timings.sun_rise.end); + dayItem.end_time = moment.utc(timings.sun_set.start); + dayItem.bgColor = "white"; + dayItem.selectedBgColor = "white"; + sunItems.push(dayItem); + } else { + /* If no sunrise and sunset, show it as night time. Later it should be done as either day or night. */ + let befSunriseItem = { id: `bef-sunrise-${number}-${station.id}`, + group: station.id, + // title: `${timings.sun_rise.start} to ${timings.sun_rise.end}`, + title: "", + project: "", + name: "", + duration: "", + start_time: moment.utc(date.format("YYYY-MM-DD 00:00:00")), + end_time: moment.utc(date.format("YYYY-MM-DD 23:59:59")), + bgColor: "grey", + selectedBgColor: "grey", + type: "SUNTIME"}; + sunItems.push(befSunriseItem); + } } else { - /* If no sunrise and sunset, show it as night time. Later it should be done as either day or night. */ - let befSunriseItem = { id: `bef-sunrise-${number}-${station.id}`, - group: station.id, - // title: `${timings.sun_rise.start} to ${timings.sun_rise.end}`, - title: "", - project: "", - name: "", - duration: "", - start_time: moment.utc(date.format("YYYY-MM-DD 00:00:00")), - end_time: moment.utc(date.format("YYYY-MM-DD 23:59:59")), - bgColor: "grey", - selectedBgColor: "grey", - type: "SUNTIME"}; - sunItems.push(befSunriseItem); + break; } } } + this.loadingStationSunTimes = false; if (!this.props.showSunTimings && this.state.viewType === UIConstants.timeline.types.NORMAL) { items = sunItems; } @@ -978,6 +998,7 @@ export class CalendarTimeline extends Component { */ async addWeekSunTimes(startTime, endTime, weekGroup, items) { let sunItems = _.cloneDeep(items); + this.loadingWeekSunTimes = true; for (const weekDay of weekGroup) { if (weekDay.value) { const timings = await UtilService.getSunTimings(weekDay.value.format("YYYY-MM-DD"), 'CS001'); @@ -1028,6 +1049,7 @@ export class CalendarTimeline extends Component { sunItems.push(afterSunsetItem); } } + this.loadingWeekSunTimes = false; } if (this.state.viewType === UIConstants.timeline.types.WEEKVIEW) { items = _.orderBy(sunItems, ['type'], ['desc']); @@ -1245,22 +1267,26 @@ export class CalendarTimeline extends Component { * @param {Object} props */ async updateTimeline(props) { - this.setState({ showSpinner: true }); - let group = DEFAULT_GROUP.concat(props.group); - if (!this.props.showSunTimings && this.state.viewType === UIConstants.timeline.types.NORMAL) { - props.items = await this.addStationSunTimes(this.state.defaultStartTime, this.state.defaultEndTime, props.group, props.items); - } else if(this.props.showSunTimings && this.state.viewType === UIConstants.timeline.types.NORMAL) { - this.setNormalSuntimings(this.state.defaultStartTime, this.state.defaultEndTime); - } else if (this.state.viewType === UIConstants.timeline.types.WEEKVIEW) { - props.items = await this.addWeekSunTimes(this.state.defaultStartTime, this.state.defaultEndTime, group, props.items); + if (!this.state.mouseEvent) { // No need to update timeline items for mouseover and mouseout events + // this.setState({ showSpinner: true }); + let group = DEFAULT_GROUP.concat(props.group); + if (!this.props.showSunTimings && this.state.viewType === UIConstants.timeline.types.NORMAL && !this.loadingStationSunTimes) { + props.items = await this.addStationSunTimes(this.state.defaultStartTime, this.state.defaultEndTime, props.group, props.items); + } else if(this.props.showSunTimings && this.state.viewType === UIConstants.timeline.types.NORMAL && !this.loadingNormalSuntimes) { + this.setNormalSuntimings(this.state.defaultStartTime, this.state.defaultEndTime); + } else if (this.state.viewType === UIConstants.timeline.types.WEEKVIEW && !this.loadingWeekSunTimes) { + props.items = await this.addWeekSunTimes(this.state.defaultStartTime, this.state.defaultEndTime, group, props.items); + } + this.setState({group: group, showSpinner: false, items: _.orderBy(props.items, ['type'], ['desc'])}); + } else { + this.setState({mouseEvent: false}); } - this.setState({group: group, showSpinner: false, items: _.orderBy(props.items, ['type'], ['desc'])}); } render() { return ( <React.Fragment> - <CustomPageSpinner visible={this.state.showSpinner} /> + {/* <CustomPageSpinner visible={this.state.showSpinner} /> */} {/* Toolbar for the timeline */} <div className={`p-fluid p-grid timeline-toolbar ${this.props.className}`}> {/* Clock Display */} @@ -1345,7 +1371,7 @@ export class CalendarTimeline extends Component { <div className='col-1 su-legend su-finished' title="Finished">Finished</div> </div> </div> - {!this.props.showSunTimings && this.state.viewType===UIConstants.timeline.types.NORMAL && + {!this.props.showSunTimings && <div className="col-3"> <div style={{fontWeight:'500', height: '25px'}}>Station Reservation</div> <div className="p-grid"> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/layout/components/AppTopbar.js b/SAS/TMSS/frontend/tmss_webapp/src/layout/components/AppTopbar.js index f112943d779cdedc9448a0f7ff2f42ce10fab3c2..6625eb1ea1cb57c76a93d7f35e14ce598edd2a98 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/layout/components/AppTopbar.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/layout/components/AppTopbar.js @@ -5,19 +5,17 @@ import 'primereact/resources/themes/nova-light/theme.css'; import 'primereact/resources/primereact.css'; import 'primeflex/primeflex.css'; import { PropTypes } from 'prop-types'; - import Auth from '../../authenticate/auth'; - +import { FindObject } from './FindObject'; export class AppTopbar extends Component { constructor(props) { super(props); this.state = { - username: Auth.getUser().name + username: Auth.getUser().name, }; } - static defaultProps = { onToggleMenu: null } @@ -31,9 +29,11 @@ export class AppTopbar extends Component { <React.Fragment> <div className="layout-wrapper layout-static layout-static-sidebar-inactive"> <div className="layout-topbar clearfix"> + <button className="p-link layout-menu-button" onClick={this.props.onToggleMenu}> <i className="pi pi-bars"></i></button> <span className="header-title">TMSS</span> + {this.props.isLoggedIn && <div className="top-right-bar"> <span><i className="fa fa-user"></i>{this.state.username}</span> @@ -41,6 +41,7 @@ export class AppTopbar extends Component { <i className="pi pi-power-off"></i></button> </div> } + <FindObject setSearchField={this.props.setSearchField} /> </div> </div> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/layout/components/FindObject.js b/SAS/TMSS/frontend/tmss_webapp/src/layout/components/FindObject.js new file mode 100644 index 0000000000000000000000000000000000000000..530ba4d002023dce0d7dacdd940e4d5db175c50e --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/layout/components/FindObject.js @@ -0,0 +1,109 @@ +import React, {Component} from 'react'; +import { Dropdown } from 'primereact/dropdown'; +import _ from 'lodash'; +import { appGrowl , setAppGrowl } from './AppGrowl'; +import { Growl } from 'primereact/components/growl/Growl'; +import { InputText } from 'primereact/inputtext'; + +export class FindObject extends Component { + + constructor(props) { + super(props); + this.state = { + // Find Object - dropdown list value + objectTypes: [ + {name: 'Scheduling Unit', code: 'sublueprint'}, + {name: 'Task', code: 'taskblueprint'}, + {name: 'Subtask', code: 'subtask'}, + // {name: 'Task Draft', code: 'taskdraft'}, + //{name: 'SU Draft', code: 'sudraft'}, + // {name: 'Project', code: 'project'}, + ], + objectId: '', + objectType: {name: 'Scheduling Unit', code: 'sublueprint'} + }; + this.findObject = this.findObject.bind(this); + this.setObjectType = this.setObjectType.bind(this); + this.setFindObjectId = this.setFindObjectId.bind(this); + this.handleEvent = this.handleEvent.bind(this); + } + + /** + * + * @param {Key Event} e - Key code + */ + handleEvent(e) { + var key = e.which || e.keyCode; + if(key === 13 || key === 'Enter') { + this.findObject(); + } + } + + /** + * Set Object Type + * @param {String} value - Object type value + */ + setObjectType(value) { + if (value.name && value.name === 'Project') { + this.setState({objectType: value}); + } else if(isNaN(this.state.objectId)){ + this.setState({objectType: value, objectId: ''}); + } else { + this.setState({objectType: value}); + } + } + + /** + * Set Object id value + * @param {String/Number} value - Object id, accepts alphanumeric if object type is 'Project' + */ + setFindObjectId(value) { + if (this.state.objectType.name === 'Project' || !isNaN(value)) { + this.setState({objectId: value}); + } else{ + appGrowl.show({severity: 'info', summary: 'Information', detail: 'Enter valid object Id'}); + } + } + + /** + * Callback function to find Object + */ + findObject() { + if (this.state.objectId && this.state.objectId.length > 0) { + this.props.setSearchField(this.state.objectType.code, this.state.objectId); + } else { + appGrowl.show({severity: 'info', summary: 'Information', detail: 'Enter Object Id'}); + } + } + + render() { + return ( + <React.Fragment> + <Growl ref={(el) => setAppGrowl(el)} /> + <div className="top-right-bar find-object-search" style={{marginRight: '1em'}}> + <Dropdown + className="p-link layout-menu-button find-object-type" + value={this.state.objectType} + options={this.state.objectTypes} + optionLabel="name" + onChange={(e) => {this.setObjectType(e.value)}} + /> + + + <InputText + value={this.state.objectId} + onChange={(e) => {this.setFindObjectId(e.target.value)}} + title='Enter Object Id to search Object' + className="find-object-search-input" + placeholder="Search by ID" + onKeyDown={this.handleEvent} + /> + <button className="p-link layout-menu-button" style={{float: 'right'}} onClick={this.findObject} > + <i className="pi pi-search find-object-search-btn" /> + </button> + + </div> + </React.Fragment> + ); + } +} diff --git a/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_content.scss b/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_content.scss index 5c49ad86c0d840f2b6876fd5662d5ca981e34331..16fda99097df01c69485d146d9d0bb3940775d50 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_content.scss +++ b/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_content.scss @@ -3,4 +3,9 @@ padding: 60px 16px 16px 25px; min-height: 95vh; background-color: white; +} + +.find-obj-tree-view { + margin-left: 1em; + margin-right: 1em; } \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_timeline.scss b/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_timeline.scss index 7cf493ad504c52e7f507c474d121e00df44f57e1..e9e71c99a8042f651bc6227ef639e90b6f6841b5 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_timeline.scss +++ b/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_timeline.scss @@ -242,7 +242,7 @@ color: white; } -.reserve.dynamic { +.reserve-dynamic { background-color: #9b9999; color: white; } diff --git a/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_topbar.scss b/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_topbar.scss index 6c190d5e90b4061687c6da38aa6fdc6f3246ccfb..a7a0ff6d53998a391bc5b943bc20ba5f4b133170 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_topbar.scss +++ b/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_topbar.scss @@ -118,9 +118,10 @@ color: $topbarItemColor; @include transition(color $transitionDuration); - span { + // Search type dropdown arrow looks bigger in topbar, + /* span { font-size: 2em; - } + }*/ &:hover { color: $topbarItemHoverColor; @@ -143,4 +144,30 @@ .top-right-bar button { padding-left: 5px; -} \ No newline at end of file +} + +.find-object-search { + padding-top: 0px; + +} + +.find-object-search-input { + border-inline-start-width: 0px; + border-inline-end-width: 2em !important; + width: 11em; +} + +.find-object-search-btn { + display: inline-block; + right: 27px; + position: relative; + top: 6px; + color: darkblue; +} + +.find-object-type { + width: 12em; + right:1em; +} + + \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Search/find.object.result.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Search/find.object.result.js new file mode 100644 index 0000000000000000000000000000000000000000..d341e5e30893b8fee51cdd2e253028804c5949a8 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Search/find.object.result.js @@ -0,0 +1,231 @@ +import React, {Component} from 'react'; +import PageHeader from '../../layout/components/PageHeader'; +import AppLoader from '../../layout/components/AppLoader'; +import { Tree } from 'primereact/tree'; +import TaskService from './../../services/task.service'; +import ScheduleService from './../../services/schedule.service'; +import ProjectService from './../../services/project.service'; + +export class FindObjectResult extends Component{ + constructor(props){ + super(props); + this.state = { + objNodes: [], + expandedKeys: {}, + isLoading: true + }; + this.schedulingSetList= {}; + this.projectsList= {}; + this.data= {}; + this.expandAll = this.expandAll.bind(this); + this.expandNode = this.expandNode.bind(this); + } + + + componentDidUpdate(prevProps, prevState) { + const objectType = this.props.match.params.type; + const objectId = this.props.match.params.id; + const prevObjectType = prevProps.match.params.type; + const prevObjectId = prevProps.match.params.id; + if(objectType !== prevObjectType || objectId !== prevObjectId){ + this.findObject(); + } + } + + componentDidMount(){ + this.findObject(); + } + + /** + * Find Object based in search id + */ + async findObject(){ + let objNodes = []; + this.setState({objNodes: objNodes, isLoading: true}); + const objectType = this.props.match.params.type;//(this.props.location.state && this.props.location.state.objectType)?this.props.location.state.objectType:''; + const objectid = this.props.match.params.id; + if (objectType === 'subtask') { + objNodes = await this.findSubTask(objectid); + } + else if (objectType === 'taskdraft') { + objNodes = await this.findTask('draft', objectid); + } + else if (objectType === 'taskblueprint') { + objNodes = await this.findTask('blueprint', objectid); + } + else if (objectType === 'sublueprint') { + objNodes = await this.findSchedulingUnit('blueprint', objectid); + } + else if (objectType === 'sudraft') { + objNodes = await this.findSchedulingUnit('draft', objectid); + } + else if (objectType === 'project') { + objNodes = await this.findProject(objectid); + } + this.setState({objNodes: objNodes, isLoading: false}); + this.expandAll(); + } + + /** + * Find SubTask for given id + * @param {*} id + * @returns + */ + async findSubTask(id){ + const subtaskDetails = await TaskService.getSubtaskDetails(id); + if (subtaskDetails) { + let subtask = {}; + subtask['key'] = 'subtask'+subtaskDetails.id; + subtask['label'] = <> SubTask ({subtaskDetails.id}) + {/* -- View page not available yet -- + <span className="find-obj-tree-view"><a href="" target='_blank'>View</a></span> */} + <span className="find-obj-tree-view"> <a href={subtaskDetails.url} target='_blank' + title=" View SubTask API"><i className="fa fa-link" /></a></span></>; + subtask['icon'] = 'fas fa-tasks'; + subtask['children'] = await this.findTask('blueprint', subtaskDetails.task_blueprint_id); + return [subtask]; + } + return ''; + } + + /** + * Find Task details for given id + * @param {*} taskType + * @param {*} id + * @returns + */ + async findTask(taskType, id){ + const taskDetails = await TaskService.getTask(taskType, id); + if (taskDetails) { + let task = {}; + task['key'] = 'task'+taskDetails.id; + task['label'] = <> Task ({taskDetails.id}) + <span className="find-obj-tree-view"> + <a href={`/task/view/${taskType}/${taskDetails.id}`} target='_blank' title=" View Task Details"> + <i className="fa fa-eye" /> + </a> + </span> + <span> <a href={taskDetails.url} target='_blank' title=" View Task API"><i className="fa fa-link" /></a></span></>; + task['icon'] = 'fa fa-tasks'; + if (taskType === 'blueprint') { + task['children'] = await this.findSchedulingUnit('blueprint', taskDetails.scheduling_unit_blueprint_id); + } else { + task['children'] = await this.findSchedulingUnit('draft', taskDetails.scheduling_unit_draft_id); + } + return [task]; + } + return ''; + } + + /** + * Find Scheduling Unit for given id + * @param {*} suType + * @param {*} id + * @returns + */ + async findSchedulingUnit(suType, id){ + let suDetails = null; + if (suType === 'blueprint') { + suDetails = await ScheduleService.getSchedulingUnitBlueprintById (id); + } else { + suDetails = await ScheduleService.getSchedulingUnitDraftById(id); + } + if (suDetails) { + let schedulingUnit = {}; + schedulingUnit['key'] = 'su'+suDetails.id; + schedulingUnit['label'] = <> Scheduling Unit ({suDetails.id}) + <span className="find-obj-tree-view"><a href={`/schedulingunit/view/${suType}/${suDetails.id}`} + target='_blank' title=" View Scheduling Unit Details"><i className="fa fa-eye" /></a> </span> + <span><a href={suDetails.url} target='_blank' title=" View Scheduling Unit API" > + <i className="fa fa-link" /></a></span></>; + schedulingUnit['icon'] = 'pi pi-fw pi-calendar'; + schedulingUnit['children'] = await this.findSchedulingSetBySUId(suDetails); + return [schedulingUnit]; + } + return ''; + } + + /** + * Find project for given SU id + * @param {*} suId + */ + async findSchedulingSetBySUId(suDetails) { + const suSetDetails = suDetails.scheduling_set_object; + if (suSetDetails) { + let suSet = {}; + suSet['key'] = 'suset'+suSetDetails.id; + suSet['label'] = <> Scheduling Set ({suSetDetails.id}) + {/* -- View page not available yet -- + <span className="find-obj-tree-view"><a href="" + target='_blank' title='View Project details'><i className="fa fa-eye" /></a></span> */} + <span className="find-obj-tree-view"> + <a href={suSetDetails.url} target='_blank' title='View Scheduling Set API'><i className="fa fa-link" /></a></span></>; + suSet['icon'] = 'fa fa-table'; + suSet['children'] = await this.findProject(suSetDetails.project_id); + return [suSet]; + } + return ''; + } + + /** + * Find project details for given id + * @param {*} id + * @returns + */ + async findProject(id){ + const projectDetails = await ProjectService.getProjectDetails(id); + if (projectDetails) { + let project = {}; + project['key'] = projectDetails.name; + project['label'] = <> Project ({projectDetails.name}) + <span className="find-obj-tree-view"><a href={`/project/view/${projectDetails.name}`} + target='_blank' title='View Project details'><i className="fa fa-eye" /></a></span> + <span><a href={projectDetails.url} target='_blank' title='View Project API'><i className="fa fa-link" /></a></span></>; + project['icon'] = 'fab fa-fw fa-wpexplorer'; + return [project]; + } + return ''; + } + + + expandNode(node, expandedKeys) { + if (node.children && node.children.length) { + expandedKeys[node.key] = true; + + for (let child of node.children) { + this.expandNode(child, expandedKeys); + } + } + } + + expandAll() { + let expandedKeys = {}; + for (let node of this.state.objNodes) { + this.expandNode(node, expandedKeys); + } + this.setState({expandedKeys: expandedKeys }); + } + + render(){ + return( + <> + <PageHeader location={this.props.location} title={'Search Result'} + actions={[]} + /> + { this.state.isLoading ? <AppLoader /> : + <> + {this.state.objNodes.length > 0 && + <> + <Tree value={this.state.objNodes} selectionMode="multiple" expandedKeys={this.state.expandedKeys} + style={{width: 'auto'}} onToggle={e => this.setState({expandedKeys: e.value})} /> + </> + } + {this.state.objNodes.length === 0 && + <> No Object found ! </> + } + </> + } + </> + ) + } +} diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Search/index.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Search/index.js new file mode 100644 index 0000000000000000000000000000000000000000..fcfd0526ca2aec256d95352823d62bab13b9e8a5 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Search/index.js @@ -0,0 +1,3 @@ +import {FindObjectResult} from './find.object.result'; + +export {FindObjectResult} ; diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/index.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/index.js index 91955b294875ad02e7bba6314ccadeac920920f1..8af02d3feb05672ec8893e7145f0931b4bec2e85 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/index.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/index.js @@ -1,5 +1,6 @@ import {TaskEdit} from './edit'; import {TaskView} from './view'; import {DataProduct} from './dataproduct'; +import { TaskList } from './list'; -export {TaskEdit, TaskView, DataProduct} ; +export {TaskEdit, TaskView, DataProduct,TaskList} ; diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/list.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/list.js new file mode 100644 index 0000000000000000000000000000000000000000..5bdef19b88263dc4fab8d695fc6ae02f9d2f7f49 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/list.js @@ -0,0 +1,384 @@ +import React, {Component} from 'react'; +import {Redirect} from 'react-router-dom' +import moment from 'moment'; +import { Dialog } from 'primereact/dialog'; +import { DataTable } from 'primereact/datatable'; +import { Column } from 'primereact/column'; +import _ from 'lodash'; +import TaskService from '../../services/task.service'; +import AppLoader from '../../layout/components/AppLoader'; +import PageHeader from '../../layout/components/PageHeader'; +import ViewTable from '../../components/ViewTable'; +import UIConstants from '../../utils/ui.constants'; +import TaskStatusLogs from './state_logs'; +import { appGrowl } from '../../layout/components/AppGrowl'; +import { CustomDialog } from '../../layout/components/CustomDialog'; +import ScheduleService from '../../services/schedule.service'; +import UnitConverter from '../../utils/unit.converter'; + +export class TaskList extends Component { + constructor(props) { + super(props); + this.state = { + isLoading: true, + tasks: [], + paths: [{ + "View": "/task", + }], + columnOrders: [ + "Status Logs", + "Status", + "Type", + "Scheduling Unit ID", + "Scheduling Unit Name", + "ID", + "Control ID", + "Name", + "Description", + "Start Time", + "End Time", + "Duration (HH:mm:ss)", + "Relative Start Time (HH:mm:ss)", + "Relative End Time (HH:mm:ss)", + "#Dataproducts", + "size", + "dataSizeOnDisk", + "subtaskContent", + "tags", + "blueprint_draft", + "url", + "Cancelled", + "Created at", + "Updated at" + ], + dialog: {}, + defaultcolumns: [ { + status_logs: "Status Logs", + status:{ + name:"Status", + filter: "select" + }, + tasktype:{ + name:"Type", + filter:"select" + }, + schedulingUnitId: "Scheduling Unit ID", + schedulingUnitName: "Scheduling Unit Name", + id: "ID", + subTaskID: 'Control ID', + name:"Name", + description:"Description", + start_time:{ + name:"Start Time", + filter: "date", + format:UIConstants.CALENDAR_DATETIME_FORMAT + }, + stop_time:{ + name:"End Time", + filter: "date", + format:UIConstants.CALENDAR_DATETIME_FORMAT + }, + duration:"Duration (HH:mm:ss)", + relative_start_time:"Relative Start Time (HH:mm:ss)", + relative_stop_time:"Relative End Time (HH:mm:ss)", + noOfOutputProducts: "#Dataproducts", + do_cancel:{ + name: "Cancelled", + filter: "switch" + }, + }], + optionalcolumns: [{ + size: "Data size", + dataSizeOnDisk: "Data size on Disk", + subtaskContent: "Subtask Content", + tags:"Tags", + blueprint_draft:"BluePrint / Task Draft link", + url:"API URL", + created_at:{ + name: "Created at", + filter: "date", + format:UIConstants.CALENDAR_DATETIME_FORMAT + }, + updated_at:{ + name: "Updated at", + filter: "date", + format:UIConstants.CALENDAR_DATETIME_FORMAT + }, + actionpath:"actionpath" + }], + columnclassname: [{ + "Status Logs": "filter-input-0", + "Type":"filter-input-75", + "Scheduling Unit ID": "filter-input-50", + "Scheduling Unit Name": "filter-input-100", + "ID":"filter-input-50", + "Control ID":"filter-input-75", + "Cancelled":"filter-input-50", + "Duration (HH:mm:ss)":"filter-input-75", + "Template ID":"filter-input-50", + // "BluePrint / Task Draft link": "filter-input-100", + "Relative Start Time (HH:mm:ss)": "filter-input-75", + "Relative End Time (HH:mm:ss)": "filter-input-75", + "Status":"filter-input-100", + "#Dataproducts":"filter-input-75", + "Data size":"filter-input-50", + "Data size on Disk":"filter-input-50", + "Subtask Content":"filter-input-75", + "BluePrint / Task Draft link":"filter-input-50", + }] + }; + this.selectedRows = []; + this.subtaskTemplates = []; + this.confirmDeleteTasks = this.confirmDeleteTasks.bind(this); + this.onRowSelection = this.onRowSelection.bind(this); + this.deleteTasks = this.deleteTasks.bind(this); + this.closeDialog = this.closeDialog.bind(this); + this.getTaskDialogContent = this.getTaskDialogContent.bind(this); + } + + subtaskComponent = (task)=> { + return ( + <button className="p-link" onClick={(e) => {this.setState({showStatusLogs: true, task: task})}}> + <i className="fa fa-history"></i> + </button> + ); + }; + + + /** + * Formatting the task_blueprints in blueprint view to pass to the ViewTable component + * @param {Object} schedulingUnit - scheduling_unit_blueprint object from extended API call loaded with tasks(blueprint) along with their template and subtasks + */ + getFormattedTaskBlueprints(schedulingUnit) { + let taskBlueprintsList = []; + for(const taskBlueprint of schedulingUnit.task_blueprints) { + taskBlueprint['status_logs'] = this.subtaskComponent(taskBlueprint); + taskBlueprint['tasktype'] = 'Blueprint'; + taskBlueprint['actionpath'] = '/task/view/blueprint/'+taskBlueprint['id']; + taskBlueprint['blueprint_draft'] = taskBlueprint['draft']; + taskBlueprint['relative_start_time'] = 0; + taskBlueprint['relative_stop_time'] = 0; + taskBlueprint.duration = moment.utc((taskBlueprint.duration || 0)*1000).format(UIConstants.CALENDAR_TIME_FORMAT); + taskBlueprint.template = taskBlueprint.specifications_template; + taskBlueprint.schedulingUnitName = schedulingUnit.name; + for (const subtask of taskBlueprint.subtasks) { + subtask.subTaskTemplate = _.find(this.subtaskTemplates, ['id', subtask.specifications_template_id]); + } + taskBlueprint.schedulingUnitId = taskBlueprint.scheduling_unit_blueprint_id; + taskBlueprint.subTasks = taskBlueprint.subtasks; + taskBlueprintsList.push(taskBlueprint); + } + return taskBlueprintsList; + } + + /** + * Formatting the task_drafts and task_blueprints in draft view to pass to the ViewTable component + * @param {Object} schedulingUnit - scheduling_unit_draft object from extended API call loaded with tasks(draft & blueprint) along with their template and subtasks + */ + getFormattedTaskDrafts(schedulingUnit) { + let scheduletasklist=[]; + // Common keys for Task and Blueprint + let commonkeys = ['id','created_at','description','name','tags','updated_at','url','do_cancel','relative_start_time','relative_stop_time','start_time','stop_time','duration','status']; + for(const task of schedulingUnit.task_drafts){ + let scheduletask = {}; + scheduletask['tasktype'] = 'Draft'; + scheduletask['actionpath'] = '/task/view/draft/'+task['id']; + scheduletask['blueprint_draft'] = _.map(task['task_blueprints'], 'url'); + scheduletask['status'] = task['status']; + + //fetch task draft details + for(const key of commonkeys){ + scheduletask[key] = task[key]; + } + scheduletask['specifications_doc'] = task['specifications_doc']; + scheduletask.duration = moment.utc((scheduletask.duration || 0)*1000).format(UIConstants.CALENDAR_TIME_FORMAT); + scheduletask.relative_start_time = moment.utc(scheduletask.relative_start_time*1000).format(UIConstants.CALENDAR_TIME_FORMAT); + scheduletask.relative_stop_time = moment.utc(scheduletask.relative_stop_time*1000).format(UIConstants.CALENDAR_TIME_FORMAT); + scheduletask.template = task.specifications_template; + scheduletask.type_value = task.specifications_template.type_value; + scheduletask.produced_by = task.produced_by; + scheduletask.produced_by_ids = task.produced_by_ids; + scheduletask.schedulingUnitId = task.scheduling_unit_draft_id; + scheduletask.schedulingUnitName = schedulingUnit.name; + //Add Task Draft details to array + scheduletasklist.push(scheduletask); + } + return scheduletasklist; + } + + async formatDataProduct(tasks) { + await Promise.all(tasks.map(async task => { + task.status_logs = task.tasktype === "Blueprint"?this.subtaskComponent(task):""; + //Displaying SubTask ID of the 'control' Task + const subTaskIds = task.subTasks?task.subTasks.filter(sTask => sTask.subTaskTemplate.name.indexOf('control') >= 0):[]; + const promise = []; + subTaskIds.map(subTask => promise.push(ScheduleService.getSubtaskOutputDataproduct(subTask.id))); + const dataProducts = promise.length > 0? await Promise.all(promise):[]; + task.dataProducts = []; + task.size = 0; + task.dataSizeOnDisk = 0; + task.noOfOutputProducts = 0; + task.canSelect = task.tasktype.toLowerCase() === 'blueprint' ? true:(task.tasktype.toLowerCase() === 'draft' && task.blueprint_draft.length === 0)?true:false; + if (dataProducts.length && dataProducts[0].length) { + task.dataProducts = dataProducts[0]; + task.noOfOutputProducts = dataProducts[0].length; + task.size = _.sumBy(dataProducts[0], 'size'); + task.dataSizeOnDisk = _.sumBy(dataProducts[0], function(product) { return product.deletedSince?0:product.size}); + task.size = UnitConverter.getUIResourceUnit('bytes', (task.size)); + task.dataSizeOnDisk = UnitConverter.getUIResourceUnit('bytes', (task.dataSizeOnDisk)); + } + task.subTaskID = subTaskIds.length ? subTaskIds[0].id : ''; + return task; + })); + return tasks; + } + + + async componentDidMount() { + this.subtaskTemplates = await TaskService.getSubtaskTemplates() + const promises = [ + ScheduleService.getSchedulingUnitsExtended('draft'), + ScheduleService.getSchedulingUnitsExtended('blueprint') + ]; + Promise.all(promises).then(async (responses) => { + let allTasks = []; + for (const schedulingUnit of responses[0]) { + let tasks = schedulingUnit.task_drafts?(await this.getFormattedTaskDrafts(schedulingUnit)):this.getFormattedTaskBlueprints(schedulingUnit); + let ingestGroup = tasks.map(task => ({name: task.name, canIngest: task.canIngest, type_value: task.type_value, id: task.id })); + ingestGroup = _.groupBy(_.filter(ingestGroup, 'type_value'), 'type_value'); + tasks = await this.formatDataProduct(tasks); + allTasks = [...allTasks, ...tasks]; + } + for (const schedulingUnit of responses[1]) { + let tasks = schedulingUnit.task_drafts?(await this.getFormattedTaskDrafts(schedulingUnit)):this.getFormattedTaskBlueprints(schedulingUnit); + let ingestGroup = tasks.map(task => ({name: task.name, canIngest: task.canIngest, type_value: task.type_value, id: task.id })); + ingestGroup = _.groupBy(_.filter(ingestGroup, 'type_value'), 'type_value'); + tasks = await this.formatDataProduct(tasks); + allTasks = [...allTasks, ...tasks]; + } + this.setState({ tasks: allTasks, isLoading: false }); + }); + } + + /** + * Prepare Task(s) details to show on confirmation dialog + */ + getTaskDialogContent() { + let selectedTasks = []; + for(const obj of this.selectedRows) { + selectedTasks.push({id:obj.id, suId: obj.schedulingUnitId, suName: obj.schedulingUnitName, + taskId: obj.id, controlId: obj.subTaskID, taskName: obj.name, status: obj.status}); + } + return <> + <DataTable value={selectedTasks} resizableColumns columnResizeMode="expand" className="card" style={{paddingLeft: '0em'}}> + <Column field="suId" header="Scheduling Unit Id"></Column> + <Column field="taskId" header="Task Id"></Column> + <Column field="taskName" header="Task Name"></Column> + <Column field="status" header="Status"></Column> + </DataTable> + </> + } + + confirmDeleteTasks() { + if(this.selectedRows.length === 0) { + appGrowl.show({severity: 'info', summary: 'Select Row', detail: 'Select Task to delete.'}); + } else { + let dialog = {}; + dialog.type = "confirmation"; + dialog.header= "Confirm to Delete Task(s)"; + dialog.detail = "Do you want to delete the selected Task(s)?"; + dialog.content = this.getTaskDialogContent; + dialog.actions = [{id: 'yes', title: 'Yes', callback: this.deleteTasks}, + {id: 'no', title: 'No', callback: this.closeDialog}]; + dialog.onSubmit = this.deleteTasks; + dialog.width = '55vw'; + dialog.showIcon = false; + this.setState({dialog: dialog, dialogVisible: true}); + } + } + + /** + * Delete Task(s) + */ + async deleteTasks() { + let hasError = false; + for(const task of this.selectedRows) { + if(!await TaskService.deleteTask(task.tasktype, task.id)) { + hasError = true; + } + } + if(hasError){ + appGrowl.show({severity: 'error', summary: 'error', detail: 'Error while deleting Task(s)'}); + this.setState({dialogVisible: false}); + } else { + this.selectedRows = []; + this.setState({dialogVisible: false}); + this.componentDidMount(); + appGrowl.show({severity: 'success', summary: 'Success', detail: 'Task(s) deleted successfully'}); + } + } + + /** + * Callback function to close the dialog prompted. + */ + closeDialog() { + this.setState({dialogVisible: false}); + } + + onRowSelection(selectedRows) { + this.selectedRows = selectedRows; + } + + + render() { + if (this.state.redirect) { + return <Redirect to={ {pathname: this.state.redirect} }></Redirect> + } + + return ( + <React.Fragment> + <PageHeader location={this.props.location} title={'Task - List'} /> + {this.state.isLoading? <AppLoader /> : + <> + <div className="delete-option"> + <div > + <span className="p-float-label"> + <a href="#" onClick={this.confirmDeleteTasks} title="Delete selected Task(s)"> + <i class="fa fa-trash" aria-hidden="true" ></i> + </a> + </span> + </div> + </div> + <ViewTable + data={this.state.tasks} + defaultcolumns={this.state.defaultcolumns} + optionalcolumns={this.state.optionalcolumns} + columnclassname={this.state.columnclassname} + columnOrders={this.state.columnOrders} + defaultSortColumn={this.state.defaultSortColumn} + showaction="true" + keyaccessor="id" + paths={this.state.paths} + unittest={this.state.unittest} + tablename="scheduleunit_task_list" + allowRowSelection={true} + onRowSelection = {this.onRowSelection} + /> + </> + } + {this.state.showStatusLogs && + <Dialog header={`Status change logs - ${this.state.task?this.state.task.name:""}`} + visible={this.state.showStatusLogs} maximizable maximized={false} position="left" style={{ width: '50vw' }} + onHide={() => {this.setState({showStatusLogs: false})}}> + <TaskStatusLogs taskId={this.state.task.id}></TaskStatusLogs> + </Dialog> + } + <CustomDialog type="confirmation" visible={this.state.dialogVisible} + header={this.state.dialog.header} message={this.state.dialog.detail} actions={this.state.dialog.actions} + content={this.state.dialog.content} width={this.state.dialog.width} showIcon={this.state.dialog.showIcon} + onClose={this.closeDialog} onCancel={this.closeDialog} onSubmit={this.state.dialog.onSubmit}/> + </React.Fragment> + ); + } +} + \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/view.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/view.js index f89c15afe62c4ea33919c10b6a7e1de07c01a4e3..8a5fe8ea36ca313089e04a5154f7d7899a37d83b 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/view.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/view.js @@ -71,12 +71,9 @@ export class TaskView extends Component { } componentDidMount() { - // const taskId = this.props.location.state?this.props.location.state.id:this.state.taskId; - // let taskType = this.props.location.state?this.props.location.state.type:this.state.taskType; - // taskType = taskType?taskType:'draft'; - let {taskId, taskType} = this.state; - taskId = taskId?taskId:this.props.location.state.id; - taskType = taskType?taskType:this.props.location.state.type; + const taskId = this.props.location.state?this.props.location.state.id:this.state.taskId; + let taskType = this.props.location.state && this.props.location.state.type?this.props.location.state.type:this.state.taskType; + taskType = taskType?taskType:'draft'; if (taskId && taskType) { this.getTaskDetails(taskId, taskType); diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/index.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/index.js index 2076201f6695b183946734cc9e1aa6a2b26b5ea0..658c2a00acf6f252714630e1c88ad3eec7f8b3d7 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/index.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/index.js @@ -2,4 +2,5 @@ import {TimelineView} from './view'; import {WeekTimelineView} from './week.view'; import { ReservationList} from './reservation.list'; import { ReservationCreate } from './reservation.create'; -export {TimelineView, WeekTimelineView, ReservationCreate, ReservationList} ; +import { ReservationSummary } from './reservation.summary'; +export {TimelineView, WeekTimelineView, ReservationCreate, ReservationList, ReservationSummary} ; diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/reservation.create.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/reservation.create.js index e1b884053169d5a05dd9ab001e45af3c7ae0804a..ac7fa0216a2074478fa88d6af869a0233affefa4 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/reservation.create.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/reservation.create.js @@ -7,7 +7,6 @@ import AppLoader from '../../layout/components/AppLoader'; import PageHeader from '../../layout/components/PageHeader'; import UIConstants from '../../utils/ui.constants'; import Flatpickr from "react-flatpickr"; -import { Calendar } from 'primereact/calendar'; import { InputMask } from 'primereact/inputmask'; import { Dropdown } from 'primereact/dropdown'; import {InputText } from 'primereact/inputtext'; @@ -43,14 +42,13 @@ export class ReservationCreate extends Component { name: '', description: '', start_time: null, - duration: '', + stop_time: null, project: (props.match?props.match.params.project:null) || null, }, errors: {}, // Validation Errors validFields: {}, // For Validation validForm: false, // To enable Save Button - validEditor: false, - durationError: false, + validEditor: false }; this.projects = []; // All projects to load project dropdown this.reservationTemplates = []; @@ -143,13 +141,6 @@ export class ReservationCreate extends Component { * @param {any} value */ setParams(key, value, type) { - if(key === 'duration' && !this.validateDuration( value)) { - this.setState({ - durationError: true, - isDirty: true - }) - return; - } let reservation = this.state.reservation; switch(type) { case 'NUMBER': { @@ -161,23 +152,9 @@ export class ReservationCreate extends Component { break; } } - this.setState({reservation: reservation, validForm: this.validateForm(key), durationError: false, isDirty: true}); + this.setState({reservation: reservation, validForm: this.validateForm(key), isDirty: true}); } - /** - * Validate Duration, it allows max 99:59:59 - * @param {*} duration - */ - validateDuration(duration) { - const splitOutput = duration.split(':'); - if (splitOutput.length < 3) { - return false; - } else if (parseInt(splitOutput[1])>59 || parseInt(splitOutput[2])>59) { - return false; - } - return true; - } - /** * Validation function to validate the form or field based on the form rules. * If no argument passed for fieldName, validates all fields in the form. @@ -219,10 +196,37 @@ export class ReservationCreate extends Component { this.setState({errors: errors, validFields: validFields}); if (Object.keys(validFields).length === Object.keys(this.formRules).length) { validForm = true; + delete errors['start_time']; + delete errors['stop_time']; + } + if (!this.validateDates(this.state.reservation.start_time, this.state.reservation.stop_time)) { + validForm = false; + if (!fieldName || fieldName === 'start_time') { + errors['start_time'] = "From Date cannot be same or after To Date"; + delete errors['stop_time']; + } + if (!fieldName || fieldName === 'stop_time') { + errors['stop_time'] = "To Date cannot be same or before From Date"; + delete errors['start_time']; + } + this.setState({errors: errors}); } return validForm; } + /** + * Function to validate if stop_time is always later than start_time if exists. + * @param {Date} fromDate + * @param {Date} toDate + * @returns boolean + */ + validateDates(fromDate, toDate) { + if (fromDate && toDate && moment(toDate).isSameOrBefore(moment(fromDate))) { + return false; + } + return true; + } + setEditorOutput(jsonOutput, errors) { this.paramsOutput = jsonOutput; this.validEditor = errors.length === 0; @@ -242,7 +246,7 @@ export class ReservationCreate extends Component { let reservation = this.state.reservation; let project = this.projects.find(project => project.name === reservation.project); reservation['start_time'] = moment(reservation['start_time']).format(UIConstants.CALENDAR_DATETIME_FORMAT); - reservation['duration'] = ( reservation['duration'] === ''? null: UnitService.getHHmmssToSecs(reservation['duration'])); + reservation['stop_time'] = reservation['stop_time']?moment(reservation['stop_time']).format(UIConstants.CALENDAR_DATETIME_FORMAT):reservation['stop_time']; reservation['project']= project ? project.url: null; reservation['specifications_template']= this.reservationTemplates[0].url; reservation['specifications_doc']= this.paramsOutput; @@ -263,7 +267,7 @@ export class ReservationCreate extends Component { name: '', description: '', start_time: '', - duration: '', + stop_time: '', project: '', } this.setState({ @@ -360,13 +364,14 @@ export class ReservationCreate extends Component { </div> </div> <div className="p-field p-grid"> - <label htmlFor="reservationName" className="col-lg-2 col-md-2 col-sm-12">From Date <span style={{color:'red'}}>*</span></label> + <label className="col-lg-2 col-md-2 col-sm-12">From Date <span style={{color:'red'}}>*</span></label> <div className="col-lg-3 col-md-3 col-sm-12"> <Flatpickr data-enable-time data-input options={{ "inlineHideInput": true, "wrap": true, "enableSeconds": true, "time_24hr": true, + "minuteIncrement": 1, "allowInput": true, "defaultDate": this.state.systemTime.format(UIConstants.CALENDAR_DEFAULTDATE_FORMAT), "defaultHour": this.state.systemTime.hours(), @@ -387,21 +392,33 @@ export class ReservationCreate extends Component { </div> <div className="col-lg-1 col-md-1 col-sm-12"></div> - <label htmlFor="duration" className="col-lg-2 col-md-2 col-sm-12">Duration </label> - <div className="col-lg-3 col-md-3 col-sm-12" data-testid="duration" > - <InputMask - value={this.state.reservation.duration} - mask="99:99:99" - placeholder="HH:mm:ss" - tooltip="Duration of this reservation. If it is empty, then this reservation is indefinite." - tooltipOptions={this.tooltipOptions} - onChange= {e => this.setParams('duration',e.value)} - ref={input =>{this.input = input}} - /> - <label className="error"> - {this.state.durationError ? 'Invalid duration, Maximum:99:59:59.' : ""} + <label className="col-lg-2 col-md-2 col-sm-12">To Date</label> + <div className="col-lg-3 col-md-3 col-sm-12"> + <Flatpickr data-enable-time data-input options={{ + "inlineHideInput": true, + "wrap": true, + "enableSeconds": true, + "time_24hr": true, + "minuteIncrement": 1, + "allowInput": true, + "minDate": this.state.reservation.start_time?this.state.reservation.start_time.toDate:'', + "defaultDate": this.state.systemTime.format(UIConstants.CALENDAR_DEFAULTDATE_FORMAT), + "defaultHour": this.state.systemTime.hours(), + "defaultMinute": this.state.systemTime.minutes() + }} + title="End of this reservation. If empty, then this reservation is indefinite." + value={this.state.reservation.stop_time} + onChange= {value => {this.setParams('stop_time', value[0]?value[0]:this.state.reservation.stop_time); + this.setReservationParams('stop_time', value[0]?value[0]:this.state.reservation.stop_time)}} > + <input type="text" data-input className={`p-inputtext p-component ${this.state.errors.stop_time && this.state.touched.stop_time?'input-error':''}`} /> + <i className="fa fa-calendar" data-toggle style={{position: "absolute", marginLeft: '-25px', marginTop:'5px', cursor: 'pointer'}} ></i> + <i className="fa fa-times" style={{position: "absolute", marginLeft: '-50px', marginTop:'5px', cursor: 'pointer'}} + onClick={e => {this.setParams('stop_time', ''); this.setReservationParams('stop_time', '')}}></i> + </Flatpickr> + <label className={this.state.errors.stop_time && this.state.touched.stop_time?"error":"info"}> + {this.state.errors.stop_time && this.state.touched.stop_time ? this.state.errors.stop_time : ""} </label> - </div> + </div> </div> <div className="p-field p-grid"> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/reservation.list.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/reservation.list.js index dd776d470c2d3a496d7a3f355cad38d47f75dd28..98ab06258512115f9abd678c19759de163f9c106 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/reservation.list.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/reservation.list.js @@ -30,7 +30,7 @@ export class ReservationList extends Component{ filter: "fromdatetime", format:UIConstants.CALENDAR_DATETIME_FORMAT }, - end_time: { + stop_time: { name: "End Time", filter: "todatetime", format:UIConstants.CALENDAR_DATETIME_FORMAT @@ -118,19 +118,17 @@ export class ReservationList extends Component{ reservation = this.mergeResourceWithReservation( reservation, response.specifications_doc.effects ); reservation = this.mergeResourceWithReservation( reservation, response.specifications_doc.schedulability ); if (response.specifications_doc.resources.stations ) { - reservation['stations'] = response.specifications_doc.resources.stations.join(); + reservation['stations'] = response.specifications_doc.resources.stations.join(', '); } else { reservation['stations'] = ''; } if(reservation.duration === null || reservation.duration === ''){ reservation.duration = 'Unknown'; - reservation['end_time']= 'Unknown'; + reservation['stop_time']= 'Unknown'; } else { let duration = reservation.duration; reservation.duration = UnitService.getSecsToHHmmss(reservation.duration); - let endDate = moment(reservation.start_time); - endDate = moment(endDate).add(duration, 's'); - reservation['end_time']= moment(endDate).format(UIConstants.CALENDAR_DATETIME_FORMAT); + reservation['stop_time']= moment(reservation['stop_time']).format(UIConstants.CALENDAR_DATETIME_FORMAT); } reservation['start_time']= moment(reservation['start_time']).format(UIConstants.CALENDAR_DATETIME_FORMAT); this.reservations.push(reservation); @@ -177,9 +175,9 @@ export class ReservationList extends Component{ let cycle_End_time = moment.utc(moment(cycle['stop']).format("YYYY-MM-DD")); this.state.reservationsList.forEach( reservation => { let res_Start_time = moment.utc(moment(reservation['start_time']).format("YYYY-MM-DD")); - let res_End_time = moment.utc(moment(reservation['end_time']).format("YYYY-MM-DD")); + let res_End_time = moment.utc(moment(reservation['stop_time']).format("YYYY-MM-DD")); if (cycle_Start_time.isSameOrBefore(res_Start_time) && cycle_End_time.isSameOrAfter(res_Start_time)) { - if ( reservation['end_time'] === 'Unknown'|| cycle_End_time.isSameOrAfter(res_End_time)) { + if ( reservation['stop_time'] === 'Unknown'|| cycle_End_time.isSameOrAfter(res_End_time)) { const tmpList = _.filter(reservationList, function(o) { return o.id === reservation.id }); if( tmpList.length === 0) { reservationList.push(reservation); @@ -216,7 +214,7 @@ export class ReservationList extends Component{ await this.state.reservationsList.forEach( reservation => { let res_Start_time = moment.utc(moment(reservation['start_time'])).valueOf(); let res_End_time = 'Unknown'; - if(reservation['end_time'] === 'Unknown') { + if(reservation['stop_time'] === 'Unknown') { if(res_Start_time <= fEndTime){ const tmpList = _.filter(reservationList, function(o) { return o.id === reservation.id }); if( tmpList.length === 0) { @@ -225,7 +223,7 @@ export class ReservationList extends Component{ } } else { - res_End_time = moment.utc(moment(reservation['end_time'])).valueOf(); + res_End_time = moment.utc(moment(reservation['stop_time'])).valueOf(); if(res_Start_time <= fStartTime && res_End_time >= fStartTime) { const tmpList = _.filter(reservationList, function(o) { return o.id === reservation.id }); if( tmpList.length === 0) { diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/reservation.summary.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/reservation.summary.js new file mode 100644 index 0000000000000000000000000000000000000000..50a80c71b0af29f2090e8f00ab6cac1c057b54f3 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/reservation.summary.js @@ -0,0 +1,139 @@ +import React, {Component} from 'react'; +import { Link } from 'react-router-dom/cjs/react-router-dom.min'; +import moment from 'moment'; +import _ from 'lodash'; +import { JsonToTable } from "react-json-to-table"; +import UIConstants from '../../utils/ui.constants'; +import UnitConverter from '../../utils/unit.converter'; + +/** + * Component to view summary of the Reservation + */ +export class ReservationSummary extends Component { + + constructor(props) { + super(props); + this.closeSUDets = this.closeSUDets.bind(this); + } + + componentDidMount() {} + + /** + * Function to close the summary panel and call parent callback function to close. + */ + closeSUDets() { + if(this.props.closeCallback) { + this.props.closeCallback(); + } + } + + /** + * Function to order or format all specifications to readable values + * @param {Object} specifications + */ + getOrderedSpecifications(specifications) { + for (const specKey of _.keys(specifications)) { + let specification = this.getFormattedSpecification(specifications[specKey]); + specifications[specKey] = specification; + } + return specifications; + } + + /** + * Function to format date, boolean, array object to readable values + * @param {Object} specification + */ + getFormattedSpecification(specification) { + if (specification !== null) { + const objectType = typeof specification; + switch(objectType) { + case "string": { + try { + const dateValue = moment.utc(specification); + if (dateValue.isValid()) { + specification = dateValue.format(UIConstants.CALENDAR_DATETIME_FORMAT); + } + } catch (error) {} + break; + } + case "boolean": { + specification = specification?'True':'False'; + break; + } + case "object": { + if (Array.isArray(specification)) { + let newArray = [], isStringArray = false; + for (let arrayObj of specification) { + arrayObj = this.getFormattedSpecification(arrayObj); + if (arrayObj) { + if ((typeof arrayObj) === "string") { + isStringArray = true; + } + newArray.push(arrayObj); + } + } + specification = newArray.length > 0?(isStringArray?newArray.join(", "):newArray):null; + } else { + let newObject = {}; + let keys = _.keys(specification); + for (const objectKey of _.keys(specification)) { + let object = this.getFormattedSpecification(specification[objectKey]); + if (object) { + newObject[objectKey.replace(/_/g, ' ')] = object; + } + } + specification = (!_.isEmpty(newObject))? newObject:null; + } + break; + } + default: {} + } + } + return specification; + } + + render() { + const reservation = this.props.reservation; + let specifications = reservation?_.cloneDeep(reservation.specifications_doc):null; + if (specifications) { + // Remove $schema variable + delete specifications['$schema']; + } + return ( + <React.Fragment> + { reservation && + <div className="p-grid timeline-details-pane" style={{marginTop: '10px'}}> + <h6 className="col-lg-10 col-sm-10">Reservation Details</h6> + {/* TODO: Enable the link once Reservation view page is created */} + {/* <Link to={`/su/timeline/reservation/view/${reservation.id}`} title="View Full Details" ><i className="fa fa-eye"></i></Link> */} + <i className="fa fa-eye" style={{color: 'grey'}}></i> + <Link to={this.props.location?this.props.location.pathname:"/su/timelineview"} onClick={this.closeSUDets} title="Close Details"><i className="fa fa-times"></i></Link> + <div className="col-4"><label>Name:</label></div> + <div className="col-8">{reservation.name}</div> + <div className="col-4"><label>Description:</label></div> + <div className="col-8">{reservation.description}</div> + <div className="col-4"><label>Project:</label></div> + <div className="col-8">{reservation.project}</div> + <div className="col-4"><label>Start Time:</label></div> + <div className="col-8">{moment.utc(reservation.start_time).format(UIConstants.CALENDAR_DATETIME_FORMAT)}</div> + <div className="col-4"><label>Stop Time:</label></div> + <div className="col-8">{moment.utc(reservation.stop_time).format(UIConstants.CALENDAR_DATETIME_FORMAT)}</div> + <div className="col-4"><label>Duration (HH:mm:ss):</label></div> + <div className="col-8">{UnitConverter.getSecsToHHmmss(reservation.duration)}</div> + {/* Reservation parameters Display in table format */} + {reservation.specifications_doc && + <> + <div className="col-12 constraints-summary"> + <label>Parameters:</label> + <JsonToTable json={this.getOrderedSpecifications(specifications)} /> + </div> + </> + } + </div> + } + </React.Fragment> + ); + } +} + +export default ReservationSummary; \ No newline at end of file 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 fd0d2b161d76feb6acd61c2ac80abf1d17de8ffe..7c8436aeacf4e17fa5b73fef1a7c73b81b7eb308 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/view.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/view.js @@ -22,6 +22,7 @@ import TaskService from '../../services/task.service'; import UnitConverter from '../../utils/unit.converter'; import Validator from '../../utils/validator'; import SchedulingUnitSummary from '../Scheduling/summary'; +import ReservationSummary from './reservation.summary'; import { Dropdown } from 'primereact/dropdown'; import { OverlayPanel } from 'primereact/overlaypanel'; import { RadioButton } from 'primereact/radiobutton'; @@ -88,6 +89,7 @@ export class TimelineView extends Component { this.onItemMouseOver = this.onItemMouseOver.bind(this); this.onItemMouseOut = this.onItemMouseOut.bind(this); this.showSUSummary = this.showSUSummary.bind(this); + this.showReservationSummary = this.showReservationSummary.bind(this); this.showTaskSummary = this.showTaskSummary.bind(this); this.closeSUDets = this.closeSUDets.bind(this); this.dateRangeCallback = this.dateRangeCallback.bind(this); @@ -295,6 +297,8 @@ export class TimelineView extends Component { onItemClick(item) { if (item.type === "SCHEDULE") { this.showSUSummary(item); + } else if (item.type === "RESERVATION") { + this.showReservationSummary(item); } else { this.showTaskSummary(item); } @@ -349,6 +353,14 @@ export class TimelineView extends Component { } } + /** + * To load and show Reservation summary + * @param {Object} item + */ + showReservationSummary(item) { + this.setState({selectedItem: item, isReservDetsVisible: true, isSUDetsVisible: false}); + } + /** * To load task summary and show * @param {Object} item - Timeline task item object @@ -361,7 +373,7 @@ export class TimelineView extends Component { * Closes the SU details section */ closeSUDets() { - this.setState({isSUDetsVisible: false, isTaskDetsVisible: false, canExtendSUList: true, canShrinkSUList: false}); + this.setState({isSUDetsVisible: false, isReservDetsVisible: false, isTaskDetsVisible: false, canExtendSUList: true, canShrinkSUList: false}); } /** @@ -378,23 +390,34 @@ export class TimelineView extends Component { * @param {Object} item */ onItemMouseOver(evt, item) { - const itemSU = _.find(this.state.suBlueprints, {id: (item.suId?item.suId:item.id)}); - const itemStations = this.getSUStations(itemSU); - const itemStationGroups = this.groupSUStations(itemStations); - item.stations = {groups: "", counts: ""}; - item.suName = itemSU.name; - for (const stationgroup of _.keys(itemStationGroups)) { - let groups = item.stations.groups; - let counts = item.stations.counts; - if (groups) { - groups = groups.concat("/"); - counts = counts.concat("/"); + if (item.type === "SCHEDULE" || item.type === "TASK") { + const itemSU = _.find(this.state.suBlueprints, {id: (item.suId?item.suId:item.id)}); + const itemStations = this.getSUStations(itemSU); + const itemStationGroups = this.groupSUStations(itemStations); + item.stations = {groups: "", counts: ""}; + item.suName = itemSU.name; + for (const stationgroup of _.keys(itemStationGroups)) { + let groups = item.stations.groups; + let counts = item.stations.counts; + if (groups) { + groups = groups.concat("/"); + counts = counts.concat("/"); + } + // Get station group 1st character and append 'S' to get CS,RS,IS + groups = groups.concat(stationgroup.substring(0,1).concat('S')); + counts = counts.concat(itemStationGroups[stationgroup].length); + item.stations.groups = groups; + item.stations.counts = counts; } - // Get station group 1st character and append 'S' to get CS,RS,IS - groups = groups.concat(stationgroup.substring(0,1).concat('S')); - counts = counts.concat(itemStationGroups[stationgroup].length); - item.stations.groups = groups; - item.stations.counts = counts; + } else { + const reservation = _.find(this.reservations, {'id': parseInt(item.id.split("-")[1])}); + const reservStations = reservation.specifications_doc.resources.stations; + const reservStationGroups = this.groupSUStations(reservStations); + item.name = reservation.name; + item.contact = reservation.specifications_doc.activity.contact + item.activity_type = reservation.specifications_doc.activity.type; + item.stations = reservStations; + item.planned = reservation.specifications_doc.activity.planned; } this.popOver.toggle(evt); this.setState({mouseOverItem: item}); @@ -463,7 +486,8 @@ export class TimelineView extends Component { // On range change close the Details pane // this.closeSUDets(); // console.log(_.orderBy(group, ["parent", "id"], ['asc', 'desc'])); - return {group: this.stationView? this.getStationsByGroupName() : _.orderBy(_.uniqBy(group, 'id'),["parent", "start"], ['asc', 'asc']), items: items}; + group = this.state.stationView ? this.getStationsByGroupName() : _.orderBy(_.uniqBy(group, 'id'),["parent", "start"], ['asc', 'asc']); + return {group: group, items: items}; } /** @@ -849,13 +873,18 @@ export class TimelineView extends Component { // return <AppLoader /> // } const isSUDetsVisible = this.state.isSUDetsVisible; + const isReservDetsVisible = this.state.isReservDetsVisible; const isTaskDetsVisible = this.state.isTaskDetsVisible; const canExtendSUList = this.state.canExtendSUList; const canShrinkSUList = this.state.canShrinkSUList; - let suBlueprint = null; + let suBlueprint = null, reservation = null; if (isSUDetsVisible) { suBlueprint = _.find(this.state.suBlueprints, {id: this.state.stationView?parseInt(this.state.selectedItem.id.split('-')[0]):this.state.selectedItem.id}); } + if (isReservDetsVisible) { + reservation = _.find(this.reservations, {id: parseInt(this.state.selectedItem.id.split('-')[1])}); + reservation.project = this.state.selectedItem.project; + } let mouseOverItem = this.state.mouseOverItem; return ( <React.Fragment> @@ -869,7 +898,7 @@ export class TimelineView extends Component { { this.state.isLoading ? <AppLoader /> : <div className="p-grid"> {/* SU List Panel */} - <div className={isSUDetsVisible || 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")} + <div className={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={{position: "inherit", borderRight: "5px solid #efefef", paddingTop: "10px"}}> <ViewTable viewInNewWindow @@ -895,7 +924,7 @@ export class TimelineView extends Component { /> </div> {/* Timeline Panel */} - <div className={isSUDetsVisible || isTaskDetsVisible || (!canExtendSUList && canShrinkSUList)?"col-lg-5 col-md-5 col-sm-12":((canExtendSUList && canShrinkSUList)?"col-lg-7 col-md-7 col-sm-12":"col-lg-8 col-md-8 col-sm-12")}> + <div className={isSUDetsVisible || isReservDetsVisible || isTaskDetsVisible || (!canExtendSUList && canShrinkSUList)?"col-lg-5 col-md-5 col-sm-12":((canExtendSUList && canShrinkSUList)?"col-lg-7 col-md-7 col-sm-12":"col-lg-8 col-md-8 col-sm-12")}> {/* Panel Resize buttons */} <div className="resize-div"> <button className="p-link resize-btn" disabled={!this.state.canShrinkSUList} @@ -990,12 +1019,20 @@ export class TimelineView extends Component { } </div> } + {this.state.isReservDetsVisible && + <div className="col-lg-3 col-md-3 col-sm-12" + style={{borderLeft: "1px solid #efefef", marginTop: "0px", backgroundColor: "#f2f2f2"}}> + {this.state.isSummaryLoading?<AppLoader /> : + <ReservationSummary reservation={reservation} closeCallback={this.closeSUDets}></ReservationSummary> + } + </div> + } </div> } {/* SU Item Tooltip popover with SU status color */} <OverlayPanel className="timeline-popover" ref={(el) => this.popOver = el} dismissable> - {mouseOverItem && + {(mouseOverItem && (["SCHEDULE", "TASK"].indexOf(mouseOverItem.type)>=0)) && <div className={`p-grid su-${mouseOverItem.status}`} style={{width: '350px'}}> <h3 className={`col-12 su-${mouseOverItem.status}-icon`}>{mouseOverItem.type==='SCHEDULE'?'Scheduling Unit ':'Task '}Overview</h3> <hr></hr> @@ -1030,6 +1067,37 @@ export class TimelineView extends Component { <div className="col-7">{mouseOverItem.duration}</div> </div> } + {(mouseOverItem && mouseOverItem.type == "RESERVATION") && + <div className={`p-grid`} style={{width: '350px', backgroundColor: mouseOverItem.bgColor, color: mouseOverItem.color}}> + <h3 className={`col-12`}>Reservation Overview</h3> + <hr></hr> + <label className={`col-5`} style={{color: mouseOverItem.color}}>Name:</label> + <div className="col-7">{mouseOverItem.name}</div> + <label className={`col-5`} style={{color: mouseOverItem.color}}>Description:</label> + <div className="col-7">{mouseOverItem.desc}</div> + <label className={`col-5`} style={{color: mouseOverItem.color}}>Type:</label> + <div className="col-7">{mouseOverItem.activity_type}</div> + <label className={`col-5`} style={{color: mouseOverItem.color}}>Stations:</label> + {/* <div className="col-7"><ListBox options={mouseOverItem.stations} /></div> */} + <div className="col-7 station-list"> + {mouseOverItem.stations.map((station, index) => ( + <div key={`stn-${index}`}>{station}</div> + ))} + </div> + <label className={`col-5`} style={{color: mouseOverItem.color}}>Project:</label> + <div className="col-7">{mouseOverItem.project?mouseOverItem.project:"-"}</div> + <label className={`col-5`} style={{color: mouseOverItem.color}}>Start Time:</label> + <div className="col-7">{mouseOverItem.start_time.format(UIConstants.CALENDAR_DATETIME_FORMAT)}</div> + <label className={`col-5`} style={{color: mouseOverItem.color}}>End Time:</label> + <div className="col-7">{mouseOverItem.end_time.format(UIConstants.CALENDAR_DATETIME_FORMAT)}</div> + {/* <label className={`col-5`} style={{color: mouseOverItem.color}}>Stations:</label> + <div className="col-7">{mouseOverItem.stations.groups}:{mouseOverItem.stations.counts}</div> */} + <label className={`col-5`} style={{color: mouseOverItem.color}}>Duration:</label> + <div className="col-7">{mouseOverItem.duration}</div> + <label className={`col-5`} style={{color: mouseOverItem.color}}>Planned:</label> + <div className="col-7">{mouseOverItem.planned?'Yes':'No'}</div> + </div> + } </OverlayPanel> {!this.state.isLoading && <Websocket url={process.env.REACT_APP_WEBSOCKET_URL} onOpen={this.onConnect} onMessage={this.handleData} onClose={this.onDisconnect} /> } 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 a4d46a1bd20e1cc94ce92a960d0d40ab83811c95..08322699a4c79cdcfe50a79e093874b69509c2ad 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 @@ -23,6 +23,9 @@ import SchedulingUnitSummary from '../Scheduling/summary'; import UIConstants from '../../utils/ui.constants'; import { OverlayPanel } from 'primereact/overlaypanel'; import { TieredMenu } from 'primereact/tieredmenu'; +import { InputSwitch } from 'primereact/inputswitch'; +import { Dropdown } from 'primereact/dropdown'; +import ReservationSummary from './reservation.summary'; // Color constant for status const STATUS_COLORS = { "ERROR": "FF0000", "CANCELLED": "#00FF00", "DEFINED": "#00BCD4", @@ -30,6 +33,9 @@ const STATUS_COLORS = { "ERROR": "FF0000", "CANCELLED": "#00FF00", "DEFINED": "# "OBSERVED": "#cde", "PROCESSING": "#cddc39", "PROCESSED": "#fed", "INGESTING": "#edc", "FINISHED": "#47d53d"}; +const RESERVATION_COLORS = {"true-true":{bgColor:"lightgrey", color:"#585859"}, "true-false":{bgColor:'#585859', color:"white"}, + "false-true":{bgColor:"#9b9999", color:"white"}, "false-false":{bgColor:"black", color:"white"}}; + /** * Scheduling Unit timeline view component to view SU List and timeline */ @@ -50,10 +56,13 @@ export class WeekTimelineView extends Component { selectedItem: null, suTaskList:[], isSummaryLoading: false, - stationGroup: [] + stationGroup: [], + reservationEnabled: true } this.STATUS_BEFORE_SCHEDULED = ['defining', 'defined', 'schedulable']; // Statuses before scheduled to get station_group this.mainStationGroups = {}; + this.reservations = []; + this.reservationReasons = []; this.optionsMenu = React.createRef(); this.menuOptions = [ {label:'Add Reservation', icon: "fa fa-", command: () => {this.selectOptionMenu('Add Reservation')}}, {label:'Reservation List', icon: "fa fa-", command: () => {this.selectOptionMenu('Reservation List')}}, @@ -65,9 +74,12 @@ export class WeekTimelineView extends Component { this.closeSUDets = this.closeSUDets.bind(this); this.onItemMouseOver = this.onItemMouseOver.bind(this); this.onItemMouseOut = this.onItemMouseOut.bind(this); + this.showSUSummary = this.showSUSummary.bind(this); + this.showReservationSummary = this.showReservationSummary.bind(this); this.dateRangeCallback = this.dateRangeCallback.bind(this); this.resizeSUList = this.resizeSUList.bind(this); this.suListFilterCallback = this.suListFilterCallback.bind(this); + this.addWeekReservations = this.addWeekReservations.bind(this); this.handleData = this.handleData.bind(this); this.addNewData = this.addNewData.bind(this); this.updateExistingData = this.updateExistingData.bind(this); @@ -75,21 +87,33 @@ export class WeekTimelineView extends Component { } async componentDidMount() { + UtilService.getReservationTemplates().then(templates => { + this.reservationTemplate = templates.length>0?templates[0]:null; + if (this.reservationTemplate) { + let reasons = this.reservationTemplate.schema.properties.activity.properties.type.enum; + for (const reason of reasons) { + this.reservationReasons.push({name: reason}); + } + } + }); + // Fetch all details from server and prepare data to pass to timeline and table components const promises = [ ProjectService.getProjectList(), ScheduleService.getSchedulingUnitsExtended('blueprint'), ScheduleService.getSchedulingUnitDraft(), ScheduleService.getSchedulingSets(), UtilService.getUTC(), - TaskService.getSubtaskTemplates()] ; + TaskService.getSubtaskTemplates(), + UtilService.getReservations()] ; Promise.all(promises).then(async(responses) => { this.subtaskTemplates = responses[5]; const projects = responses[0]; const suBlueprints = _.sortBy(responses[1], 'name'); const suDrafts = responses[2].data.results; const suSets = responses[3] - const group = [], items = []; + let group = [], items = []; const currentUTC = moment.utc(responses[4]); + this.reservations = responses[6]; const defaultStartTime = moment.utc().day(-2).hour(0).minutes(0).seconds(0); const defaultEndTime = moment.utc().day(8).hour(23).minutes(59).seconds(59); for (const count of _.range(11)) { @@ -150,6 +174,9 @@ export class WeekTimelineView extends Component { } } } + if (this.state.reservationEnabled) { + items = this.addWeekReservations(items, defaultStartTime, defaultEndTime, currentUTC); + } // Get all scheduling constraint templates ScheduleService.getSchedulingConstraintTemplates() .then(suConstraintTemplates => { @@ -200,7 +227,19 @@ export class WeekTimelineView extends Component { * Callback function to pass to Timeline component for item click. * @param {Object} item */ - onItemClick(item) { + onItemClick(item) { + if (item.type === "SCHEDULE") { + this.showSUSummary(item); + } else if (item.type === "RESERVATION") { + this.showReservationSummary(item); + } + } + + /** + * To load SU summary and show + * @param {Object} item - Timeline SU item object. + */ + showSUSummary(item) { if (this.state.isSUDetsVisible && item.id===this.state.selectedItem.id) { this.closeSUDets(); } else { @@ -242,11 +281,19 @@ export class WeekTimelineView extends Component { } } + /** + * To load and show Reservation summary + * @param {Object} item + */ + showReservationSummary(item) { + this.setState({selectedItem: item, isReservDetsVisible: true, isSUDetsVisible: false}); + } + /** * Closes the SU details section */ closeSUDets() { - this.setState({isSUDetsVisible: false, canExtendSUList: true, canShrinkSUList: false}); + this.setState({isSUDetsVisible: false, isReservDetsVisible: false, canExtendSUList: true, canShrinkSUList: false}); } /** @@ -263,23 +310,36 @@ export class WeekTimelineView extends Component { * @param {Object} item */ onItemMouseOver(evt, item) { - const itemSU = _.find(this.state.suBlueprints, {id: parseInt(item.id.split("-")[0])}); - const itemStations = itemSU.stations; - const itemStationGroups = this.groupSUStations(itemStations); - item.stations = {groups: "", counts: ""}; - for (const stationgroup of _.keys(itemStationGroups)) { - let groups = item.stations.groups; - let counts = item.stations.counts; - if (groups) { - groups = groups.concat("/"); - counts = counts.concat("/"); + if (item.type === "SCHEDULE") { + const itemSU = _.find(this.state.suBlueprints, {id: parseInt(item.id.split("-")[0])}); + const itemStations = itemSU.stations; + const itemStationGroups = this.groupSUStations(itemStations); + item.stations = {groups: "", counts: ""}; + for (const stationgroup of _.keys(itemStationGroups)) { + let groups = item.stations.groups; + let counts = item.stations.counts; + if (groups) { + groups = groups.concat("/"); + counts = counts.concat("/"); + } + groups = groups.concat(stationgroup.substring(0,1).concat('S')); + counts = counts.concat(itemStationGroups[stationgroup].length); + item.stations.groups = groups; + item.stations.counts = counts; + item.suStartTime = moment.utc(itemSU.start_time); + item.suStopTime = moment.utc(itemSU.stop_time); } - groups = groups.concat(stationgroup.substring(0,1).concat('S')); - counts = counts.concat(itemStationGroups[stationgroup].length); - item.stations.groups = groups; - item.stations.counts = counts; - item.suStartTime = moment.utc(itemSU.start_time); - item.suStopTime = moment.utc(itemSU.stop_time); + } else { + const reservation = _.find(this.reservations, {'id': parseInt(item.id.split("-")[1])}); + const reservStations = reservation.specifications_doc.resources.stations; + const reservStationGroups = this.groupSUStations(reservStations); + item.name = reservation.name; + item.contact = reservation.specifications_doc.activity.contact + item.activity_type = reservation.specifications_doc.activity.type; + item.stations = reservStations; + item.planned = reservation.specifications_doc.activity.planned; + item.displayStartTime = moment.utc(reservation.start_time); + item.displayEndTime = reservation.duration?moment.utc(reservation.stop_time):null; } this.popOver.toggle(evt); this.setState({mouseOverItem: item}); @@ -362,6 +422,9 @@ export class WeekTimelineView extends Component { } } } + if (this.state.reservationEnabled) { + items = this.addWeekReservations(items, startTime, endTime, currentUTC); + } } else { suBlueprintList = _.clone(this.state.suBlueprints); group = this.state.group; @@ -578,17 +641,148 @@ export class WeekTimelineView extends Component { }); } + async showReservations(e) { + await this.setState({reservationEnabled: e.value}); + let updatedItemGroupData = await this.dateRangeCallback(this.state.startTime, this.state.endTime, true); + this.timeline.updateTimeline(updatedItemGroupData); + } + + /** + * Add Week Reservations during the visible timeline period + * @param {Array} items + * @param {moment} startTime + * @param {moment} endTime + */ + addWeekReservations(items, startTime, endTime, currentUTC) { + let reservations = this.reservations; + for (const reservation of reservations) { + const reservationStartTime = moment.utc(reservation.start_time); + const reservationEndTime = reservation.duration?reservationStartTime.clone().add(reservation.duration, 'seconds'):endTime; + const reservationSpec = reservation.specifications_doc; + if ( (reservationStartTime.isSame(startTime) + || reservationStartTime.isSame(endTime) + || reservationStartTime.isBetween(startTime, endTime) + || reservationEndTime.isSame(startTime) + || reservationEndTime.isSame(endTime) + || reservationEndTime.isBetween(startTime, endTime) + || (reservationStartTime.isSameOrBefore(startTime) + && reservationEndTime.isSameOrAfter(endTime))) + && (!this.state.reservationFilter || // No reservation filter added + reservationSpec.activity.type === this.state.reservationFilter) ) { // Reservation reason == Filtered reaseon + reservation.stop_time = reservationEndTime; + let splitReservations = this.splitReservations(reservation, startTime, endTime, currentUTC); + for (const splitReservation of splitReservations) { + items.push(this.getReservationItem(splitReservation, currentUTC)); + } + + } + } + return items; + } + + /** + * Function to check if a reservation is for more than a day and split it to multiple objects to display in each day + * @param {Object} reservation - Reservation object + * @param {moment} startTime - moment object of the start datetime of the week view + * @param {moment} endTime - moment object of the end datetime of the week view + * @returns + */ + splitReservations(reservation, startTime, endTime) { + const reservationStartTime = moment.utc(reservation.start_time); + let weekStartDate = moment(startTime).add(-1, 'day').startOf('day'); + let weekEndDate = moment(endTime).add(1, 'day').startOf('day'); + let splitReservations = []; + while(weekStartDate.add(1, 'days').diff(weekEndDate) < 0) { + const dayStart = weekStartDate.clone().startOf('day'); + const dayEnd = weekStartDate.clone().endOf('day'); + let splitReservation = null; + if (reservationStartTime.isSameOrBefore(dayStart) && + (reservation.stop_time.isBetween(dayStart, dayEnd) || + reservation.stop_time.isSameOrAfter(dayEnd))) { + splitReservation = _.cloneDeep(reservation); + splitReservation.start_time = moment.utc(dayStart.format("YYYY-MM-DD HH:mm:ss")); + } else if(reservationStartTime.isBetween(dayStart, dayEnd)) { + splitReservation = _.cloneDeep(reservation); + splitReservation.start_time = reservationStartTime; + } + if (splitReservation) { + if (!reservation.stop_time || reservation.stop_time.isSameOrAfter(dayEnd)) { + splitReservation.end_time = weekStartDate.clone().hour(23).minute(59).seconds(59); + } else if (reservation.stop_time.isSameOrBefore(dayEnd)) { + splitReservation.end_time = weekStartDate.clone().hour(reservation.stop_time.hours()).minutes(reservation.stop_time.minutes()).seconds(reservation.stop_time.seconds); + } + splitReservations.push(splitReservation); + } + } + return splitReservations; + } + + /** + * Get reservation timeline item. If the reservation doesn't have duration, item endtime should be week endtime. + * @param {Object} reservation + * @param {moment} endTime + */ + getReservationItem(reservation, displayDate) { + const reservationSpec = reservation.specifications_doc; + const group = moment.utc(reservation.start_time).format("MMM DD ddd"); + const blockColor = RESERVATION_COLORS[this.getReservationType(reservationSpec.schedulability)]; + let item = { id: `Res-${reservation.id}-${group}`, + start_time: moment.utc(`${displayDate.format('YYYY-MM-DD')} ${reservation.start_time.format('HH:mm:ss')}`), + end_time: moment.utc(`${displayDate.format('YYYY-MM-DD')} ${reservation.end_time.format('HH:mm:ss')}`), + name: reservationSpec.activity.type, project: reservation.project_id, + group: group, + type: 'RESERVATION', + title: `${reservationSpec.activity.type}${reservation.project_id?("-"+ reservation.project_id):""}`, + desc: reservation.description, + duration: reservation.duration?UnitConverter.getSecsToHHmmss(reservation.duration):"Unknown", + bgColor: blockColor.bgColor, selectedBgColor: blockColor.bgColor, color: blockColor.color + }; + return item; + } + + /** + * Get the schedule type from the schedulability object. It helps to get colors of the reservation blocks + * according to the type. + * @param {Object} schedulability + */ + getReservationType(schedulability) { + if (schedulability.manual && schedulability.dynamic) { + return 'true-true'; + } else if (!schedulability.manual && !schedulability.dynamic) { + return 'false-false'; + } else if (schedulability.manual && !schedulability.dynamic) { + return 'true-false'; + } else { + return 'false-true'; + } + } + + /** + * Set reservation filter + * @param {String} filter + */ + async setReservationFilter(filter) { + await this.setState({reservationFilter: filter}); + let updatedItemGroupData = await this.dateRangeCallback(this.state.startTime, this.state.endTime, true); + this.timeline.updateTimeline(updatedItemGroupData); + } + render() { if (this.state.redirect) { return <Redirect to={ {pathname: this.state.redirect} }></Redirect> } const isSUDetsVisible = this.state.isSUDetsVisible; + const isReservDetsVisible = this.state.isReservDetsVisible; const canExtendSUList = this.state.canExtendSUList; const canShrinkSUList = this.state.canShrinkSUList; - let suBlueprint = null; + let suBlueprint = null, reservation = null; if (isSUDetsVisible) { suBlueprint = _.find(this.state.suBlueprints, {id: parseInt(this.state.selectedItem.id.split('-')[0])}); } + if (isReservDetsVisible) { + reservation = _.find(this.reservations, {id: parseInt(this.state.selectedItem.id.split('-')[1])}); + reservation.project = this.state.selectedItem.project; + } const mouseOverItem = this.state.mouseOverItem; return ( <React.Fragment> @@ -609,7 +803,7 @@ export class WeekTimelineView extends Component { </div> */} <div className="p-grid"> {/* SU List Panel */} - <div className={isSUDetsVisible || (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")} + <div className={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={{position: "inherit", borderRight: "5px solid #efefef", paddingTop: "10px"}}> <ViewTable viewInNewWindow data={this.state.suBlueprintList} @@ -628,7 +822,7 @@ export class WeekTimelineView extends Component { /> </div> {/* Timeline Panel */} - <div className={isSUDetsVisible || (!canExtendSUList && canShrinkSUList)?"col-lg-5 col-md-5 col-sm-12":((canExtendSUList && canShrinkSUList)?"col-lg-7 col-md-7 col-sm-12":"col-lg-8 col-md-8 col-sm-12")}> + <div className={isSUDetsVisible || isReservDetsVisible || (!canExtendSUList && canShrinkSUList)?"col-lg-5 col-md-5 col-sm-12":((canExtendSUList && canShrinkSUList)?"col-lg-7 col-md-7 col-sm-12":"col-lg-8 col-md-8 col-sm-12")}> {/* Panel Resize buttons */} <div className="resize-div"> <button className="p-link resize-btn" disabled={!this.state.canShrinkSUList} @@ -642,6 +836,28 @@ export class WeekTimelineView extends Component { <i className="pi pi-step-forward"></i> </button> </div> + <div className={`timeline-view-toolbar ${this.state.reservationEnabled && 'alignTimeLineHeader'}`}> + <div className="sub-header"> + <label >Show Reservations</label> + <InputSwitch checked={this.state.reservationEnabled} onChange={(e) => {this.showReservations(e)}} /> + + </div> + + {this.state.reservationEnabled && + <div className="sub-header"> + <label style={{marginLeft: '20px'}}>Reservation</label> + <Dropdown optionLabel="name" optionValue="name" + style={{top:'2px'}} + value={this.state.reservationFilter} + options={this.reservationReasons} + filter showClear={true} filterBy="name" + onChange={(e) => {this.setReservationFilter(e.value)}} + placeholder="Reason"/> + + </div> + } + </div> + <Timeline ref={(tl)=>{this.timeline=tl}} group={this.state.group} items={this.state.items} @@ -673,13 +889,20 @@ export class WeekTimelineView extends Component { } </div> } - + {this.state.isReservDetsVisible && + <div className="col-lg-3 col-md-3 col-sm-12" + style={{borderLeft: "1px solid #efefef", marginTop: "0px", backgroundColor: "#f2f2f2"}}> + {this.state.isSummaryLoading?<AppLoader /> : + <ReservationSummary reservation={reservation} location={this.props.location} closeCallback={this.closeSUDets}></ReservationSummary> + } + </div> + } </div> </> } {/* SU Item Tooltip popover with SU status color */} <OverlayPanel className="timeline-popover" ref={(el) => this.popOver = el} dismissable> - {mouseOverItem && + {mouseOverItem && mouseOverItem.type == "SCHEDULE" && <div className={`p-grid su-${mouseOverItem.status}`} style={{width: '350px'}}> <label className={`col-5 su-${mouseOverItem.status}-icon`}>Project:</label> <div className="col-7">{mouseOverItem.project}</div> @@ -701,6 +924,37 @@ export class WeekTimelineView extends Component { <div className="col-7">{mouseOverItem.duration}</div> </div> } + {(mouseOverItem && mouseOverItem.type == "RESERVATION") && + <div className={`p-grid`} style={{width: '350px', backgroundColor: mouseOverItem.bgColor, color: mouseOverItem.color}}> + <h3 className={`col-12`}>Reservation Overview</h3> + <hr></hr> + <label className={`col-5`} style={{color: mouseOverItem.color}}>Name:</label> + <div className="col-7">{mouseOverItem.name}</div> + <label className={`col-5`} style={{color: mouseOverItem.color}}>Description:</label> + <div className="col-7">{mouseOverItem.desc}</div> + <label className={`col-5`} style={{color: mouseOverItem.color}}>Type:</label> + <div className="col-7">{mouseOverItem.activity_type}</div> + <label className={`col-5`} style={{color: mouseOverItem.color}}>Stations:</label> + {/* <div className="col-7"><ListBox options={mouseOverItem.stations} /></div> */} + <div className="col-7 station-list"> + {mouseOverItem.stations.map((station, index) => ( + <div key={`stn-${index}`}>{station}</div> + ))} + </div> + <label className={`col-5`} style={{color: mouseOverItem.color}}>Project:</label> + <div className="col-7">{mouseOverItem.project?mouseOverItem.project:"-"}</div> + <label className={`col-5`} style={{color: mouseOverItem.color}}>Start Time:</label> + <div className="col-7">{mouseOverItem.displayStartTime.format(UIConstants.CALENDAR_DATETIME_FORMAT)}</div> + <label className={`col-5`} style={{color: mouseOverItem.color}}>End Time:</label> + <div className="col-7">{mouseOverItem.displayEndTime?mouseOverItem.displayEndTime.format(UIConstants.CALENDAR_DATETIME_FORMAT):'Unknown'}</div> + {/* <label className={`col-5`} style={{color: mouseOverItem.color}}>Stations:</label> + <div className="col-7">{mouseOverItem.stations.groups}:{mouseOverItem.stations.counts}</div> */} + <label className={`col-5`} style={{color: mouseOverItem.color}}>Duration:</label> + <div className="col-7">{mouseOverItem.duration}</div> + <label className={`col-5`} style={{color: mouseOverItem.color}}>Planned:</label> + <div className="col-7">{mouseOverItem.planned?'Yes':'No'}</div> + </div> + } </OverlayPanel> {/* Open Websocket after loading all initial data */} {!this.state.isLoading && diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/index.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/index.js index c296c76ff16c1d6ba260e04723bdb12ce8d9158d..286d0c21dd0a1a496adfb6f1be8a519bd5effdd9 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/index.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/index.js @@ -9,12 +9,13 @@ import {NotFound} from '../layout/components/NotFound'; import {ProjectList, ProjectCreate, ProjectView, ProjectEdit} from './Project'; import {Dashboard} from './Dashboard'; import {Scheduling} from './Scheduling'; -import {TaskEdit, TaskView, DataProduct} from './Task'; +import {TaskEdit, TaskView, DataProduct, TaskList} from './Task'; import ViewSchedulingUnit from './Scheduling/ViewSchedulingUnit' import SchedulingUnitCreate from './Scheduling/create'; import EditSchedulingUnit from './Scheduling/edit'; import { CycleList, CycleCreate, CycleView, CycleEdit } from './Cycle'; import { TimelineView, WeekTimelineView, ReservationCreate, ReservationList } from './Timeline'; +import { FindObjectResult } from './Search/' import SchedulingSetCreate from './Scheduling/excelview.schedulingset'; import Workflow from './Workflow'; import { Growl } from 'primereact/components/growl/Growl'; @@ -41,9 +42,9 @@ export const routes = [ title: 'Scheduling Unit - Add' },{ path: "/task", - component: TaskView, + component: TaskList, name: 'Task', - title: 'Task-View' + title: 'Task-List' },{ path: "/task/view", component: TaskView, @@ -165,6 +166,12 @@ export const routes = [ component: ReservationCreate, name: 'Reservation Add', title: 'Reservation - Add' + }, + { + path: "/find/object/:type/:id", + component: FindObjectResult, + name: 'Find Object', + title: 'Find Object' } ]; diff --git a/SAS/TMSS/frontend/tmss_webapp/src/services/task.service.js b/SAS/TMSS/frontend/tmss_webapp/src/services/task.service.js index 3b99780c7ae8fe731e2311362665cfe273c61d8b..fd4b6d769ecc53b022be3da580317ebbae11a24d 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/services/task.service.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/services/task.service.js @@ -2,31 +2,39 @@ const axios = require('axios'); const TaskService = { getTaskDetails: async function (taskType, taskId) { - try { - const url = taskType === 'blueprint'? '/api/task_blueprint/': '/api/task_draft/'; - const response = await axios.get(url + taskId); - response.data.predecessors = []; - response.data.successors = []; - if (taskType === 'blueprint') { - response.data.blueprints = []; - } else { - response.data.draftName = null; - } - return this.getTaskRelationsByTask(taskType, response.data) - .then(relations => { - response.data.predecessors = relations.predecessors; - response.data.successors = relations.successors; - if (taskType === 'draft') { - response.data.blueprints = relations.blueprints; - } else { - response.data.draftObject = relations.draft; - } - return response.data; - }); - - } catch (error) { - console.error(error); + try { + const responseData = await this.getTask(taskType, taskId); + responseData.predecessors = []; + responseData.successors = []; + if (taskType === 'blueprint') { + responseData.blueprints = []; + } else { + responseData.draftName = null; } + return this.getTaskRelationsByTask(taskType, responseData) + .then(relations => { + responseData.predecessors = relations.predecessors; + responseData.successors = relations.successors; + if (taskType === 'draft') { + responseData.blueprints = relations.blueprints; + } else { + responseData.draftObject = relations.draft; + } + return responseData; + }); + + } catch (error) { + console.error(error); + } + }, + getTask : async function (taskType, taskId) { + try { + const url = taskType === 'blueprint'? '/api/task_blueprint/': '/api/task_draft/'; + const response = await axios.get(url + taskId); + return response.data; + } catch (error) { + console.error(error); + } }, getTaskTemplate: async function(templateId) { try { @@ -53,6 +61,24 @@ const TaskService = { console.error(error); } }, + getTaskDraftList: async function() { + try { + const url = `/api/task_draft`; + const response = await axios.get(url); + return response.data.results; + } catch (error) { + console.error(error); + } + }, + getTaskBlueprintList: async function() { + try { + const url = `/api/task_blueprint`; + const response = await axios.get(url); + return response.data.results; + } catch (error) { + console.error(error); + } + }, updateTask: async function(type, task) { try { const response = await axios.put(('/api/task_draft/' + task.id + "/"), task); diff --git a/SAS/TMSS/frontend/tmss_webapp/src/services/util.service.js b/SAS/TMSS/frontend/tmss_webapp/src/services/util.service.js index d46b2fba3e90b785a60bf6f1f95e9ac0edc0dc74..036165fc7138afbbed3f02de55ff5a1564b4f438 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/services/util.service.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/services/util.service.js @@ -64,6 +64,19 @@ const UtilService = { return null; } }, + /** + * + * @param {String} timestamps - Date in 'YYYY-MM-DD' format. Multiples dates are separated by commas (2020-08-15, 2021-01-26). + */ + getAllStationSunTimings: async(timestamps) => { + try { + let allStations = (await axios.get("/api/station_groups/stations/1/All")).data.stations; + let allStationSuntimes = (await axios.get(`/api/util/sun_rise_and_set?stations=${allStations.join(",")}×tamps=${timestamps}`)).data; + return allStationSuntimes; + } catch(error) { + console.error(error); + } + }, /** Gets all reservations in the system */ getReservations: async() => { try {