add framework for blocking user interaction

this adds two new methods to LoopUI.js, setBusyLoading and
clearBusyLoading, and one new state variable, busyLoadingCount, for
supporting the blocking of user clicking during async back end calls
that might take a bit of time to return. Blocking the user from clicking
on a component box is implemented as an important first case use, as
well as all PerformAction calls.

Issue-ID: CLAMP-894
Change-Id: I28660afe26b6cc8184b9392aee42157f44601bf6
Signed-off-by: Ted Humphrey <Thomas.Humphrey@att.com>
diff --git a/ui-react/src/LoopUI.js b/ui-react/src/LoopUI.js
index 8624726..0ee6e6e 100644
--- a/ui-react/src/LoopUI.js
+++ b/ui-react/src/LoopUI.js
@@ -52,6 +52,7 @@
 import RefreshStatus from './components/dialogs/RefreshStatus';
 import DeployLoopModal from './components/dialogs/Loop/DeployLoopModal';
 import Alert from 'react-bootstrap/Alert';
+import Spinner from 'react-bootstrap/Spinner';
 
 import { Link } from 'react-router-dom';
 
@@ -59,6 +60,11 @@
 	background-color: ${props => props.theme.backgroundColor};
 `
 
+const StyledSpinnerDiv = styled.div`
+	justify-content: center !important;
+	display: flex !important;
+`;
+
 const ProjectNameStyled = styled.a`
 	vertical-align: middle;
 	padding-left: 30px;
@@ -108,7 +114,8 @@
 		loopName: OnapConstants.defaultLoopName,
 		loopCache: new LoopCache({}),
 		showSucAlert: false,
