diff --git a/SAS/TMSS/frontend/tmss_webapp/src/components/ViewTable.js b/SAS/TMSS/frontend/tmss_webapp/src/components/ViewTable.js index 00f02fc49b31ac94ad2bae9182a4cc2a5291df50..3428a67afc894027d9fb81d49d12ededd102db17 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/components/ViewTable.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/components/ViewTable.js @@ -1,5 +1,5 @@ import React, {useRef, useState } from "react"; -import { useSortBy, useTable, useFilters, useGlobalFilter, useAsyncDebounce, usePagination } from 'react-table' +import { useSortBy, useTable, useFilters, useGlobalFilter, useAsyncDebounce, usePagination, useRowSelect } from 'react-table' import matchSorter from 'match-sorter' import _ from 'lodash'; import moment from 'moment'; @@ -15,13 +15,15 @@ import { Button } from "react-bootstrap"; import { InputNumber } from "primereact/inputnumber"; let tbldata =[], filteredData = [] ; +let selectedRows = []; let isunittest = false; let showTopTotal = true; let showGlobalFilter = true; let showColumnFilter = true; let allowColumnSelection = true; +let allowRowSelection = false; let columnclassname =[]; -let parentCallbackFunction; +let parentCallbackFunction, parentCBonSelection; // Define a default UI for filtering function GlobalFilter({ @@ -406,6 +408,7 @@ const defaultColumn = React.useMemo( setHiddenColumns, gotoPage, setPageSize, + selectedFlatRows, } = useTable( { columns, @@ -419,7 +422,8 @@ const defaultColumn = React.useMemo( useFilters, useGlobalFilter, useSortBy, - usePagination + usePagination, + useRowSelect ); React.useEffect(() => { setHiddenColumns( @@ -478,6 +482,15 @@ const defaultColumn = React.useMemo( if (parentCallbackFunction) { parentCallbackFunction(filteredData); } + + /* Select only rows than can be selected. This is required when ALL is selected */ + selectedRows = _.filter(selectedFlatRows, selectedRow => { return (selectedRow.original.canSelect===undefined || selectedRow.original.canSelect)}); + /* Take only the original values passed to the component */ + selectedRows = _.map(selectedRows, 'original'); + /* Callback the parent function if available to pass the selected records on selection */ + if (parentCBonSelection) { + parentCBonSelection(selectedRows) + } return ( <> @@ -524,10 +537,12 @@ const defaultColumn = React.useMemo( setGlobalFilter={setGlobalFilter} /> } - </div> - { showTopTotal && + </div> + { showTopTotal && filteredData.length === data.length && <div className="total_records_top_label"> <label >Total records ({data.length})</label></div> } + { showTopTotal && filteredData.length < data.length && + <div className="total_records_top_label" ><label >Filtered {filteredData.length} from {data.length}</label></div>} </div> <div className="tmss-table table_container"> @@ -575,7 +590,10 @@ const defaultColumn = React.useMemo( </table> </div> <div className="pagination p-grid" > - <div className="total_records_bottom_label" ><label >Total records ({data.length})</label></div> + {filteredData.length === data.length && + <div className="total_records_bottom_label" ><label >Total records ({data.length})</label></div>} + {filteredData.length < data.length && + <div className="total_records_bottom_label" ><label >Filtered {filteredData.length} from {data.length}</label></div>} <div> <Paginator rowsPerPageOptions={[10,25,50,100]} first={currentpage} rows={currentrows} totalRecords={rows.length} onPageChange={onPagination}></Paginator> </div> @@ -612,12 +630,14 @@ function ViewTable(props) { // Data to show in table tbldata = props.data; parentCallbackFunction = props.filterCallback; + parentCBonSelection = props.onRowSelection; isunittest = props.unittest; columnclassname = props.columnclassname; showTopTotal = props.showTopTotal===undefined?true:props.showTopTotal; showGlobalFilter = props.showGlobalFilter===undefined?true:props.showGlobalFilter; showColumnFilter = props.showColumnFilter===undefined?true:props.showColumnFilter; allowColumnSelection = props.allowColumnSelection===undefined?true:props.allowColumnSelection; + allowRowSelection = props.allowRowSelection===undefined?false:props.allowRowSelection; // Default Header to show in table and other columns header will not show until user action on UI let defaultheader = props.defaultcolumns; let optionalheader = props.optionalcolumns; @@ -631,6 +651,33 @@ function ViewTable(props) { let columns = []; let defaultdataheader = Object.keys(defaultheader[0]); let optionaldataheader = Object.keys(optionalheader[0]); + + /* If allowRowSelection property is true for the component, add checkbox column as 1st column. + If the record has property to select, enable the checkbox */ + if (allowRowSelection) { + columns.push({ + Header: ({ getToggleAllRowsSelectedProps }) => { return ( + <div> + <IndeterminateCheckbox {...getToggleAllRowsSelectedProps()} style={{width:'15px', height:'15px'}}/> + </div> + )}, + id:'Select', + accessor: props.keyaccessor, + Cell: ({ row }) => { return ( + <div> + {(row.original.canSelect===undefined || row.original.canSelect) && + <IndeterminateCheckbox {...row.getToggleRowSelectedProps()} style={{width:'15px', height:'15px'}}/> + } + {row.original.canSelect===false && + <input type="checkbox" checked={false} disabled style={{width:'15px', height:'15px'}}></input> + } + </div> + )}, + disableFilters: true, + disableSortBy: true, + isVisible: defaultdataheader.includes(props.keyaccessor), + }); + } if(props.showaction === 'true') { columns.push({ diff --git a/SAS/TMSS/frontend/tmss_webapp/src/layout/components/CustomDialog.js b/SAS/TMSS/frontend/tmss_webapp/src/layout/components/CustomDialog.js index 36011d55503f500defa9ad2be7b36362746f022c..ea013dca232d1dd5a0cc4e1dcda11542f79af1ce 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/layout/components/CustomDialog.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/layout/components/CustomDialog.js @@ -16,26 +16,44 @@ export class CustomDialog extends Component { } render() { - const isConfirmDialog = this.props.type==='confirmation'; + const isConfirm = this.props.type.toLowerCase()==='confirmation'; + const isWarning = this.props.type.toLowerCase()==='warning'; + const isSuccess = this.props.type.toLowerCase()==='success'; + // const isError = this.props.type.toLowerCase()==='error'; + let iconClass = isConfirm?"pi-question-circle pi-warning":(isWarning?"pi-info-circle pi-warning": (isSuccess?"pi-check-circle pi-success":"pi-times-circle pi-danger")); return ( <div className="p-grid" data-testid="confirm_dialog"> - <Dialog header={this.props.header} visible={this.props.visible} style={{width: '25vw'}} inputId="confirm_dialog" + <Dialog header={this.props.header} visible={this.props.visible} style={{width: this.props.width?this.props.width:'25vw'}} + inputId="confirm_dialog" modal={true} onHide={this.props.onClose} footer={<div> - {isConfirmDialog && - <Button key="back" onClick={this.props.onCancel} label="No" /> + {/* Action buttons based on 'type' props. If 'actions' passed as props, then type is ignored */} + {!this.props.actions && + <> + {isConfirm && + <Button key="back" onClick={this.props.onCancel} label="No" /> + } + <Button key="submit" type="primary" onClick={this.props.onSubmit?this.props.onSubmit:this.props.onClose} label={isConfirm?'Yes':'Ok'} /> + </> } - <Button key="submit" type="primary" onClick={this.props.onSubmit} label={isConfirmDialog?'Yes':'Ok'} /> + {/* Action button based on the 'actions' props */} + {this.props.actions && this.props.actions.map((action, index) => { + return ( + <Button key={action.id} label={action.title} onClick={action.callback} />); + })} </div> } > <div className="p-grid"> <div className="col-lg-2 col-md-2 col-sm-2"> <span style={{position: 'absolute', top: '50%', '-ms-transform': 'translateY(-50%)', transform: 'translateY(-50%)'}}> - <i className="pi pi-question-circle pi-large pi-warning"></i> + <i className={`pi pi-large ${iconClass}`}></i> </span> </div> <div className="col-lg-10 col-md-10 col-sm-10"> - {this.props.message} + {/* Display message passed */} + {this.props.message?this.props.message:""} + {/* Render subcomponent passed as function */} + {this.props.content?this.props.content():""} </div> </div> </Dialog> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/layout/components/PageHeader.js b/SAS/TMSS/frontend/tmss_webapp/src/layout/components/PageHeader.js index fb95ec75a094fc8a2d86bdf74ba78fab8c885a39..02de326d2c7ab3829d12003304e28da3f77fa090 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/layout/components/PageHeader.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/layout/components/PageHeader.js @@ -42,7 +42,7 @@ export default ({ title, subTitle, actions, ...props}) => { {(actions || []).map((action, index) =>{ if (action.type === 'button') { return ( - <button className="p-link" key={index}> + <button className="p-link" key={index} title={action.title || ''}> <i className={`fa ${action.icon}`} onMouseOver={(e) => onButtonMouseOver(e, action)} onClick={(e) => onButtonClick(e, action)} /> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/SchedulingUnitList.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/SchedulingUnitList.js index 570ca6388bd7f10954ccb6fe2731e0b424f92435..f64b1133eb4b63b6787e5913e8e4a2f75f48452f 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/SchedulingUnitList.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/SchedulingUnitList.js @@ -56,6 +56,8 @@ class SchedulingUnitList extends Component{ }], defaultSortColumn: [{id: "Name", desc: false}], } + this.onRowSelection = this.onRowSelection.bind(this); + this.reloadData = this.reloadData.bind(this); } async getSchedulingUnitList () { @@ -86,6 +88,7 @@ class SchedulingUnitList extends Component{ blueP['created_at'] = moment(blueP['created_at'], moment.ISO_8601).format("YYYY-MMM-DD HH:mm:ss"); blueP['updated_at'] = moment(blueP['updated_at'], moment.ISO_8601).format("YYYY-MMM-DD HH:mm:ss"); blueP.project = project.name; + blueP.canSelect = false; return blueP; }); output.push(...blueprintdata); @@ -95,11 +98,13 @@ class SchedulingUnitList extends Component{ scheduleunit['created_at'] = moment(scheduleunit['created_at'], moment.ISO_8601).format("YYYY-MMM-DD HH:mm:ss"); scheduleunit['updated_at'] = moment(scheduleunit['updated_at'], moment.ISO_8601).format("YYYY-MMM-DD HH:mm:ss"); scheduleunit.project = project.name; + scheduleunit.canSelect = true; output.push(scheduleunit); } this.setState({ scheduleunit: output, isLoading: false }); + this.selectedRows = []; }) } } @@ -109,6 +114,22 @@ class SchedulingUnitList extends Component{ } + /** + * Callback function passed to ViewTable component to pass back the selected rows. + * @param {Array} selectedRows - Subset of data passed to the ViewTable component based on selection. + */ + onRowSelection(selectedRows) { + this.selectedRows = selectedRows; + } + + /** + * Funtion to reload data. This function can be called from the implementing component. + */ + reloadData() { + this.setState({isLoading: true}); + this.getSchedulingUnitList(); + } + render(){ if (this.state.isLoading) { return <AppLoader/> @@ -139,6 +160,8 @@ class SchedulingUnitList extends Component{ paths={this.state.paths} unittest={this.state.unittest} tablename="scheduleunit_list" + allowRowSelection={this.props.allowRowSelection} + onRowSelection = {this.onRowSelection} /> :<div>No scheduling unit found </div> } diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/ViewSchedulingUnit.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/ViewSchedulingUnit.js index 174057f6d07a1fd79af824f849439bbdd705dc37..57919d4da296971a9a0523dceb4c9acf00172280 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/ViewSchedulingUnit.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/ViewSchedulingUnit.js @@ -17,6 +17,7 @@ import Stations from './Stations'; import { Redirect } from 'react-router-dom'; import { CustomDialog } from '../../layout/components/CustomDialog'; import { CustomPageSpinner } from '../../components/CustomPageSpinner'; +import { Growl } from 'primereact/components/growl/Growl'; class ViewSchedulingUnit extends Component{ constructor(props){ @@ -77,7 +78,7 @@ class ViewSchedulingUnit extends Component{ "Status":"filter-input-100" }], stationGroup: [], - dialog: {header: 'Confirm', detail: 'Blueprint(s) already exist for this Scheduling Unit. Do you want to create another one?'}, + dialog: {header: 'Confirm', detail: 'Do you want to create a Scheduling Unit Blueprint?'}, dialogVisible: false } this.actions = []; @@ -153,7 +154,7 @@ class ViewSchedulingUnit extends Component{ if (this.props.match.params.type === 'draft') { this.actions.unshift({icon: 'fa-edit', title: 'Click to edit', props : { pathname:`/schedulingunit/edit/${ this.props.match.params.id}`} }); - this.actions.unshift({icon:'fa-stamp',title: '', type:'button', + this.actions.unshift({icon:'fa-stamp', title: 'Create Blueprint', type:'button', actOn:'click', props : { callback: this.checkAndCreateBlueprint}, }); } else { @@ -176,25 +177,36 @@ class ViewSchedulingUnit extends Component{ return ScheduleService.getSchedulingUnitBlueprintById(id) } + /** + * Checks if the draft scheduling unit has existing blueprints and alerts. If confirms to create, creates blueprint. + */ checkAndCreateBlueprint() { if (this.state.scheduleunit) { + let dialog = this.state.dialog; if (this.state.scheduleunit.scheduling_unit_blueprints.length>0) { - this.setState({dialogVisible: true}); - } else { - this.setState({dialogVisible: false}); - this.createBlueprintTree(); + dialog.detail = "Blueprint(s) already exist for this Scheduling Unit. Do you want to create another one?"; } + dialog.actions = [{id: 'yes', title: 'Yes', callback: this.createBlueprintTree}, + {id: 'no', title: 'No', callback: this.closeDialog}]; + this.setState({dialogVisible: true, dialog: dialog}); } } + /** + * Funtion called to create blueprint on confirmation. + */ createBlueprintTree() { this.setState({dialogVisible: false, showSpinner: true}); ScheduleService.createSchedulingUnitBlueprintTree(this.state.scheduleunit.id) .then(blueprint => { + this.growl.show({severity: 'success', summary: 'Success', detail: 'Blueprint created successfully!'}); this.setState({showSpinner: false, redirect: `/schedulingunit/view/blueprint/${blueprint.id}`, isLoading: true}); }); } + /** + * Callback function to close the dialog prompted. + */ closeDialog() { this.setState({dialogVisible: false}); } @@ -205,22 +217,7 @@ class ViewSchedulingUnit extends Component{ } return( <> - {/*} <div className="p-grid"> - <div className="p-col-10"> - <h2>Scheduling Unit - Details </h2> - </div> - <div className="p-col-2"> - <Link to={{ pathname: '/schedulingunit'}} title="Close" - style={{float:'right'}}> - <i className="fa fa-times" style={{marginTop: "10px", marginLeft: '5px'}}></i> - </Link> - <Link to={{ pathname: '/schedulingunit/edit', state: {id: this.state.scheduleunit?this.state.scheduleunit.id:''}}} title="Edit" - style={{float:'right'}}> - <i className="fa fa-edit" style={{marginTop: "10px"}}></i> - </Link> - </div> - </div> */ - /*TMSS-363 Blueprint icon changes */} + <Growl ref={(el) => this.growl = el} /> <PageHeader location={this.props.location} title={'Scheduling Unit - Details'} actions={this.actions}/> { this.state.isLoading ? <AppLoader/> :this.state.scheduleunit && @@ -309,7 +306,7 @@ class ViewSchedulingUnit extends Component{ } {/* Dialog component to show messages and get confirmation */} <CustomDialog type="confirmation" visible={this.state.dialogVisible} - header={this.state.dialog.header} message={this.state.dialog.detail} + header={this.state.dialog.header} message={this.state.dialog.detail} actions={this.state.dialog.actions} onClose={this.closeDialog} onCancel={this.closeDialog} onSubmit={this.createBlueprintTree}></CustomDialog> {/* Show spinner during backend API call */} <CustomPageSpinner visible={this.state.showSpinner} /> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/index.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/index.js index 8ff0d98a82fecec96b172ba1b4d85ec20d8466fc..3c4005621301b56437fac0f8ac0389dfb232510b 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/index.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/index.js @@ -1,7 +1,13 @@ import React, {Component} from 'react'; +import _ from 'lodash'; + import SchedulingUnitList from './SchedulingUnitList'; import PageHeader from '../../layout/components/PageHeader'; import { TieredMenu } from 'primereact/tieredmenu'; +import { CustomDialog } from '../../layout/components/CustomDialog'; +import { CustomPageSpinner } from '../../components/CustomPageSpinner'; +import ScheduleService from '../../services/schedule.service'; +import { Growl } from 'primereact/components/growl/Growl'; export class Scheduling extends Component { constructor(props){ @@ -10,7 +16,8 @@ export class Scheduling extends Component { scheduleunit: [], schedule_unit_task: [] , isLoading:false, - redirect: '' + redirect: '', + dialog: {header: 'Confirm', detail: 'Do you want to create blueprints for the selected drafts?'}, }; this.optionsMenu = React.createRef(); @@ -18,6 +25,11 @@ export class Scheduling extends Component { this.showOptionMenu = this.showOptionMenu.bind(this); this.selectOptionMenu = this.selectOptionMenu.bind(this); + this.checkAndCreateBlueprint = this.checkAndCreateBlueprint.bind(this); + this.createBlueprintTree = this.createBlueprintTree.bind(this); + this.createBlueprintTreeNewOnly = this.createBlueprintTreeNewOnly.bind(this); + this.warningContent = this.warningContent.bind(this); + this.closeDialog = this.closeDialog.bind(this); } showOptionMenu(event) { @@ -36,20 +48,133 @@ export class Scheduling extends Component { } } + /** + * Subcomponet to display in the confirmation dialog. + */ + warningContent() { + const suListWithBlueprint = this.state.schedulingUnitsWithBlueprint; + const suListWithoutBlueprint = _.difference(this.suList.selectedRows, suListWithBlueprint); + return ( + <> + {suListWithBlueprint && suListWithBlueprint.length>0 && + <div> + <hr></hr> + <span>Blueprint(s) already exist for the following Scheduling Units. If you want to create a blueprint for all of them click “yes”. If you want to create a blue print for a subset click “no” to change your selection.</span> + <div className="p-grid" key={`dlg-msg-head`} style={{marginTop: '10px'}}> + <label className="col-lg-3">ID</label> + <label className="col-lg-9">Name</label> + </div> + {suListWithBlueprint.map((schedulingUnit, index) => ( + <div className="p-grid" key={`dlg-msg-${index}`} style={{marginBottom: "5px"}}> + <span className="col-lg-3">{schedulingUnit.id}</span> + <span className="col-lg-9">{schedulingUnit.name}</span> + </div> + ))} + </div> + } + {suListWithoutBlueprint && suListWithoutBlueprint.length>0 && + <div> + <hr></hr> + <span>Selected Scheduling Unit drafts without blueprint are listed below.</span> + <div className="p-grid" key={`dlg-msg-head`} style={{marginTop: '10px'}}> + <label className="col-lg-3">ID</label> + <label className="col-lg-9">Name</label> + </div> + {suListWithoutBlueprint.map((schedulingUnit, index) => ( + <div className="p-grid" key={`dlg-msg-${index}`} style={{marginBottom: "5px"}}> + <span className="col-lg-3">{schedulingUnit.id}</span> + <span className="col-lg-9">{schedulingUnit.name}</span> + </div> + ))} + {suListWithBlueprint && suListWithBlueprint.length>0 && + <span>If you want to create blueprints for only drafts without blueprints, click 'Create Only New'</span> + } + </div> + } + + </> + ); + } + + /** + * Function to check if blueprint already exist for the selected Scheduling Units and propmt contfirmation dialog. + * When confirmed will create new blueprints for the selected Scheduling Units. + */ + checkAndCreateBlueprint() { + if (this.suList.selectedRows && this.suList.selectedRows.length>0) { + let dialog = this.state.dialog; + dialog.content = this.warningContent; + const schedulingUnitsWithBlueprint = _.filter(this.suList.selectedRows, schedulingUnit=> { return schedulingUnit.scheduling_unit_blueprints.length>0}); + dialog.actions = [ {id:"yes", title: 'Yes', callback: this.createBlueprintTree}, + {id:"no", title: 'No', callback: this.closeDialog} ] + /* Add this action only when both new and old drafts are selected */ + if (schedulingUnitsWithBlueprint.length > 0 && this.suList.selectedRows.length>schedulingUnitsWithBlueprint.length) { + dialog.actions.unshift({id:"newOnly", title: 'Create Only New', callback: this.createBlueprintTreeNewOnly}); + } + this.setState({dialogVisible: true, dialog: dialog, schedulingUnitsWithBlueprint: _.sortBy(schedulingUnitsWithBlueprint,['id'])}); + } else { + this.growl.show({severity: 'info', summary: 'Select Row', detail: 'Please select one or more Scheduling Unit Draft(s)'}); + } + } + + /** + * Callback function from dialog to create blueprints for only new drafts without blueprints. + * @param {Event} event + */ + createBlueprintTreeNewOnly(event){ + this.createBlueprintTree(event, true); + } + + /** + * Function to create actual blueprints for the selected drafts + * @param {Event} event + * @param {Boolean} excludeOld + */ + async createBlueprintTree(event, excludeOld) { + this.setState({dialogVisible: false, showSpinner: true}); + let selectedRows = this.suList.selectedRows; + // Remove old drafts from selected rows + if (excludeOld) { + selectedRows = _.difference(selectedRows, this.state.schedulingUnitsWithBlueprint); + } + for (const schedulingUnit of selectedRows) { + await ScheduleService.createSchedulingUnitBlueprintTree(schedulingUnit.id); + } + this.setState({showSpinner: false, schedulingUnitsWithBlueprint:null}); + this.growl.show({severity: 'success', summary: 'Success', detail: 'Blueprint(s) created successfully!'}); + this.suList.reloadData(); + } + + /** + * Callback function to close the dialog. + */ + closeDialog() { + this.setState({dialogVisible: false}); + } + render() { return ( <> - <TieredMenu className="app-header-menu" model={this.menuOptions} popup ref={el => this.optionsMenu = el} /> - <PageHeader location={this.props.location} title={'Scheduling Unit - List'} + <Growl ref={(el) => this.growl = el} style={{paddingTop:"50px"}} /> + <TieredMenu className="app-header-menu" model={this.menuOptions} popup ref={el => this.optionsMenu = el} /> + <PageHeader location={this.props.location} title={'Scheduling Unit - List'} actions={[ - + {icon:'fa-stamp', title: 'Create Blueprint', type:'button', + actOn:'click', props : { callback: this.checkAndCreateBlueprint}}, {icon: 'fa fa-plus-square', title: 'Add New Scheduling Unit', props: {pathname: '/schedulingunit/create'}}, {icon: 'fa fa-table', title: 'Add Scheduling Set', props: {pathname: '/schedulingset/schedulingunit/create'}}]} /> {this.state.scheduleunit && - <SchedulingUnitList /> } + <SchedulingUnitList allowRowSelection={true} ref={suList => {this.suList = suList}} /> } + {/* Dialog component to show messages and get confirmation */} + <CustomDialog type="confirmation" visible={this.state.dialogVisible} width="40vw" + header={this.state.dialog.header} message={this.state.dialog.detail} content={this.state.dialog.content} + onClose={this.closeDialog} onCancel={this.closeDialog} onSubmit={this.createBlueprintTree} + actions={this.state.dialog.actions}></CustomDialog> + {/* Show spinner during backend API call */} + <CustomPageSpinner visible={this.state.showSpinner} /> </> ); }