Merge "Fix delete flow bug"
authorSébastien Determe <sebastien.determe@intl.att.com>
Fri, 10 Jul 2020 12:05:23 +0000 (12:05 +0000)
committerGerrit Code Review <gerrit@onap.org>
Fri, 10 Jul 2020 12:05:23 +0000 (12:05 +0000)
14 files changed:
docs/images/architecture/distdepl.png
docs/index.rst
pom.xml
src/test/java/org/onap/clamp/clds/it/RobotItCase.java [new file with mode: 0644]
src/test/resources/robotframework/Dockerfile [new file with mode: 0644]
src/test/resources/robotframework/requirements.txt [new file with mode: 0644]
src/test/resources/robotframework/robotframework-test.properties [new file with mode: 0644]
src/test/resources/robotframework/tests/01_healthcheck.robot [new file with mode: 0644]
ui-react/src/LoopUI.js
ui-react/src/__snapshots__/LoopUI.test.js.snap
ui-react/src/__snapshots__/OnapClamp.test.js.snap
ui-react/src/components/dialogs/PerformActions.js
ui-react/src/components/dialogs/PerformActions.test.js
ui-react/src/components/loop_viewer/svg/SvgGenerator.js

index 0016a85..27a3930 100644 (file)
Binary files a/docs/images/architecture/distdepl.png and b/docs/images/architecture/distdepl.png differ
index 31da7f9..c8aafc2 100644 (file)
@@ -38,7 +38,7 @@ CLAMP uses the API's exposed by the following ONAP components:
 - SDC : REST based interface exposed by the SDC, Distribution of service to DCAE
 - DCAE: REST based interface exposed by DCAE, Common Controller Framework, DCAE microservices onboarded (TCA, Stringmatch, Holmes (optional))
 - Policy: REST based interface, Policy engine target both XACML and Drools PDP, Policy Engine trigger operations to App-C/VF-C/SDN-C
-- CDS: REST based interface, to retrieve list of actors/actions at runtime.
+- CDS: REST based interface, to retrieve list of operations/actions with their corresponding payload at runtime for Operational Policies where the field 'actor' is 'CDS'.
 
 Delivery
 --------
diff --git a/pom.xml b/pom.xml
index e8b4ba0..245bae0 100644 (file)
--- a/pom.xml
+++ b/pom.xml
                        <version>2.0.4</version>
                        <scope>test</scope>
                </dependency>
+               <dependency>
+                       <groupId>com.github.docker-java</groupId>
+                       <artifactId>docker-java-core</artifactId>
+                       <version>3.2.1</version>
+                       <scope>test</scope>
+               </dependency>
+               <dependency>
+                       <groupId>com.github.docker-java</groupId>
+                       <artifactId>docker-java</artifactId>
+                       <version>3.2.1</version>
+                       <scope>test</scope>
+               </dependency>
        </dependencies>
 
        <build>
                                                                <portName>docker.mariadb.port.host</portName>
                                                                <portName>docker.http-cache.port.host</portName>
                                                                <portName>clamp.it.tests.http-redirected</portName>