-		showFailAlert: false
+		showFailAlert: false,
+		busyLoadingCount: 0
 	};
 
 	constructor() {
@@ -120,6 +127,9 @@
 		this.showSucAlert =  this.showSucAlert.bind(this);
 		this.showFailAlert =  this.showFailAlert.bind(this);
 		this.disableAlert =  this.disableAlert.bind(this);
+		this.setBusyLoading = this.setBusyLoading.bind(this);
+		this.clearBusyLoading = this.clearBusyLoading.bind(this);
+		this.isBusyLoading = this.isBusyLoading.bind(this);
 	}
 
 	componentWillMount() {
@@ -191,7 +201,7 @@
 	renderLoopViewBody() {
 		return (
 			<LoopViewBodyDivStyled>
-				<SvgGenerator loopCache={this.state.loopCache} clickable={true} generatedFrom={SvgGenerator.GENERATED_FROM_INSTANCE}/>
+				<SvgGenerator loopCache={this.state.loopCache} clickable={true} generatedFrom={SvgGenerator.GENERATED_FROM_INSTANCE} isBusyLoading={this.isBusyLoading}/>
 				<LoopStatus loopCache={this.state.loopCache}/>
 				<LoopLogs loopCache={this.state.loopCache} />
 			</LoopViewBodyDivStyled>
@@ -225,53 +235,160 @@
 	showFailAlert(message) {
 		this.setState ({ showFailAlert: true, showMessage:message });
 	}
- 
+
 	disableAlert() {
 		this.setState ({ showSucAlert: false, showFailAlert: false });
 	}
 
 	loadLoop(loopName) {
+		this.setBusyLoading();
 		LoopService.getLoop(loopName).then(loop => {
 			console.debug("Updating loopCache");
 			LoopActionService.refreshStatus(loopName).then(data => {
 				this.updateLoopCache(data);
+				this.clearBusyLoading();
 				this.props.history.push('/');
 			})
 			.catch(error => {
 				this.updateLoopCache(loop);
+				this.clearBusyLoading();
 				this.props.history.push('/');
 			});
 		});
 	}
 
+	setBusyLoading() {
+		this.setState((state,props) => ({ busyLoadingCount: ++state.busyLoadingCount }));
+	}
+
+	clearBusyLoading() {
+		this.setState((state,props) => ({ busyLoadingCount: --state.busyLoadingCount }));
+	}
+
+	isBusyLoading() {
+		if (this.state.busyLoadingCount === 0) {
+			return false;
+		} else {
+			return true;
+		}
+	}
+
 	closeLoop() {
 		this.setState({ loopCache: new LoopCache({}), loopName: OnapConstants.defaultLoopName });
 		this.props.history.push('/');
 	}
 
-	render() {
-		return (
-				<StyledMainDiv id="main_div">
+	renderRoutes() {
+		return(
+			<React.Fragment>
 				<Route path="/uploadToscaPolicyModal" render={(routeProps) => (<UploadToscaPolicyModal {...routeProps} />)} />
 				<Route path="/viewToscaPolicyModal" render={(routeProps) => (<ViewToscaPolicyModal {...routeProps} />)} />
 				<Route path="/ViewLoopTemplatesModal" render={(routeProps) => (<ViewLoopTemplatesModal {...routeProps} />)} />
 				<Route path="/ManageDictionaries" render={(routeProps) => (<ManageDictionaries {...routeProps} />)} />
-				<Route path="/policyModal/:policyInstanceType/:policyName" render={(routeProps) => (<PolicyModal {...routeProps} loopCache={this.getLoopCache()} loadLoopFunction={this.loadLoop}/>)} />
-				<Route path="/createLoop" render={(routeProps) => (<CreateLoopModal {...routeProps} loadLoopFunction={this.loadLoop} />)} />
-				<Route path="/openLoop" render={(routeProps) => (<OpenLoopModal {...routeProps} loadLoopFunction={this.loadLoop} />)} />
-				<Route path="/loopProperties" render={(routeProps) => (<LoopPropertiesModal {...routeProps} loopCache={this.getLoopCache()} loadLoopFunction={this.loadLoop}/>)} />
-				<Route path="/modifyLoop" render={(routeProps) => (<ModifyLoopModal {...routeProps} loopCache={this.getLoopCache()} loadLoopFunction={this.loadLoop}/>)} />
+
+				<Route path="/policyModal/:policyInstanceType/:policyName" render={(routeProps) => (<PolicyModal {...routeProps}
+					loopCache={this.getLoopCache()}
+					loadLoopFunction={this.loadLoop}/>)}
+				/>
+				<Route path="/createLoop" render={(routeProps) => (<CreateLoopModal {...routeProps}
+					loadLoopFunction={this.loadLoop} />)}
+				/>
+				<Route path="/openLoop" render={(routeProps) => (<OpenLoopModal {...routeProps}
+					loadLoopFunction={this.loadLoop} />)}
+				/>
+				<Route path="/loopProperties" render={(routeProps) => (<LoopPropertiesModal {...routeProps}
+					loopCache={this.getLoopCache()}
+					loadLoopFunction={this.loadLoop}/>)}
+				/>
+				<Route path="/modifyLoop" render={(routeProps) => (<ModifyLoopModal {...routeProps}
+					loopCache={this.getLoopCache()}
+					loadLoopFunction={this.loadLoop}/>)}
+				/>
 
 				<Route path="/userInfo" render={(routeProps) => (<UserInfoModal {...routeProps} />)} />
 				<Route path="/closeLoop" render={this.closeLoop} />
-				<Route path="/submit" render={(routeProps) => (<PerformAction {...routeProps} loopAction="submit" loopCache={this.getLoopCache()} updateLoopFunction={this.updateLoopCache} showSucAlert={this.showSucAlert} showFailAlert={this.showFailAlert}/>)} />
-				<Route path="/stop" render={(routeProps) => (<PerformAction {...routeProps} loopAction="stop" loopCache={this.getLoopCache()} updateLoopFunction={this.updateLoopCache} showSucAlert={this.showSucAlert} showFailAlert={this.showFailAlert}/>)} />
-				<Route path="/restart" render={(routeProps) => (<PerformAction {...routeProps} loopAction="restart" loopCache={this.getLoopCache()} updateLoopFunction={this.updateLoopCache} showSucAlert={this.showSucAlert} showFailAlert={this.showFailAlert}/>)} />
-				<Route path="/delete" render={(routeProps) => (<PerformAction {...routeProps} loopAction="delete" loopCache={this.getLoopCache()} updateLoopFunction={this.updateLoopCache} showSucAlert={this.showSucAlert} showFailAlert={this.showFailAlert}/>)} />
-				<Route path="/undeploy" render={(routeProps) => (<PerformAction {...routeProps} loopAction="undeploy" loopCache={this.getLoopCache()} updateLoopFunction={this.updateLoopCache} showSucAlert={this.showSucAlert} showFailAlert={this.showFailAlert}/>)} />
-				<Route path="/deploy" render={(routeProps) => (<DeployLoopModal {...routeProps} loopCache={this.getLoopCache()} updateLoopFunction={this.updateLoopCache} showSucAlert={this.showSucAlert} showFailAlert={this.showFailAlert}/>)} />
-				<Route path="/refreshStatus" render={(routeProps) => (<RefreshStatus {...routeProps} loopCache={this.getLoopCache()} updateLoopFunction={this.updateLoopCache} showSucAlert={this.showSucAlert} showFailAlert={this.showFailAlert}/>)} />
-				<GlobalClampStyle />
+
+				<Route path="/submit" render={(routeProps) => (<PerformAction {...routeProps}
+					loopAction="submit"
+					loopCache={this.getLoopCache()}
+					updateLoopFunction={this.updateLoopCache}
+					showSucAlert={this.showSucAlert}
+					showFailAlert={this.showFailAlert}
+					setBusyLoading={this.setBusyLoading}
+					clearBusyLoading={this.clearBusyLoading}/>)}
+				/>
+				<Route path="/stop" render={(routeProps) => (<PerformAction {...routeProps}
+					loopAction="stop"
+					loopCache={this.getLoopCache()}
+					updateLoopFunction={this.updateLoopCache}
+					showSucAlert={this.showSucAlert}
+					showFailAlert={this.showFailAlert}
+					setBusyLoading={this.setBusyLoading}
+					clearBusyLoading={this.clearBusyLoading}/>)}
+				/>
+				<Route path="/restart" render={(routeProps) => (<PerformAction {...routeProps}
+					loopAction="restart"
+					loopCache={this.getLoopCache()}
+					updateLoopFunction={this.updateLoopCache}
+					showSucAlert={this.showSucAlert}
+					showFailAlert={this.showFailAlert}
+					setBusyLoading={this.setBusyLoading}
+					clearBusyLoading={this.clearBusyLoading}/>)}
+				/>
+				<Route path="/delete" render={(routeProps) => (<PerformAction {...routeProps}
+					loopAction="delete"
+					loopCache={this.getLoopCache()}
+					updateLoopFunction={this.updateLoopCache}
+					showSucAlert={this.showSucAlert}
+					showFailAlert={this.showFailAlert}
+					setBusyLoading={this.setBusyLoading}
+					clearBusyLoading={this.clearBusyLoading}/>)}
+				/>
+				<Route path="/undeploy" render={(routeProps) => (<PerformAction {...routeProps}
+					loopAction="undeploy"
+					loopCache={this.getLoopCache()}
+					updateLoopFunction={this.updateLoopCache}
+					showSucAlert={this.showSucAlert}
+					showFailAlert={this.showFailAlert}
+					setBusyLoading={this.setBusyLoading}
+					clearBusyLoading={this.clearBusyLoading}/>)}
+				/>
+				<Route path="/deploy" render={(routeProps) => (<DeployLoopModal {...routeProps}
+					loopCache={this.getLoopCache()}
+					updateLoopFunction={this.updateLoopCache}
+					showSucAlert={this.showSucAlert}
+					showFailAlert={this.showFailAlert}/>)}
+				/>
+				<Route path="/refreshStatus" render={(routeProps) => (<RefreshStatus {...routeProps}
+					loopCache={this.getLoopCache()}
+					updateLoopFunction={this.updateLoopCache}
+					showSucAlert={this.showSucAlert}
+					showFailAlert={this.showFailAlert}/>)}
+				/>
+			</React.Fragment>
+		);
+	}
+
+	renderSpinner() {
+		if (this.isBusyLoading()) {
+			return (
+				<StyledSpinnerDiv>
+					<Spinner animation="border" role="status">
+						<span className="sr-only">Loading...</span>
+					</Spinner>
+				</StyledSpinnerDiv>
+			);
+		} else {
+			return (<div></div>);
+		}
+	}
+
+	render() {
+		return (
+				<StyledMainDiv id="main_div">
+					<GlobalClampStyle />
+					{this.renderRoutes()}
+					{this.renderSpinner()}
 					{this.renderAlertBar()}
 					{this.renderNavBar()}
 					{this.renderLoopViewer()}
diff --git a/ui-react/src/__snapshots__/LoopUI.test.js.snap b/ui-react/src/__snapshots__/LoopUI.test.js.snap
index 2dfa480..cae9182 100644
--- a/ui-react/src/__snapshots__/LoopUI.test.js.snap
+++ b/ui-react/src/__snapshots__/LoopUI.test.js.snap
@@ -4,6 +4,7 @@
 <styled.div
   id="main_div"
 >
+  <GlobalStyleComponent />
   <Route
     path="/uploadToscaPolicyModal"
     render={[Function]}
@@ -76,7 +77,7 @@
     path="/refreshStatus"
     render={[Function]}
   />
-  <GlobalStyleComponent />
+  <div />
   <div>
     <Alert
       closeLabel="Close alert"
@@ -166,6 +167,7 @@
       <withRouter(SvgGenerator)
         clickable={true}
         generatedFrom="INSTANCE"
+        isBusyLoading={[Function]}
         loopCache={
           LoopCache {
             "loopJsonCache": Object {},
diff --git a/ui-react/src/__snapshots__/OnapClamp.test.js.snap b/ui-react/src/__snapshots__/OnapClamp.test.js.snap
index 56d022f..d4573b3 100644
--- a/ui-react/src/__snapshots__/OnapClamp.test.js.snap
+++ b/ui-react/src/__snapshots__/OnapClamp.test.js.snap
@@ -31,6 +31,7 @@
   <styled.div
     id="main_div"
   >
+    <GlobalStyleComponent />
     <Route
       path="/uploadToscaPolicyModal"
       render={[Function]}
@@ -103,7 +104,7 @@
       path="/refreshStatus"
       render={[Function]}
     />
-    <GlobalStyleComponent />
+    <div />
     <div>
       <Alert
         closeLabel="Close alert"
@@ -191,6 +192,7 @@
         <withRouter(SvgGenerator)
           clickable={true}
           generatedFrom="INSTANCE"
+          isBusyLoading={[Function]}
           loopCache={
             LoopCache {
               "loopJsonCache": Object {},
diff --git a/ui-react/src/components/dialogs/PerformActions.js b/ui-react/src/components/dialogs/PerformActions.js
index cf5a3c20..f6001e2 100644
--- a/ui-react/src/components/dialogs/PerformActions.js
+++ b/ui-react/src/components/dialogs/PerformActions.js
@@ -22,24 +22,19 @@
  */
 import React from 'react';
 import LoopActionService from '../../api/LoopActionService';
-import Spinner from 'react-bootstrap/Spinner'
-import styled from 'styled-components';
 
-const StyledSpinnerDiv = styled.div`
-	justify-content: center !important;
-	display: flex !important;
-`;
 
 export default class PerformActions extends React.Component {
 	state = {
 		loopName: this.props.loopCache.getLoopName(),
 		loopAction: this.props.loopAction
 	};
+
 	constructor(props, context) {
 		super(props, context);
-
 		this.refreshStatus = this.refreshStatus.bind(this);
 	}
+
 	componentWillReceiveProps(newProps) {
 		this.setState({
 			loopName: newProps.loopCache.getLoopName(),
@@ -51,35 +46,50 @@
 		const action = this.state.loopAction;
 		const loopName = this.state.loopName;
 
-		LoopActionService.performAction(loopName, action).then(pars => {
+		if (action === 'delete') {
+			if (window.confirm('You are about to remove Control Loop Model "' + loopName +
+					'". Select OK to continue with deletion or Cancel to keep the model.') === false) {
+				return;
+			}
+		}
+
+		this.props.setBusyLoading(); // Alert top level to start block user clicks
+
+		LoopActionService.performAction(loopName, action)
+		.then(pars => {
 			this.props.showSucAlert("Action " + action + " successfully performed");
-			// refresh status and update loop logs
-			this.refreshStatus(loopName);
+			if (action === 'delete') {
+				this.props.updateLoopFunction(null);
+				this.props.history.push('/');
+			} else {
+				// refresh status and update loop logs
+				this.refreshStatus(loopName);
+			}
 		})
 		.catch(error => {
 			this.props.showFailAlert("Action " + action + " failed");
 			// refresh status and update loop logs
 			this.refreshStatus(loopName);
-		});
-
+		})
+		.finally(() => this.props.clearBusyLoading());
 	}
 
 	refreshStatus(loopName) {
-		LoopActionService.refreshStatus(loopName).then(data => {
+
+		this.props.setBusyLoading();
+
+		LoopActionService.refreshStatus(loopName)
+		.then(data => {
 			this.props.updateLoopFunction(data);
 			this.props.history.push('/');
 		})
-			.catch(error => {
+		.catch(error => {
 			this.props.history.push('/');
-		});
+		})
+		.finally(() => this.props.clearBusyLoading());
 	}
 
 	render() {
-		return (
-			<StyledSpinnerDiv>
-				<Spinner animation="border" role="status">
-				</Spinner>
-			</StyledSpinnerDiv>
-		);
+		return null;
 	}
 }
diff --git a/ui-react/src/components/dialogs/PerformActions.test.js b/ui-react/src/components/dialogs/PerformActions.test.js
index b833a92..c91c2f6 100644
--- a/ui-react/src/components/dialogs/PerformActions.test.js
+++ b/ui-react/src/components/dialogs/PerformActions.test.js
@@ -38,6 +38,8 @@
 		const updateLoopFunction = jest.fn();
 		const showSucAlert = jest.fn();
 		const showFailAlert = jest.fn();
+		const setBusyLoading = jest.fn();
+		const clearBusyLoading = jest.fn();
 		
 		LoopActionService.refreshStatus = jest.fn().mockImplementation(() => {
 			return Promise.resolve({
@@ -47,7 +49,7 @@
 			});
 		});
 		const component = shallow(<PerformActions loopCache={loopCache} 
-					loopAction="submit" history={historyMock} updateLoopFunction={updateLoopFunction} showSucAlert={showSucAlert} showFailAlert={showFailAlert} />)
+					loopAction="submit" history={historyMock} updateLoopFunction={updateLoopFunction} showSucAlert={showSucAlert} showFailAlert={showFailAlert} setBusyLoading={setBusyLoading} clearBusyLoading={clearBusyLoading}/>)
 		await flushPromises();
 		component.update();
 
@@ -60,6 +62,8 @@
 		const updateLoopFunction = jest.fn();
 		const showSucAlert = jest.fn();
 		const showFailAlert = jest.fn();
+		const setBusyLoading = jest.fn();
+		const clearBusyLoading = jest.fn();
 
 		LoopActionService.performAction = jest.fn().mockImplementation(() => {
 			return Promise.resolve({
@@ -76,7 +80,7 @@
 			});
 		});
 		const component = shallow(<PerformActions loopCache={loopCache} 
-						loopAction="submit" history={historyMock} updateLoopFunction={updateLoopFunction} showSucAlert={showSucAlert} showFailAlert={showFailAlert} />)
+						loopAction="submit" history={historyMock} updateLoopFunction={updateLoopFunction} showSucAlert={showSucAlert} showFailAlert={showFailAlert} setBusyLoading={setBusyLoading} clearBusyLoading={clearBusyLoading}/>)
 		await flushPromises();
 		component.update();
 
diff --git a/ui-react/src/components/loop_viewer/svg/SvgGenerator.js b/ui-react/src/components/loop_viewer/svg/SvgGenerator.js
index d718c2e..7070455 100644
--- a/ui-react/src/components/loop_viewer/svg/SvgGenerator.js
+++ b/ui-react/src/components/loop_viewer/svg/SvgGenerator.js
@@ -70,13 +70,15 @@
 	}
 
 	handleSvgClick(event) {
-	    if (this.state.clickable) {
-            console.debug("svg click event received");
-            var elementName = event.target.parentNode.getAttribute('policyId');
-            console.info("SVG element clicked", elementName);
-            if (elementName !== null) {
-                this.props.history.push("/policyModal/"+event.target.parentNode.getAttribute('policyType')+"/"+elementName);
-            }
+		console.debug("svg click event received");
+		if (this.state.clickable) {
+			var elementName = event.target.parentNode.getAttribute('policyId');
+			console.info("SVG element clicked", elementName);
+			// Only allow movement to policy editing IF there busyLoadingCOunt is 0,
+			// meaning we are not waiting for refreshStatus to complete, for example
+			if (elementName !== null && !this.props.isBusyLoading()) {
+				this.props.history.push("/policyModal/"+event.target.parentNode.getAttribute('policyType')+"/"+elementName);
+			}
 		}
 	}