diff options
-rw-r--r-- | pom.xml | 15 | ||||
-rw-r--r-- | src/main/resources/clds/camel/rest/clamp-api-v2.xml | 18 | ||||
-rw-r--r-- | src/main/resources/clds/camel/routes/dcae-flows.xml | 8 | ||||
-rw-r--r-- | src/test/java/org/onap/clamp/clds/it/RobotItCase.java | 114 | ||||
-rw-r--r-- | src/test/java/org/onap/clamp/loop/DeployFlowTestItCase.java | 3 | ||||
-rw-r--r-- | src/test/resources/robotframework/Dockerfile | 9 | ||||
-rw-r--r-- | src/test/resources/robotframework/requirements.txt | 6 | ||||
-rw-r--r-- | src/test/resources/robotframework/robotframework-test.properties | 173 | ||||
-rw-r--r-- | src/test/resources/robotframework/tests/01_healthcheck.robot | 19 | ||||
-rw-r--r-- | ui-react/src/LoopUI.js | 155 | ||||
-rw-r--r-- | ui-react/src/__snapshots__/LoopUI.test.js.snap | 4 | ||||
-rw-r--r-- | ui-react/src/__snapshots__/OnapClamp.test.js.snap | 4 | ||||
-rw-r--r-- | ui-react/src/components/dialogs/PerformActions.js | 52 | ||||
-rw-r--r-- | ui-react/src/components/dialogs/PerformActions.test.js | 8 | ||||
-rw-r--r-- | ui-react/src/components/loop_viewer/svg/SvgGenerator.js | 16 |
15 files changed, 548 insertions, 56 deletions
@@ -547,6 +547,18 @@ <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> @@ -850,6 +862,7 @@ <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> @@ -902,7 +915,7 @@ <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/main/resources/clds/camel/rest/clamp-api-v2.xml b/src/main/resources/clds/camel/rest/clamp-api-v2.xml index 99e92f5be..505ea30d9 100644 --- a/src/main/resources/clds/camel/rest/clamp-api-v2.xml +++ b/src/main/resources/clds/camel/rest/clamp-api-v2.xml @@ -200,6 +200,9 @@ <doTry> <log loggingLevel="INFO" message="DCAE DEPLOY request for loop: ${header.loopName}" /> + <setProperty propertyName="raiseHttpExceptionFlag"> + <simple resultType="java.lang.Boolean">true</simple> + </setProperty> <to uri="bean:org.onap.clamp.flow.log.FlowLogOperation?method=startLog(*, 'DCAE DEPLOY request')" /> <to @@ -330,6 +333,9 @@ <doTry> <log loggingLevel="INFO" message="DCAE UNDEPLOY request for loop: ${header.loopName}" /> + <setProperty propertyName="raiseHttpExceptionFlag"> + <simple resultType="java.lang.Boolean">true</simple> + </setProperty> <to uri="bean:org.onap.clamp.flow.log.FlowLogOperation?method=startLog(*, 'DCAE UNDEPLOY request')" /> <to @@ -373,6 +379,9 @@ <doTry> <log loggingLevel="INFO" message="STOP request for loop: ${header.loopName}" /> + <setProperty propertyName="raiseHttpExceptionFlag"> + <simple resultType="java.lang.Boolean">true</simple> + </setProperty> <to uri="bean:org.onap.clamp.flow.log.FlowLogOperation?method=startLog(*,'STOP request')" /> <to @@ -416,6 +425,9 @@ <doTry> <log loggingLevel="INFO" message="RESTART request for loop: ${header.loopName}" /> + <setProperty propertyName="raiseHttpExceptionFlag"> + <simple resultType="java.lang.Boolean">true</simple> + </setProperty> <to uri="bean:org.onap.clamp.flow.log.FlowLogOperation?method=startLog(*,'RESTART request')" /> <to @@ -460,6 +472,9 @@ <doTry> <log loggingLevel="INFO" message="POLICY SUBMIT request for loop: ${header.loopName}" /> + <setProperty propertyName="raiseHttpExceptionFlag"> + <simple resultType="java.lang.Boolean">true</simple> + </setProperty> <to uri="bean:org.onap.clamp.flow.log.FlowLogOperation?method=startLog(*, 'POLICY SUBMIT request')" /> <to @@ -544,6 +559,9 @@ <doTry> <log loggingLevel="INFO" message="DELETE request for loop: ${header.loopName}" /> + <setProperty propertyName="raiseHttpExceptionFlag"> + <simple resultType="java.lang.Boolean">true</simple> + </setProperty> <to uri="bean:org.onap.clamp.flow.log.FlowLogOperation?method=startLog(*,'DELETE request')" /> <to diff --git a/src/main/resources/clds/camel/routes/dcae-flows.xml b/src/main/resources/clds/camel/routes/dcae-flows.xml index e36f28abf..d71db1763 100644 --- a/src/main/resources/clds/camel/routes/dcae-flows.xml +++ b/src/main/resources/clds/camel/routes/dcae-flows.xml @@ -239,7 +239,7 @@ method="getStatusUrl(${exchangeProperty[dcaeResponse]})" /> </setProperty> <to - uri="bean:org.onap.clamp.policy.microservice.MicroServicePolicyService?method=updateDcaeDeploymentFields(${exchangeProperty[microServicePolicy]},${exchangeProperty[microServicePolicy].getDcaeDeploymentId()},${exchangeProperty[dcaeStatusUrl]})" /> + uri="bean:org.onap.clamp.policy.microservice.MicroServicePolicyService?method=updateDcaeDeploymentFields(${exchangeProperty[microServicePolicy]},null,${exchangeProperty[dcaeStatusUrl]})" /> <to uri="bean:org.onap.clamp.loop.log.LoopLogService?method=addLogForComponent('Undeploy for the micro service: ${exchangeProperty[microServicePolicy].getName()} - ${header.CamelHttpResponseCode} : ${header.CamelHttpResponseText}','INFO','DCAE',${exchangeProperty[loopObject]})" /> </when> @@ -322,7 +322,7 @@ method="getStatusUrl(${exchangeProperty[dcaeResponse]})" /> </setProperty> <to - uri="bean:org.onap.clamp.loop.LoopService?method=updateDcaeDeploymentFields(${exchangeProperty[loopObject]},${exchangeProperty[loopObject].getDcaeDeploymentId()},${exchangeProperty[dcaeStatusUrl]})" /> + uri="bean:org.onap.clamp.loop.LoopService?method=updateDcaeDeploymentFields(${exchangeProperty[loopObject]},null,${exchangeProperty[dcaeStatusUrl]})" /> <doFinally> <to uri="direct:reset-raise-http-exception-flag" /> <to @@ -378,9 +378,9 @@ </handled> <log loggingLevel="ERROR" - message="GET policy request FAILED for loop: ${header.loopName}, ${exception.stacktrace}" /> + message="GET DCAE deployment request FAILED for loop: ${header.loopName}, ${exception.stacktrace}" /> <to - uri="bean:org.onap.clamp.loop.log.LoopLogService?method=addLog('GET policy request failed, Error reported: ${exception.message}','ERROR',${exchangeProperty[loopObject]})" /> + uri="bean:org.onap.clamp.loop.log.LoopLogService?method=addLog('GET DCAE deployment request failed, Error reported: ${exception.message}','ERROR',${exchangeProperty[loopObject]})" /> </doCatch> <doFinally> <to uri="direct:reset-raise-http-exception-flag" /> 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 index 000000000..b386d9bb8 --- /dev/null +++ b/src/test/java/org/onap/clamp/clds/it/RobotItCase.java @@ -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/java/org/onap/clamp/loop/DeployFlowTestItCase.java b/src/test/java/org/onap/clamp/loop/DeployFlowTestItCase.java index 07e7c4d7c..169db9db1 100644 --- a/src/test/java/org/onap/clamp/loop/DeployFlowTestItCase.java +++ b/src/test/java/org/onap/clamp/loop/DeployFlowTestItCase.java @@ -160,6 +160,7 @@ public class DeployFlowTestItCase { Loop loopAfterTest = loopService.getLoop("ControlLoopTest"); assertThat(loopAfterTest.getDcaeDeploymentStatusUrl().contains("/uninstall")).isTrue(); + assertThat(loopAfterTest.getDcaeDeploymentId()).isNull(); } /** @@ -196,6 +197,8 @@ public class DeployFlowTestItCase { Set<MicroServicePolicy> policyList = loopAfterTest.getMicroServicePolicies(); for (MicroServicePolicy policy : policyList) { assertThat(policy.getDcaeDeploymentStatusUrl().contains("/uninstall")).isTrue(); + assertThat(policy.getDcaeDeploymentId()).isNull(); + } assertThat(loopAfterTest.getDcaeDeploymentStatusUrl()).isNull(); assertThat(loopAfterTest.getDcaeDeploymentId()).isNull(); diff --git a/src/test/resources/robotframework/Dockerfile b/src/test/resources/robotframework/Dockerfile new file mode 100644 index 000000000..4ae08208e --- /dev/null +++ b/src/test/resources/robotframework/Dockerfile @@ -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 index 000000000..2ae8f4500 --- /dev/null +++ b/src/test/resources/robotframework/requirements.txt @@ -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 index 000000000..4ec657355 --- /dev/null +++ b/src/test/resources/robotframework/robotframework-test.properties @@ -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 index 000000000..f19266781 --- /dev/null +++ b/src/test/resources/robotframework/tests/01_healthcheck.robot @@ -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 diff --git a/ui-react/src/LoopUI.js b/ui-react/src/LoopUI.js index 8624726be..0ee6e6e24 100644 --- a/ui-react/src/LoopUI.js +++ b/ui-react/src/LoopUI.js @@ -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()} diff --git a/ui-react/src/__snapshots__/LoopUI.test.js.snap b/ui-react/src/__snapshots__/LoopUI.test.js.snap index 2dfa48091..cae9182ff 100644 --- a/ui-react/src/__snapshots__/LoopUI.test.js.snap +++ b/ui-react/src/__snapshots__/LoopUI.test.js.snap @@ -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 {}, diff --git a/ui-react/src/__snapshots__/OnapClamp.test.js.snap b/ui-react/src/__snapshots__/OnapClamp.test.js.snap index 56d022fc6..d4573b3d1 100644 --- a/ui-react/src/__snapshots__/OnapClamp.test.js.snap +++ b/ui-react/src/__snapshots__/OnapClamp.test.js.snap @@ -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 {}, diff --git a/ui-react/src/components/dialogs/PerformActions.js b/ui-react/src/components/dialogs/PerformActions.js index cf5a3c20e..f6001e21f 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 @@ 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; } } diff --git a/ui-react/src/components/dialogs/PerformActions.test.js b/ui-react/src/components/dialogs/PerformActions.test.js index b833a929d..c91c2f675 100644 --- a/ui-react/src/components/dialogs/PerformActions.test.js +++ b/ui-react/src/components/dialogs/PerformActions.test.js @@ -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(); diff --git a/ui-react/src/components/loop_viewer/svg/SvgGenerator.js b/ui-react/src/components/loop_viewer/svg/SvgGenerator.js index d718c2e44..7070455e7 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 @@ 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); + } } } |