+                                                               <portName>clamp.it.tests.robotframework.http</portName>
                                                                <portName>clamp.it.tests.https</portName>
                                                                <portName>clamp.it.tests.http</portName>
                                                        </portNames>
                                                                <include>**/*ItCase.java</include>
                                                        </includes>
                                                        <forkCount>1C</forkCount>
-                                                       <reuseForks>true</reuseForks>
+                                                       <reuseForks>false</reuseForks>
                                                        <useSystemClassLoader>false</useSystemClassLoader>
                                                        <argLine>${failsafeArgLine}</argLine>
                                                </configuration>
diff --git a/src/test/java/org/onap/clamp/clds/it/RobotItCase.java b/src/test/java/org/onap/clamp/clds/it/RobotItCase.java
new file mode 100644 (file)
index 0000000..b386d9b
--- /dev/null
@@ -0,0 +1,114 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP CLAMP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights
+ *                             reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END============================================
+ * ===================================================================
+ *
+ */
+
+package org.onap.clamp.clds.it;
+
+import com.att.eelf.configuration.EELFLogger;
+import com.att.eelf.configuration.EELFManager;
+import com.github.dockerjava.api.DockerClient;
+import com.github.dockerjava.api.command.BuildImageResultCallback;
+import com.github.dockerjava.api.command.CreateContainerResponse;
+import com.github.dockerjava.api.command.InspectContainerResponse;
+import com.github.dockerjava.api.command.LogContainerCmd;
+import com.github.dockerjava.api.model.AccessMode;
+import com.github.dockerjava.api.model.Bind;
+import com.github.dockerjava.api.model.BuildResponseItem;
+import com.github.dockerjava.api.model.Frame;
+import com.github.dockerjava.api.model.Volume;
+import com.github.dockerjava.core.DockerClientBuilder;
+import com.github.dockerjava.core.command.LogContainerResultCallback;
+import com.github.dockerjava.netty.NettyDockerCmdExecFactory;
+import java.io.File;
+import java.util.Objects;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.TestPropertySource;
+import org.springframework.test.context.junit4.SpringRunner;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
+@TestPropertySource(locations = "classpath:robotframework/robotframework-test.properties")
+public class RobotItCase {
+
+    @Value("${server.port}")
+    private String httpPort;
+    private static final int TIMEOUT_S = 150;
+    protected static final EELFLogger logger = EELFManager.getInstance().getLogger(RobotItCase.class);
+
+    @Test
+    public void robotTests() throws Exception {
+        File robotFolder = new File(getClass().getClassLoader().getResource("robotframework").getFile());
+        Volume testsVolume = new Volume("/opt/robotframework/tests");
+        DockerClient client = DockerClientBuilder
+                .getInstance()
+                .withDockerCmdExecFactory(new NettyDockerCmdExecFactory())
+                .build();
+
+
+        BuildImageResultCallback callback = new BuildImageResultCallback() {
+            @Override
+            public void onNext(BuildResponseItem item) {
+                System.out.println("XXX ITEM " + item);
+                super.onNext(item);
+            }
+        };
+
+        String imageId = client.buildImageCmd(robotFolder).exec(callback).awaitImageId();
+        CreateContainerResponse createContainerResponse = client.createContainerCmd(imageId)
+                .withVolumes(testsVolume)
+                .withBinds(
+                        new Bind(robotFolder.getAbsolutePath() + "/tests/", testsVolume, AccessMode.rw))
+                .withEnv("CLAMP_PORT=" + httpPort)
+                .withStopTimeout(TIMEOUT_S)
+                .withNetworkMode("host")
+                .exec();
+        String id = createContainerResponse.getId();
+        client.startContainerCmd(id).exec();
+        InspectContainerResponse exec;
+
+        int tries = 0;
+        do {
+            Thread.sleep(1000);
+            exec = client.inspectContainerCmd(id).exec();
+            tries++;
+        } while (exec.getState().getRunning() && tries < TIMEOUT_S);
+        Assert.assertEquals(exec.getState().getError(), 0L,
+                Objects.requireNonNull(exec.getState().getExitCodeLong()).longValue());
+        LogContainerCmd logContainerCmd = client.logContainerCmd(id);
+        logContainerCmd.withStdOut(true).withStdErr(true);
+        try {
+            logContainerCmd.exec(new LogContainerResultCallback() {
+                @Override
+                public void onNext(Frame item) {
+                    logger.info(item.toString());
+                }
+            }).awaitCompletion();
+        } catch (InterruptedException e) {
+            throw new Exception("Failed to retrieve logs of container " + id, e);
+        }
+        client.stopContainerCmd(id);
+    }
+}
diff --git a/src/test/resources/robotframework/Dockerfile b/src/test/resources/robotframework/Dockerfile
new file mode 100644 (file)
index 0000000..4ae0820
--- /dev/null
@@ -0,0 +1,9 @@
+#FROM robotframework/rfdocker
+#
+#### Uncomment following two lines if having external test libraries:
+##COPY --chown=robot:robot requirements.txt .
+#RUN pip3 install --no-cache-dir -r requirements.txt
+#COPY *.robot /home/robot/atest
+FROM  ppodgorsek/robot-framework:3.0.3
+COPY requirements.txt .
+RUN pip install -r requirements.txt
\ No newline at end of file
diff --git a/src/test/resources/robotframework/requirements.txt b/src/test/resources/robotframework/requirements.txt
new file mode 100644 (file)
index 0000000..2ae8f45
--- /dev/null
@@ -0,0 +1,6 @@
+certifi
+chardet
+idna
+requests
+urllib3
+robotframework-extendedrequestslibrary
diff --git a/src/test/resources/robotframework/robotframework-test.properties b/src/test/resources/robotframework/robotframework-test.properties
new file mode 100644 (file)
index 0000000..4ec6573
--- /dev/null
@@ -0,0 +1,173 @@
+###
+# ============LICENSE_START=======================================================
+# ONAP CLAMP
+# ================================================================================
+# Copyright (C) 2017-2018 AT&T Intellectual Property. All rights
+#                             reserved.
+# ================================================================================
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# ============LICENSE_END============================================
+# ===================================================================
+#
+###
+
+### Set the port for HTTP or HTTPS protocol (Controlled by Spring framework, only one at a time).
+### (See below for the parameter 'server.http.port' if you want to have both enabled)
+### To have only HTTP, keep the lines server.ssl.* commented
+### To have only HTTPS enabled, uncomment the server.ssl.* lines and specify a right keystore location
+server.port=${clamp.it.tests.robotframework.http}
+### Settings for HTTPS (this automatically enables the HTTPS on the port 'server.port')
+#server.ssl.key-store=file:/tmp/mykey.jks
+#server.ssl.key-store-password=pass
+#server.ssl.key-password=pass
+
+### In order to be user friendly when HTTPS is enabled,
+### you can add another HTTP port that will be automatically redirected to HTTPS
+### by enabling this parameter (server.http.port) and set it to another port (80 or 8080, 8090, etc ...)
+#server.http-to-https-redirection.port=8090
+
+### HTTP Example:
+###--------------
+### server.port=8080
+
+### HTTPS Example:
+### --------------
+### server.port=8443
+### server.ssl.key-store=file:/tmp/mykey.jks
+### server.ssl.key-store-password=mypass
+### server.ssl.key-password=mypass
+
+### HTTP (Redirected to HTTPS) and HTTPS Example:
+### --------------------------------------------
+### server.port=8443           <-- The HTTPS port
+### server.ssl.key-store=file:/tmp/mykey.jks
+### server.ssl.key-store-password=mypass
+### server.ssl.key-password=mypass
+### server.http-to-https-redirection.port=8090  <-- The HTTP port
+
+server.servlet.context-path=/
+#Modified engine-rest applicationpath
+spring.profiles.active=clamp-default,clamp-default-user
+spring.http.converters.preferred-json-mapper=gson
+
+#The max number of active threads in this pool
+server.tomcat.max-threads=200
+#The minimum number of threads always kept alive
+server.tomcat.min-Spare-Threads=25
+#The number of milliseconds before an idle thread shutsdown, unless the number of active threads are less or equal to minSpareThreads
+server.tomcat.max-idle-time=60000
+
+#Servlet context parameters
+server.context_parameters.p-name=value #context parameter with p-name as key and value as value.
+
+camel.springboot.consumer-template-cache-size=1000
+camel.springboot.producer-template-cache-size=1000
+# JMX enabled to have Camel Swagger runtime working
+camel.springboot.jmx-enabled=true
+camel.defaultthreadpool.poolsize=10
+camel.defaultthreadpool.maxpoolsize=20
+camel.defaultthreadpool.maxqueuesize=1000
+camel.defaultthreadpool.keepaliveTime=60
+camel.defaultthreadpool.rejectpolicy=CallerRuns
+#camel.springboot.xmlRoutes = false
+camel.springboot.xmlRoutes=classpath:/clds/camel/routes/*.xml
+camel.springboot.xmlRests=classpath:/clds/camel/rest/*.xml
+#camel.springboot.typeConversion = false
+
+#clds datasource connection details
+spring.datasource.driverClassName=org.mariadb.jdbc.Driver
+spring.datasource.url=jdbc:mariadb:sequential://localhost:3306,localhost:${docker.mariadb.port.host}/cldsdb4?autoReconnect=true&connectTimeout=10000&socketTimeout=10000&retriesAllDown=3
+spring.datasource.username=clds
+spring.datasource.password=sidnnd83K
+spring.datasource.validationQuery=SELECT 1
+spring.datasource.validationQueryTimeout=20000
+spring.datasource.validationInterval=30000
+spring.datasource.testWhileIdle = true
+spring.datasource.minIdle = 0
+spring.datasource.initialSize=0
+# Automatically test whether a connection provided is good or not
+spring.datasource.testOnBorrow=true
+spring.datasource.ignoreExceptionOnPreLoad=true
+
+spring.jpa.properties.javax.persistence.schema-generation.database.action=none
+#spring.jpa.properties.javax.persistence.schema-generation.create-source=metadata
+#spring.jpa.properties.javax.persistence.schema-generation.scripts.action=create
+#spring.jpa.properties.javax.persistence.schema-generation.scripts.create-target=create.sql
+# disable Hibernate DDL generation because the schema will be generated from a sql script
+spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
+spring.jpa.properties.hibernate.ddl-auto=validate
+spring.jpa.properties.hibernate.hbm2ddl.delimiter=;
+spring.jpa.properties.hibernate.format_sql=true
+spring.jpa.properties.hibernate.use-new-id-generator-mappings=true
+
+# Whether to enable logging of SQL statements.
+#spring.jpa.show-sql=true
+
+#Async Executor default Parameters
+async.core.pool.size=10
+async.max.pool.size=20
+async.queue.capacity=500
+
+#For EELF logback file
+#com.att.eelf.logging.path=
+clamp.config.logback.filename=logback-default.xml
+#The log folder that will be used in logback.xml file
+clamp.config.log.path=log
+clamp.config.files.systemProperties=classpath:/system.properties
+clamp.config.files.cldsUsers=classpath:/clds/clds-users.json
+clamp.config.files.globalProperties=classpath:/clds/templates/globalProperties.json
+clamp.config.files.sdcController=classpath:/clds/sdc-controllers-config.json
+
+#
+# Configuration Settings for Policy Engine Components
+clamp.config.policy.api.url=http4://localhost:${docker.http-cache.port.host}
+clamp.config.policy.api.userName=healthcheck
+clamp.config.policy.api.password=zb!XztG34
+clamp.config.policy.pap.url=http4://localhost:${docker.http-cache.port.host}
+clamp.config.policy.pap.userName=healthcheck
+clamp.config.policy.pap.password=zb!XztG34
+
+# Sdc service properties
+#
+clamp.config.sdc.csarFolder = ${project.build.directory}/sdc-tests
+
+#DCAE Inventory Url Properties
+clamp.config.dcae.inventory.url=http4://localhost:${docker.http-cache.port.host}
+clamp.config.dcae.intentory.retry.interval=100
+clamp.config.dcae.intentory.retry.limit=1
+
+#DCAE Deployment Url Properties
+clamp.config.dcae.deployment.url=http4://localhost:${docker.http-cache.port.host}
+clamp.config.dcae.deployment.userName=test
+clamp.config.dcae.deployment.password=test
+
+#Define user permission related parameters, the permission type can be changed but MUST be redefined in clds-users.properties in that case !
+clamp.config.security.permission.type.cl=permission-type-cl
+clamp.config.security.permission.type.cl.manage=permission-type-cl-manage
+clamp.config.security.permission.type.cl.event=permission-type-cl-event
+clamp.config.security.permission.type.filter.vf=permission-type-filter-vf
+clamp.config.security.permission.type.template=permission-type-template
+clamp.config.security.permission.type.tosca=permission-type-tosca
+#This one indicates the type of instances (dev|prod|perf...), this must be set accordingly in clds-users.properties
+clamp.config.security.permission.instance=dev
+clamp.config.security.authentication.class=org.onap.aaf.cadi.principal.X509Principal
+
+# Configuration settings for CDS
+clamp.config.cds.url=http4://localhost:${docker.http-cache.port.host}
+clamp.config.cds.userName=ccsdkapps
+clamp.config.cds.password=ccsdkapps
+
+## Tosca converter
+clamp.config.tosca.converter.json.schema.templates=classpath:/clds/tosca-converter/templates.json
+clamp.config.tosca.converter.default.datatypes=classpath:/clds/tosca-converter/default-tosca-types.yaml
+clamp.config.tosca.converter.dictionary.support.enabled=true
\ No newline at end of file
diff --git a/src/test/resources/robotframework/tests/01_healthcheck.robot b/src/test/resources/robotframework/tests/01_healthcheck.robot
new file mode 100644 (file)
index 0000000..f192667
--- /dev/null
@@ -0,0 +1,19 @@
+*** Settings ***
+Library     Collections
+Library     RequestsLibrary
+Library     OperatingSystem
+Library     json
+Library     OperatingSystem
+*** Variables ***
+${login}                     admin
+${passw}                     password
+*** Keywords ***
+Create the sessions
+*** Test Cases ***
+Get Requests health check ok
+    ${port} =    Get Environment Variable   CLAMP_PORT
+    ${auth}=    Create List     ${login}    ${passw}
+    Create Session   clamp  http://localhost:${port}   auth=${auth}   disable_warnings=1
+    Set Global Variable     ${clamp_session}      clamp
+    ${resp}=    Get Request    ${clamp_session}   /restservices/clds/v1/healthcheck
+    Should Be Equal As Strings  ${resp.status_code}     200
\ No newline at end of file
index 8624726..0ee6e6e 100644 (file)
@@ -52,6 +52,7 @@ import PerformAction from './components/dialogs/PerformActions';
 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 @@ const StyledMainDiv = styled.div`
        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 @@ export default class LoopUI extends React.Component {
                loopName: OnapConstants.defaultLoopName,
                loopCache: new LoopCache({}),
                showSucAlert: false,
-               showFailAlert: false
+               showFailAlert: false,
+               busyLoadingCount: 0
        };
 
        constructor() {
@@ -120,6 +127,9 @@ export default class LoopUI extends React.Component {
                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 @@ export default class LoopUI extends React.Component {
        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 @@ export default class LoopUI extends React.Component {
        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()}
index 2dfa480..cae9182 100644 (file)
@@ -4,6 +4,7 @@ exports[`Verify LoopUI Test the render method 1`] = `
 <styled.div
   id="main_div"
 >
+  <GlobalStyleComponent />
   <Route
     path="/uploadToscaPolicyModal"
     render={[Function]}
@@ -76,7 +77,7 @@ exports[`Verify LoopUI Test the render method 1`] = `
     path="/refreshStatus"
     render={[Function]}
   />
-  <GlobalStyleComponent />
+  <div />
   <div>
     <Alert
       closeLabel="Close alert"
@@ -166,6 +167,7 @@ exports[`Verify LoopUI Test the render method 1`] = `
       <withRouter(SvgGenerator)
         clickable={true}
         generatedFrom="INSTANCE"
+        isBusyLoading={[Function]}
         loopCache={
           LoopCache {
             "loopJsonCache": Object {},
index 56d022f..d4573b3 100644 (file)
@@ -31,6 +31,7 @@ exports[`Verify OnapClamp Test the render method 1`] = `
   <styled.div
     id="main_div"
   >
+    <GlobalStyleComponent />
     <Route
       path="/uploadToscaPolicyModal"
       render={[Function]}
@@ -103,7 +104,7 @@ exports[`Verify OnapClamp Test the render method 1`] = `
       path="/refreshStatus"
       render={[Function]}
     />
-    <GlobalStyleComponent />
+    <div />
     <div>
       <Alert
         closeLabel="Close alert"
@@ -191,6 +192,7 @@ exports[`Verify OnapClamp Test the render method 1`] = `
         <withRouter(SvgGenerator)
           clickable={true}
           generatedFrom="INSTANCE"
+          isBusyLoading={[Function]}
           loopCache={
             LoopCache {
               "loopJsonCache": Object {},
index cf5a3c2..f6001e2 100644 (file)
  */
 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 @@ export default class PerformActions extends React.Component {
                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;
        }
 }
index b833a92..c91c2f6 100644 (file)
@@ -38,6 +38,8 @@ describe('Verify PerformActions', () => {
                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 @@ describe('Verify PerformActions', () => {
                        });
                });
                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 @@ describe('Verify PerformActions', () => {
                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 @@ describe('Verify PerformActions', () => {
                        });
                });
                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();
 
index d718c2e..7070455 100644 (file)
@@ -70,13 +70,15 @@ class SvgGenerator extends React.Component {
        }
 
        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);
+                       }
                }
        }