From: Lott, Christopher (cl778h) Date: Fri, 28 Jun 2019 12:17:04 +0000 (-0400) Subject: Add error handling to improve user experience X-Git-Tag: R2~61 X-Git-Url: https://gerrit.o-ran-sc.org/r/gitweb?a=commitdiff_plain;h=226fd436a085f717d4cbf81bf1719bdaf1468414;hp=baba2efde0ab32026b1d13941dcc9e9dc95dc640;p=portal%2Fric-dashboard.git Add error handling to improve user experience Add front-end error handlers to indicate when data could not be fetched, which explains an empty table. Revise back-end error handling to reduce lines of code; add a central ControllerAdvice class that catches any HttpStatusCodeException and generates a JSON response. Change-Id: I434e602a89d05ed4de13cddc31633970aa2c8e5d Signed-off-by: Lott, Christopher (cl778h) --- diff --git a/docs/release-notes.rst b/docs/release-notes.rst index ecc41450..7a9e784c 100644 --- a/docs/release-notes.rst +++ b/docs/release-notes.rst @@ -20,15 +20,16 @@ RIC Dashboard Release Notes =========================== -Version 1.0.5, 3 July 2019 +Version 1.0.5, 5 July 2019 -------------------------- * Upgrade to Angular version 8 * Upgrade to Spring-Boot 2.1.6.RELEASE -* Fixed AC xApp policy page title is not aligned +* Align AC xApp policy page title * Update E2 manager client to spec version 20190703 * Add configuration-driven mock of E2 getNodebIdList * Revise front-end components to use prefix 'rd' -* Revise the notification service to display multiple notifications +* Revise the notification service to allow multiple +* Improve error handling in BE and FE code Version 1.0.4, 27 June 2019 --------------------------- @@ -51,7 +52,7 @@ Version 1.0.4, 27 June 2019 * Update App manager client to spec version 0.1.5 * Move RAN connection feature to control screen * Rework admin table -* Update the notification service +* Update the notification service * Move RAN connection feature to control screen * Repair deploy-app feature and use icon instead of text button diff --git a/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/AcXappController.java b/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/AcXappController.java index 279d504a..46a4aab0 100644 --- a/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/AcXappController.java +++ b/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/AcXappController.java @@ -31,13 +31,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; import org.springframework.util.Assert; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.client.HttpStatusCodeException; import com.fasterxml.jackson.databind.JsonNode; @@ -93,18 +91,11 @@ public class AcXappController { */ @ApiOperation(value = "Sets the admission control policy for AC xApp via the A1 Mediator") @RequestMapping(value = "catime", method = RequestMethod.PUT) - public Object setAdmissionControlPolicy( - @ApiParam(value = "Admission control policy") @RequestBody JsonNode acPolicy, // + public void setAdmissionControlPolicy(@ApiParam(value = "Admission control policy") @RequestBody JsonNode acPolicy, // HttpServletResponse response) { logger.debug("setAdmissionControlPolicy {}", acPolicy); - try { - a1MediatorApi.a1ControllerPutHandler(AC_CONTROL_NAME, acPolicy); - response.setStatus(a1MediatorApi.getApiClient().getStatusCode().value()); - return null; - } catch (HttpStatusCodeException ex) { - logger.warn("setAdmissionControlPolicy failed: {}", ex.toString()); - return ResponseEntity.status(HttpServletResponse.SC_BAD_GATEWAY).body(ex.getResponseBodyAsString()); - } + a1MediatorApi.a1ControllerPutHandler(AC_CONTROL_NAME, acPolicy); + response.setStatus(a1MediatorApi.getApiClient().getStatusCode().value()); } } diff --git a/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/AnrXappController.java b/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/AnrXappController.java index 9faeff72..2449caa9 100644 --- a/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/AnrXappController.java +++ b/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/AnrXappController.java @@ -36,7 +36,6 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; import org.springframework.util.Assert; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; @@ -44,7 +43,6 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.client.HttpStatusCodeException; import io.swagger.annotations.ApiOperation; @@ -90,94 +88,60 @@ public class AnrXappController { @ApiOperation(value = "Performs a liveness probe on the ANR xApp, result expressed as the response code.") @RequestMapping(value = "/health/alive", method = RequestMethod.GET) - public Object getHealthAlive(HttpServletResponse response) { + public void getHealthAlive(HttpServletResponse response) { logger.debug("getHealthAlive"); - try { - healthApi.getHealthAlive(); - response.setStatus(healthApi.getApiClient().getStatusCode().value()); - return null; - } catch (HttpStatusCodeException ex) { - logger.warn("getHealthAlive failed: {}", ex.toString()); - return ResponseEntity.status(HttpServletResponse.SC_BAD_GATEWAY).body(ex.getResponseBodyAsString()); - } + healthApi.getHealthAlive(); + response.setStatus(healthApi.getApiClient().getStatusCode().value()); } @ApiOperation(value = "Performs a readiness probe on the ANR xApp, result expressed as the response code.") @RequestMapping(value = "/health/ready", method = RequestMethod.GET) - public Object getHealthReady(HttpServletResponse response) { + public void getHealthReady(HttpServletResponse response) { logger.debug("getHealthReady"); - try { - healthApi.getHealthReady(); - response.setStatus(healthApi.getApiClient().getStatusCode().value()); - return null; - } catch (HttpStatusCodeException ex) { - logger.warn("getHealthAlive failed: {}", ex.toString()); - return ResponseEntity.status(HttpServletResponse.SC_BAD_GATEWAY).body(ex.getResponseBodyAsString()); - } + healthApi.getHealthReady(); + response.setStatus(healthApi.getApiClient().getStatusCode().value()); } @ApiOperation(value = "Returns list of gNodeB IDs based on NCRT in ANR", response = GgNodeBTable.class) @RequestMapping(value = "/gnodebs", method = RequestMethod.GET) - public Object getGnodebs() { + public GgNodeBTable getGnodebs() { logger.debug("getGnodebs"); - try { - return ncrtApi.getgNodeB(); - } catch (HttpStatusCodeException ex) { - logger.warn("getGnodebs failed: {}", ex.toString()); - return ResponseEntity.status(HttpServletResponse.SC_BAD_GATEWAY).body(ex.getResponseBodyAsString()); - } + return ncrtApi.getgNodeB(); } @ApiOperation(value = "Returns neighbor cell relation table for all gNodeBs or based on query parameters", response = NeighborCellRelationTable.class) @RequestMapping(value = "/ncrt", method = RequestMethod.GET) - public Object getNcrt( // + public NeighborCellRelationTable getNcrt( // @RequestParam(name = QP_NODEB, required = false) String ggnbId, // @RequestParam(name = QP_SERVING, required = false) String servingCellNrcgi, // @RequestParam(name = QP_NEIGHBOR, required = false) String neighborCellNrpci) { logger.debug("getNcrt: ggnbid {}, servingCellNrpci {}, neighborCellNrcgi {}", ggnbId, servingCellNrcgi, neighborCellNrpci); - try { - return ncrtApi.getNcrt(ggnbId, servingCellNrcgi, neighborCellNrpci); - } catch (HttpStatusCodeException ex) { - logger.warn("getNcrt failed: {}", ex.toString()); - return ResponseEntity.status(HttpServletResponse.SC_BAD_GATEWAY).body(ex.getResponseBodyAsString()); - } + return ncrtApi.getNcrt(ggnbId, servingCellNrcgi, neighborCellNrpci); } // /ncrt/servingcells/{servCellNrcgi}/neighborcells/{neighCellNrpci} : @ApiOperation(value = "Modify neighbor cell relation based on Serving Cell NRCGI and Neighbor Cell NRPCI") @RequestMapping(value = "/ncrt/" + PP_SERVING + "/{" + PP_SERVING + "}/" + PP_NEIGHBOR + "/{" + PP_NEIGHBOR + "}", method = RequestMethod.PUT) - public Object modifyNcrt(@PathVariable(PP_SERVING) String servingCellNrcgi, // + public void modifyNcrt(@PathVariable(PP_SERVING) String servingCellNrcgi, // @PathVariable(PP_NEIGHBOR) String neighborCellNrpci, // @RequestBody NeighborCellRelationMod ncrMod, HttpServletResponse response) { logger.debug("modifyNcrt: servingCellNrcgi {}, neighborCellNrpci {}, ncrMod {}", servingCellNrcgi, neighborCellNrpci, ncrMod); - try { - ncrtApi.modifyNcrt(servingCellNrcgi, neighborCellNrpci, ncrMod); - response.setStatus(healthApi.getApiClient().getStatusCode().value()); - return null; - } catch (HttpStatusCodeException ex) { - logger.warn("modifyNcrt failed: {}", ex.toString()); - return ResponseEntity.status(HttpServletResponse.SC_BAD_GATEWAY).body(ex.getResponseBodyAsString()); - } + ncrtApi.modifyNcrt(servingCellNrcgi, neighborCellNrpci, ncrMod); + response.setStatus(healthApi.getApiClient().getStatusCode().value()); } @ApiOperation(value = "Delete neighbor cell relation based on Serving Cell NRCGI and Neighbor Cell NRPCI") @RequestMapping(value = "/ncrt/" + PP_SERVING + "/{" + PP_SERVING + "}/" + PP_NEIGHBOR + "/{" + PP_NEIGHBOR + "}", method = RequestMethod.DELETE) - public Object deleteNcrt(@PathVariable(PP_SERVING) String servingCellNrcgi, // + public void deleteNcrt(@PathVariable(PP_SERVING) String servingCellNrcgi, // @PathVariable(PP_NEIGHBOR) String neighborCellNrpci, // HttpServletResponse response) { logger.debug("deleteNcrt: servingCellNrcgi {}, neighborCellNrpci {}", servingCellNrcgi, neighborCellNrpci); - try { - ncrtApi.deleteNcrt(servingCellNrcgi, neighborCellNrpci); - response.setStatus(healthApi.getApiClient().getStatusCode().value()); - return null; - } catch (HttpStatusCodeException ex) { - logger.warn("modifyNcrt failed: {}", ex.toString()); - return ResponseEntity.status(HttpServletResponse.SC_BAD_GATEWAY).body(ex.getResponseBodyAsString()); - } + ncrtApi.deleteNcrt(servingCellNrcgi, neighborCellNrpci); + response.setStatus(healthApi.getApiClient().getStatusCode().value()); } } diff --git a/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/AppManagerController.java b/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/AppManagerController.java index e8ad8a92..9c356ca0 100644 --- a/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/AppManagerController.java +++ b/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/AppManagerController.java @@ -42,14 +42,12 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; import org.springframework.util.Assert; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.client.HttpStatusCodeException; import io.swagger.annotations.ApiOperation; @@ -88,147 +86,90 @@ public class AppManagerController { @ApiOperation(value = "Health check of xApp Manager - Liveness probe.") @RequestMapping(value = "/health/alive", method = RequestMethod.GET) - public Object getHealth(HttpServletResponse response) { + public void getHealth(HttpServletResponse response) { logger.debug("getHealthAlive"); - try { - healthApi.getHealthAlive(); - response.setStatus(healthApi.getApiClient().getStatusCode().value()); - return null; - } catch (HttpStatusCodeException ex) { - logger.error("getHealthAlive failed: {}", ex.toString()); - return ResponseEntity.status(HttpServletResponse.SC_BAD_GATEWAY).body(ex.getResponseBodyAsString()); - } + healthApi.getHealthAlive(); + response.setStatus(healthApi.getApiClient().getStatusCode().value()); } @ApiOperation(value = "Readiness check of xApp Manager - Readiness probe.") @RequestMapping(value = "/health/ready", method = RequestMethod.GET) - public Object getHealthReady(HttpServletResponse response) { + public void getHealthReady(HttpServletResponse response) { logger.debug("getHealthReady"); - try { - healthApi.getHealthReady(); - response.setStatus(healthApi.getApiClient().getStatusCode().value()); - return null; - } catch (HttpStatusCodeException ex) { - logger.error("getHealthReady failed: {}", ex.toString()); - return ResponseEntity.status(HttpServletResponse.SC_BAD_GATEWAY).body(ex.getResponseBodyAsString()); - } + healthApi.getHealthReady(); + response.setStatus(healthApi.getApiClient().getStatusCode().value()); } @ApiOperation(value = "Returns the configuration of all xapps.", response = AllXappConfig.class) @RequestMapping(value = "/config", method = RequestMethod.GET) - public Object getAllXappConfig() { + public AllXappConfig getAllXappConfig() { logger.debug("getAllXappConfig"); - try { - return xappApi.getAllXappConfig(); - } catch (HttpStatusCodeException ex) { - logger.error("getAllXappConfig failed: {}", ex.toString()); - return ResponseEntity.status(HttpServletResponse.SC_BAD_GATEWAY).body(ex.getResponseBodyAsString()); - } + return xappApi.getAllXappConfig(); } - @ApiOperation(value = "Create xApp config.") + @ApiOperation(value = "Create xApp config.", response = XAppConfig.class) @RequestMapping(value = "/config", method = RequestMethod.POST) - public Object createXappConfig(@RequestBody XAppConfig xAppConfig) { + public XAppConfig createXappConfig(@RequestBody XAppConfig xAppConfig) { logger.debug("createXappConfig {}", xAppConfig); - try { - return xappApi.createXappConfig(xAppConfig); - } catch (HttpStatusCodeException ex) { - logger.error("undeployXapp failed: {}", ex.toString()); - return ResponseEntity.status(HttpServletResponse.SC_BAD_GATEWAY).body(ex.getResponseBodyAsString()); - } + return xappApi.createXappConfig(xAppConfig); } - @ApiOperation(value = "Modify xApp config.") + @ApiOperation(value = "Modify xApp config.", response = XAppConfig.class) @RequestMapping(value = "/config", method = RequestMethod.PUT) - public Object modifyXappConfig(@RequestBody XAppConfig xAppConfig) { + public XAppConfig modifyXappConfig(@RequestBody XAppConfig xAppConfig) { logger.debug("modifyXappConfig {}", xAppConfig); - try { - return xappApi.modifyXappConfig(xAppConfig); - } catch (HttpStatusCodeException ex) { - logger.error("modifyXappConfig failed: {}", ex.toString()); - return ResponseEntity.status(HttpServletResponse.SC_BAD_GATEWAY).body(ex.getResponseBodyAsString()); - } + return xappApi.modifyXappConfig(xAppConfig); } @ApiOperation(value = "Delete xApp configuration.") @RequestMapping(value = "/config/{xAppName}", method = RequestMethod.DELETE) - public Object deleteXappConfig(@RequestBody ConfigMetadata configMetadata, HttpServletResponse response) { + public void deleteXappConfig(@RequestBody ConfigMetadata configMetadata, HttpServletResponse response) { logger.debug("deleteXappConfig {}", configMetadata); - try { - xappApi.deleteXappConfig(configMetadata); - response.setStatus(healthApi.getApiClient().getStatusCode().value()); - return null; - } catch (HttpStatusCodeException ex) { - logger.error("deleteXappConfig failed: {}", ex.toString()); - return ResponseEntity.status(HttpServletResponse.SC_BAD_GATEWAY).body(ex.getResponseBodyAsString()); - } + xappApi.deleteXappConfig(configMetadata); + response.setStatus(healthApi.getApiClient().getStatusCode().value()); } @ApiOperation(value = "Returns a list of deployable xapps.", response = DashboardDeployableXapps.class) @RequestMapping(value = "/xapps/list", method = RequestMethod.GET) - public Object getAvailableXapps() { + public DashboardDeployableXapps getAvailableXapps() { logger.debug("getAvailableXapps"); - try { - AllDeployableXapps appNames = xappApi.listAllXapps(); - // Answer a collection of structure instead of string - DashboardDeployableXapps apps = new DashboardDeployableXapps(); - for (String n : appNames) - apps.add(new AppTransport(n)); - return apps; - } catch (HttpStatusCodeException ex) { - logger.error("getAvailableXapps failed: {}", ex.toString()); - return ResponseEntity.status(HttpServletResponse.SC_BAD_GATEWAY).body(ex.getResponseBodyAsString()); - } + AllDeployableXapps appNames = xappApi.listAllXapps(); + // Answer a collection of structure instead of string + // because I expect the AppMgr to be extended with + // additional properties for each one. + DashboardDeployableXapps apps = new DashboardDeployableXapps(); + for (String n : appNames) + apps.add(new AppTransport(n)); + return apps; } @ApiOperation(value = "Returns the status of all deployed xapps.", response = AllDeployedXapps.class) @RequestMapping(value = "/xapps", method = RequestMethod.GET) - public Object getDeployedXapps() { + public AllDeployedXapps getDeployedXapps() { logger.debug("getDeployedXapps"); - try { - return xappApi.getAllXapps(); - } catch (HttpStatusCodeException ex) { - logger.error("getDeployedXapps failed: {}", ex.toString()); - return ResponseEntity.status(HttpServletResponse.SC_BAD_GATEWAY).body(ex.getResponseBodyAsString()); - } + return xappApi.getAllXapps(); } @ApiOperation(value = "Returns the status of a given xapp.", response = Xapp.class) @RequestMapping(value = "/xapps/{xAppName}", method = RequestMethod.GET) - public Object getXapp(@PathVariable("xAppName") String xAppName) { + public Xapp getXapp(@PathVariable("xAppName") String xAppName) { logger.debug("getXapp {}", xAppName); - try { - return xappApi.getXappByName(xAppName); - } catch (HttpStatusCodeException ex) { - logger.error("getXapp failed: {}", ex.toString()); - return ResponseEntity.status(HttpServletResponse.SC_BAD_GATEWAY).body(ex.getResponseBodyAsString()); - } + return xappApi.getXappByName(xAppName); } @ApiOperation(value = "Deploy a xapp.", response = Xapp.class) @RequestMapping(value = "/xapps", method = RequestMethod.POST) - public Object deployXapp(@RequestBody XAppInfo xAppInfo) { + public Xapp deployXapp(@RequestBody XAppInfo xAppInfo) { logger.debug("deployXapp {}", xAppInfo); - try { - return xappApi.deployXapp(xAppInfo); - } catch (HttpStatusCodeException ex) { - logger.error("deployXapp failed: {}", ex.toString()); - return ResponseEntity.status(HttpServletResponse.SC_BAD_GATEWAY).body(ex.getResponseBodyAsString()); - } + return xappApi.deployXapp(xAppInfo); } @ApiOperation(value = "Undeploy an existing xapp.") @RequestMapping(value = "/xapps/{xAppName}", method = RequestMethod.DELETE) - public Object undeployXapp(@PathVariable("xAppName") String xAppName, HttpServletResponse response) { + public void undeployXapp(@PathVariable("xAppName") String xAppName, HttpServletResponse response) { logger.debug("undeployXapp {}", xAppName); - try { - xappApi.undeployXapp(xAppName); - response.setStatus(healthApi.getApiClient().getStatusCode().value()); - return null; - } catch (HttpStatusCodeException ex) { - logger.error("undeployXapp failed: {}", ex.toString()); - return ResponseEntity.status(HttpServletResponse.SC_BAD_GATEWAY).body(ex.getResponseBodyAsString()); - } + xappApi.undeployXapp(xAppName); + response.setStatus(healthApi.getApiClient().getStatusCode().value()); } } diff --git a/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/CustomizedResponseEntityExceptionHandler.java b/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/CustomizedResponseEntityExceptionHandler.java new file mode 100644 index 00000000..95bcfb4c --- /dev/null +++ b/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/CustomizedResponseEntityExceptionHandler.java @@ -0,0 +1,77 @@ +/*- + * ===============LICENSE_START======================================================= + * Acumos + * =================================================================================== + * Copyright (C) 2019 AT&T Intellectual Property & Tech Mahindra. All rights reserved. + * =================================================================================== + * This Acumos software file is distributed by AT&T and Tech Mahindra + * 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 + * + * This file 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.oransc.ric.portal.dashboard.controller; + +import java.lang.invoke.MethodHandles; + +import org.oransc.ric.portal.dashboard.model.ErrorTransport; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.client.HttpStatusCodeException; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +/** + * Catches Http status code exceptions and builds a response with code 502 and + * some details wrapped in an ErrorTransport object. This factors out try-catch + * blocks in many controller methods. + * + * Why 502? I quote:
HTTP server received an invalid response from a + * server it consulted when acting as a proxy or gateway.
+ * + * This class and the methods are not strictly necessary, the + * SimpleErrorController is invoked when any controller method takes an uncaught + * exception, but this approach provides a better response to the front end and + * doesn't signal internal server error. + * + * Also see:
+ * https://www.baeldung.com/exception-handling-for-rest-with-spring + * https://www.springboottutorial.com/spring-boot-exception-handling-for-rest-services + */ +@ControllerAdvice +public class CustomizedResponseEntityExceptionHandler extends ResponseEntityExceptionHandler { + + private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + /** + * Generates the response when a REST controller method takes an + * HttpStatusCodeException. Confusingly, the container first redirects to /error + * which invokes the + * {@link org.oransc.ric.portal.dashboard.controller.SimpleErrorController} + * method, and that response arrives here as the response body. + * + * @param ex + * The exception + * @param request + * The orignal request + * @return A response entity with status code 502 plus some details in the body. + */ + @ExceptionHandler(HttpStatusCodeException.class) + public final ResponseEntity handleHttpStatusCodeException(HttpStatusCodeException ex, WebRequest request) { + logger.warn("Request {} failed, status code {}", request.getDescription(false), ex.getStatusCode()); + return new ResponseEntity( + new ErrorTransport(ex.getRawStatusCode(), ex.getResponseBodyAsString(), ex), HttpStatus.BAD_GATEWAY); + } + +} diff --git a/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/E2ManagerController.java b/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/E2ManagerController.java index 631515b3..a4533b64 100644 --- a/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/E2ManagerController.java +++ b/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/E2ManagerController.java @@ -33,7 +33,6 @@ import org.oransc.ric.e2mgr.client.model.NodebIdentityGlobalNbId; import org.oransc.ric.e2mgr.client.model.SetupRequest; import org.oransc.ric.portal.dashboard.DashboardApplication; import org.oransc.ric.portal.dashboard.DashboardConstants; -import org.oransc.ric.portal.dashboard.model.ErrorTransport; import org.oransc.ric.portal.dashboard.model.RanDetailsTransport; import org.oransc.ric.portal.dashboard.model.SuccessTransport; import org.slf4j.Logger; @@ -41,9 +40,7 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; import org.springframework.util.Assert; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; @@ -104,41 +101,27 @@ public class E2ManagerController { @ApiOperation(value = "Gets the health from the E2 manager, expressed as the response code.") @RequestMapping(value = "/health", method = RequestMethod.GET) - public Object healthGet(HttpServletResponse response) { + public void healthGet(HttpServletResponse response) { logger.debug("healthGet"); - try { - e2HealthCheckApi.healthGet(); - response.setStatus(e2HealthCheckApi.getApiClient().getStatusCode().value()); - return null; - } catch (HttpStatusCodeException ex) { - logger.warn("healthGet failed: {}", ex.toString()); - return new ResponseEntity(new ErrorTransport(ex.getRawStatusCode(), ex.toString()), - HttpStatus.BAD_GATEWAY); - } + e2HealthCheckApi.healthGet(); + response.setStatus(e2HealthCheckApi.getApiClient().getStatusCode().value()); } // This calls other methods to simplify the task of the front-end. @ApiOperation(value = "Gets all RAN identities and statuses from the E2 manager.", response = RanDetailsTransport.class, responseContainer = "List") @RequestMapping(value = "/ran", method = RequestMethod.GET) - public Object getRanDetails() { + public List getRanDetails() { logger.debug("getRanDetails"); - List nodebIdList = null; - try { - // TODO: remove mock when e2mgr delivers the getNodebIdList() method - nodebIdList = mockNodebIdList.isEmpty() ? e2NodebApi.getNodebIdList() : mockNodebIdList; - } catch (HttpStatusCodeException ex) { - logger.warn("getRanDetails: getNodebIdList failed: {}", ex.toString()); - return new ResponseEntity(new ErrorTransport(ex.getRawStatusCode(), ex.toString()), - HttpStatus.BAD_GATEWAY); - } + // TODO: remove mock when e2mgr delivers the getNodebIdList() method + List nodebIdList = mockNodebIdList.isEmpty() ? e2NodebApi.getNodebIdList() : mockNodebIdList; List details = new ArrayList<>(); for (NodebIdentity nbid : nodebIdList) { GetNodebResponse nbResp = null; try { - // Keep looping despite failures + // Catch exceptions to keep looping despite failures nbResp = e2NodebApi.getNb(nbid.getInventoryName()); } catch (HttpStatusCodeException ex) { - logger.warn("getRanDetails failed for name {}: {}", nbid.getInventoryName(), ex.toString()); + logger.warn("E2 getNb failed for name {}: {}", nbid.getInventoryName(), ex.toString()); nbResp = new GetNodebResponse().connectionStatus("UNKNOWN").ip("UNKNOWN").port(-1) .ranName(nbid.getInventoryName()); } @@ -149,73 +132,40 @@ public class E2ManagerController { @ApiOperation(value = "Get RAN identities list.", response = NodebIdentity.class, responseContainer = "List") @RequestMapping(value = "/nodeb-ids", method = RequestMethod.GET) - public Object getNodebIdList() { + public List getNodebIdList() { logger.debug("getNodebIdList"); - try { - return e2NodebApi.getNodebIdList(); - } catch (HttpStatusCodeException ex) { - logger.warn("getNodebIdList failed: {}", ex.toString()); - return new ResponseEntity(new ErrorTransport(ex.getRawStatusCode(), ex.toString()), - HttpStatus.BAD_GATEWAY); - } + return e2NodebApi.getNodebIdList(); } @ApiOperation(value = "Get RAN by name.", response = GetNodebResponse.class) @RequestMapping(value = "/nodeb/{" + PP_RANNAME + "}", method = RequestMethod.GET) - public Object getNb(@PathVariable(PP_RANNAME) String ranName) { + public GetNodebResponse getNb(@PathVariable(PP_RANNAME) String ranName) { logger.debug("getNb {}", ranName); - try { - return e2NodebApi.getNb(ranName); - } catch (HttpStatusCodeException ex) { - logger.warn("getNb failed: {}", ex.toString()); - return new ResponseEntity(new ErrorTransport(ex.getRawStatusCode(), ex.toString()), - HttpStatus.BAD_GATEWAY); - } + return e2NodebApi.getNb(ranName); } @ApiOperation(value = "Close all connections to the RANs and delete the data from the nodeb-rnib DB.") @RequestMapping(value = "/nodeb", method = RequestMethod.DELETE) - public Object nodebDelete(HttpServletResponse response) { + public void nodebDelete(HttpServletResponse response) { logger.debug("nodebDelete"); - try { - e2NodebApi.nodebDelete(); - response.setStatus(e2NodebApi.getApiClient().getStatusCode().value()); - return null; - } catch (HttpStatusCodeException ex) { - logger.warn("nodebDelete failed: {}", ex.toString()); - return new ResponseEntity(new ErrorTransport(ex.getRawStatusCode(), ex.toString()), - HttpStatus.BAD_GATEWAY); - } + e2NodebApi.nodebDelete(); + response.setStatus(e2NodebApi.getApiClient().getStatusCode().value()); } @ApiOperation(value = "Sets up an EN-DC RAN connection via the E2 manager.") @RequestMapping(value = "/endcSetup", method = RequestMethod.POST) - public Object endcSetup(@RequestBody SetupRequest setupRequest, HttpServletResponse response) { + public void endcSetup(@RequestBody SetupRequest setupRequest, HttpServletResponse response) { logger.debug("endcSetup {}", setupRequest); - try { - e2NodebApi.endcSetup(setupRequest); - response.setStatus(e2NodebApi.getApiClient().getStatusCode().value()); - return null; - } catch (HttpStatusCodeException ex) { - logger.warn("endcSetup failed: {}", ex.toString()); - return new ResponseEntity(new ErrorTransport(ex.getRawStatusCode(), ex.toString()), - HttpStatus.BAD_GATEWAY); - } + e2NodebApi.endcSetup(setupRequest); + response.setStatus(e2NodebApi.getApiClient().getStatusCode().value()); } @ApiOperation(value = "Sets up an X2 RAN connection via the E2 manager.") @RequestMapping(value = "/x2Setup", method = RequestMethod.POST) - public Object x2Setup(@RequestBody SetupRequest setupRequest, HttpServletResponse response) { + public void x2Setup(@RequestBody SetupRequest setupRequest, HttpServletResponse response) { logger.debug("x2Setup {}", setupRequest); - try { - e2NodebApi.x2Setup(setupRequest); - response.setStatus(e2NodebApi.getApiClient().getStatusCode().value()); - return null; - } catch (HttpStatusCodeException ex) { - logger.warn("x2Setup failed: {}", ex.toString()); - return new ResponseEntity(new ErrorTransport(ex.getRawStatusCode(), ex.toString()), - HttpStatus.BAD_GATEWAY); - } + e2NodebApi.x2Setup(setupRequest); + response.setStatus(e2NodebApi.getApiClient().getStatusCode().value()); } } diff --git a/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/SimpleErrorController.java b/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/SimpleErrorController.java index a1678be0..859c9134 100644 --- a/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/SimpleErrorController.java +++ b/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/SimpleErrorController.java @@ -19,10 +19,13 @@ */ package org.oransc.ric.portal.dashboard.controller; +import java.lang.invoke.MethodHandles; import java.util.Map; import javax.servlet.http.HttpServletRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.web.servlet.error.ErrorAttributes; import org.springframework.boot.web.servlet.error.ErrorController; @@ -35,10 +38,12 @@ import org.springframework.web.context.request.WebRequest; import springfox.documentation.annotations.ApiIgnore; /** - * Returns JSON on error within the Spring-managed context. Does not fire for - * anything else; e.g., resource not found outside the context. If trace is - * requested via request parameter ("?trace=true") and available, adds stack - * trace information to the standard JSON error response. + * Provides an endpoint that returns JSON, which is invoked following any error + * within the Spring-managed context. This is NOT called for errors outside the + * context; e.g., resource not found. + * + * If trace is requested via request parameter ("?trace=true") and available, + * adds stack trace information to the standard JSON error response. * * Excluded from Swagger API documentation. * @@ -49,6 +54,8 @@ import springfox.documentation.annotations.ApiIgnore; @RestController public class SimpleErrorController implements ErrorController { + private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + private static final String ERROR_PATH = "/error"; private static final String TRACE = "trace"; private final ErrorAttributes errorAttributes; @@ -80,6 +87,7 @@ public class SimpleErrorController implements ErrorController { @RequestMapping(ERROR_PATH) public Map error(HttpServletRequest aRequest) { Map body = getErrorAttributes(aRequest, getTraceParameter(aRequest)); + logger.warn("Failed in request for {}", body.get("path")); body.put("decorated-by", SimpleErrorController.class.getName()); body.computeIfPresent(TRACE, (key, value) -> body.put(TRACE, ((String) value).split("\n\t"))); return body; diff --git a/webapp-frontend/src/app/anr-xapp/anr-xapp.component.html b/webapp-frontend/src/app/anr-xapp/anr-xapp.component.html index 09e676d4..4cdd55e1 100644 --- a/webapp-frontend/src/app/anr-xapp/anr-xapp.component.html +++ b/webapp-frontend/src/app/anr-xapp/anr-xapp.component.html @@ -35,10 +35,6 @@ -
- -
- @@ -83,13 +79,18 @@ - + + No records found. + + +
-
- ANR client version {{anrClientVersion}} +
+
+
diff --git a/webapp-frontend/src/app/anr-xapp/anr-xapp.component.scss b/webapp-frontend/src/app/anr-xapp/anr-xapp.component.scss index 094a32da..5d8c3ffb 100644 --- a/webapp-frontend/src/app/anr-xapp/anr-xapp.component.scss +++ b/webapp-frontend/src/app/anr-xapp/anr-xapp.component.scss @@ -30,10 +30,9 @@ } .spinner-container { - height: 360px; - width: 390px; - position: fixed; -} + height: 100px; + width: 100px; + } .spinner-container mat-spinner { margin: 130px auto 0 auto; @@ -80,3 +79,8 @@ letter-spacing: 0.1rem; font-size: 10px; } + +.display-none { + display: none; + } + \ No newline at end of file diff --git a/webapp-frontend/src/app/anr-xapp/anr-xapp.component.ts b/webapp-frontend/src/app/anr-xapp/anr-xapp.component.ts index 3440482c..ba240714 100644 --- a/webapp-frontend/src/app/anr-xapp/anr-xapp.component.ts +++ b/webapp-frontend/src/app/anr-xapp/anr-xapp.component.ts @@ -39,7 +39,6 @@ import { ANRXappDataSource } from './anr-xapp.datasource'; export class AnrXappComponent implements AfterViewInit, OnInit { dataSource: ANRXappDataSource; - anrClientVersion: string; gNodeBIds: string[]; @ViewChild('ggNodeB', {static: true}) ggNodeB: ElementRef; @ViewChild('servingCellNrcgi', {static: true}) servingCellNrcgi: ElementRef; @@ -57,11 +56,10 @@ export class AnrXappComponent implements AfterViewInit, OnInit { private notificationService: NotificationService) { } ngOnInit() { - this.dataSource = new ANRXappDataSource(this.anrXappService, this.sort); + this.dataSource = new ANRXappDataSource(this.anrXappService, this.sort, this.notificationService); this.dataSource.loadTable(); // Empty string occurs first in the array of gNodeBIds this.anrXappService.getgNodeBs().subscribe((res: string[]) => this.gNodeBIds = res); - this.anrXappService.getVersion().subscribe((res: string) => this.anrClientVersion = res); } ngAfterViewInit() { diff --git a/webapp-frontend/src/app/anr-xapp/anr-xapp.datasource.ts b/webapp-frontend/src/app/anr-xapp/anr-xapp.datasource.ts index bc04b6b0..cbf353f5 100644 --- a/webapp-frontend/src/app/anr-xapp/anr-xapp.datasource.ts +++ b/webapp-frontend/src/app/anr-xapp/anr-xapp.datasource.ts @@ -19,59 +19,70 @@ */ import { CollectionViewer, DataSource } from '@angular/cdk/collections'; +import { HttpErrorResponse } from '@angular/common/http'; import { MatSort } from '@angular/material'; -import { merge } from 'rxjs'; -import { BehaviorSubject } from 'rxjs/BehaviorSubject'; import { Observable } from 'rxjs/Observable'; +import { BehaviorSubject } from 'rxjs/BehaviorSubject'; import { of } from 'rxjs/observable/of'; +import { merge } from 'rxjs'; import { catchError, finalize, map } from 'rxjs/operators'; import { ANRNeighborCellRelation } from '../interfaces/anr-xapp.types'; import { ANRXappService } from '../services/anr-xapp/anr-xapp.service'; +import { NotificationService } from '../services/ui/notification.service'; -// https://blog.angular-university.io/angular-material-data-table/ export class ANRXappDataSource extends DataSource { - private relationsSubject = new BehaviorSubject([]); + private relationsSubject = new BehaviorSubject([]); - private loadingSubject = new BehaviorSubject(false); + private loadingSubject = new BehaviorSubject(false); - public loading$ = this.loadingSubject.asObservable(); + public loading$ = this.loadingSubject.asObservable(); - constructor(private anrXappService: ANRXappService, private sort: MatSort) { - super(); - } + public rowCount = 1; // hide footer during intial load - loadTable(ggnodeb = '', servingCellNrcgi = '', neighborCellNrpci = '') { - this.loadingSubject.next(true); - this.anrXappService.getNcrtInfo(ggnodeb, servingCellNrcgi, neighborCellNrpci) - .pipe( - catchError(() => of([])), - finalize(() => this.loadingSubject.next(false)) - ) - .subscribe(ncrt => this.relationsSubject.next(ncrt)); - } + constructor(private anrXappService: ANRXappService, + private sort: MatSort, + private notificationService: NotificationService) { + super(); + } - connect(collectionViewer: CollectionViewer): Observable { - const dataMutations = [ - this.relationsSubject.asObservable(), - this.sort.sortChange - ]; - return merge(...dataMutations).pipe(map(() => { - return this.getSortedData([...this.relationsSubject.getValue()]); - })); - } + loadTable(ggnodeb: string = '', servingCellNrcgi: string = '', neighborCellNrpci: string = '') { + this.loadingSubject.next(true); + this.anrXappService.getNcrtInfo(ggnodeb, servingCellNrcgi, neighborCellNrpci) + .pipe( + catchError( (err: HttpErrorResponse) => { + console.log('ANRXappDataSource failed: ' + err.message); + this.notificationService.error('Failed to get data.'); + return of([]); + }), + finalize(() => this.loadingSubject.next(false)) + ) + .subscribe( (ncrt: ANRNeighborCellRelation[]) => { + this.rowCount = ncrt.length; + this.relationsSubject.next(ncrt); + }); + } - disconnect(collectionViewer: CollectionViewer): void { - this.relationsSubject.complete(); - this.loadingSubject.complete(); - } + connect(collectionViewer: CollectionViewer): Observable { + const dataMutations = [ + this.relationsSubject.asObservable(), + this.sort.sortChange + ]; + return merge(...dataMutations).pipe(map(() => { + return this.getSortedData([...this.relationsSubject.getValue()]); + })); + } + + disconnect(collectionViewer: CollectionViewer): void { + this.relationsSubject.complete(); + this.loadingSubject.complete(); + } private getSortedData(data: ANRNeighborCellRelation[]) { if (!this.sort.active || this.sort.direction === '') { return data; } - - return data.sort((a, b) => { + return data.sort((a: ANRNeighborCellRelation, b: ANRNeighborCellRelation) => { const isAsc = this.sort.direction === 'asc'; switch (this.sort.active) { case 'cellIdentifierNrcgi': return compare(a.servingCellNrcgi, b.servingCellNrcgi, isAsc); @@ -84,9 +95,8 @@ export class ANRXappDataSource extends DataSource { } }); } - } -function compare(a, b, isAsc) { +function compare(a: any, b: any, isAsc: boolean) { return (a < b ? -1 : 1) * (isAsc ? 1 : -1); } diff --git a/webapp-frontend/src/app/app-control/app-control.component.html b/webapp-frontend/src/app/app-control/app-control.component.html index f8a9d4c5..6c8a453d 100644 --- a/webapp-frontend/src/app/app-control/app-control.component.html +++ b/webapp-frontend/src/app/app-control/app-control.component.html @@ -20,9 +20,7 @@

xApp Control

-
- -
+ @@ -82,10 +80,21 @@ + + No records found. + + + +
+ +
+ +
+
diff --git a/webapp-frontend/src/app/app-control/app-control.component.css b/webapp-frontend/src/app/app-control/app-control.component.scss similarity index 92% rename from webapp-frontend/src/app/app-control/app-control.component.css rename to webapp-frontend/src/app/app-control/app-control.component.scss index db05cc3f..37034c89 100644 --- a/webapp-frontend/src/app/app-control/app-control.component.css +++ b/webapp-frontend/src/app/app-control/app-control.component.scss @@ -30,13 +30,12 @@ } .spinner-container { - height: 360px; - width: 390px; - position: fixed; + height: 100px; + width: 100px; } .spinner-container mat-spinner { - margin: 130px auto 0 auto; + margin: 0 auto 0 auto; } .app-control-table { @@ -49,3 +48,7 @@ tr.message-row { height: 0; } + +.display-none { + display: none; +} diff --git a/webapp-frontend/src/app/app-control/app-control.component.ts b/webapp-frontend/src/app/app-control/app-control.component.ts index f87b9b5b..b172152f 100644 --- a/webapp-frontend/src/app/app-control/app-control.component.ts +++ b/webapp-frontend/src/app/app-control/app-control.component.ts @@ -31,7 +31,7 @@ import { AppControlDataSource } from './app-control.datasource'; @Component({ selector: 'rd-app-control', templateUrl: './app-control.component.html', - styleUrls: ['./app-control.component.css'], + styleUrls: ['./app-control.component.scss'], animations: [AppControlAnimations.messageTrigger] }) export class AppControlComponent implements OnInit { @@ -45,10 +45,10 @@ export class AppControlComponent implements OnInit { private router: Router, private confirmDialogService: ConfirmDialogService, private errorDialogService: ErrorDialogService, - private notification: NotificationService) { } + private notificationService: NotificationService) { } ngOnInit() { - this.dataSource = new AppControlDataSource(this.appMgrSvc, this.sort); + this.dataSource = new AppControlDataSource(this.appMgrSvc, this.sort, this.notificationService); this.dataSource.loadTable(); } @@ -76,10 +76,10 @@ export class AppControlComponent implements OnInit { this.dataSource.loadTable(); switch (response.status) { case 200: - this.notification.success('xApp undeployed successfully!'); + this.notificationService.success('xApp undeployed successfully!'); break; default: - this.notification.warn('xApp undeploy failed.'); + this.notificationService.warn('xApp undeploy failed.'); } } ); diff --git a/webapp-frontend/src/app/app-control/app-control.datasource.ts b/webapp-frontend/src/app/app-control/app-control.datasource.ts index cf0c3dcf..fa98dfad 100644 --- a/webapp-frontend/src/app/app-control/app-control.datasource.ts +++ b/webapp-frontend/src/app/app-control/app-control.datasource.ts @@ -19,24 +19,28 @@ */ import { CollectionViewer, DataSource } from '@angular/cdk/collections'; +import { HttpErrorResponse } from '@angular/common/http'; import { MatSort } from '@angular/material'; -import { merge } from 'rxjs'; -import { BehaviorSubject } from 'rxjs/BehaviorSubject'; import { Observable } from 'rxjs/Observable'; +import { BehaviorSubject } from 'rxjs/BehaviorSubject'; +import { merge } from 'rxjs'; import { of } from 'rxjs/observable/of'; import { catchError, finalize, map } from 'rxjs/operators'; import { XappControlRow, XMDeployedApp, XMXappInstance } from '../interfaces/app-mgr.types'; import { AppMgrService } from '../services/app-mgr/app-mgr.service'; +import { NotificationService } from '../services/ui/notification.service'; export class AppControlDataSource extends DataSource { - private xAppInstancesSubject = new BehaviorSubject([]); + private appControlSubject = new BehaviorSubject([]); private loadingSubject = new BehaviorSubject(false); public loading$ = this.loadingSubject.asObservable(); - emptyInstances: XMXappInstance = + public rowCount = 1; // hide footer during intial load + + private emptyInstances: XMXappInstance = { ip: null, name: null, port: null, @@ -45,7 +49,9 @@ export class AppControlDataSource extends DataSource { txMessages: [], }; - constructor(private appMgrSvc: AppMgrService, private sort: MatSort) { + constructor(private appMgrSvc: AppMgrService, + private sort: MatSort, + private notificationService: NotificationService) { super(); } @@ -53,28 +59,36 @@ export class AppControlDataSource extends DataSource { this.loadingSubject.next(true); this.appMgrSvc.getDeployed() .pipe( - catchError(() => of([])), + catchError( (err: HttpErrorResponse) => { + console.log('AppControlDataSource failed: ' + err.message); + this.notificationService.error('Failed to get applications.'); + return of([]); + }), finalize(() => this.loadingSubject.next(false)) ) - .subscribe(xApps => this.xAppInstancesSubject.next(this.flatten(xApps))); + .subscribe( (xApps: XMDeployedApp[]) => { + this.rowCount = xApps.length; + const flattenedApps = this.flatten(xApps); + this.appControlSubject.next(flattenedApps); + }); } connect(collectionViewer: CollectionViewer): Observable { const dataMutations = [ - this.xAppInstancesSubject.asObservable(), + this.appControlSubject.asObservable(), this.sort.sortChange ]; return merge(...dataMutations).pipe(map(() => { - return this.getSortedData([...this.xAppInstancesSubject.getValue()]); + return this.getSortedData([...this.appControlSubject.getValue()]); })); } disconnect(collectionViewer: CollectionViewer): void { - this.xAppInstancesSubject.complete(); + this.appControlSubject.complete(); this.loadingSubject.complete(); } - private flatten(allxappdata: XMDeployedApp[]) { + private flatten(allxappdata: XMDeployedApp[]): XappControlRow[] { const xAppInstances: XappControlRow[] = []; for (const xapp of allxappdata) { if (!xapp.instances) { @@ -115,6 +129,6 @@ export class AppControlDataSource extends DataSource { } } -function compare(a, b, isAsc) { +function compare(a: any, b: any, isAsc: boolean) { return (a < b ? -1 : 1) * (isAsc ? 1 : -1); } diff --git a/webapp-frontend/src/app/catalog/catalog.component.html b/webapp-frontend/src/app/catalog/catalog.component.html index 6f2e06d9..2a9b72dd 100644 --- a/webapp-frontend/src/app/catalog/catalog.component.html +++ b/webapp-frontend/src/app/catalog/catalog.component.html @@ -19,9 +19,7 @@ -->

xApp Catalog

-
- -
+ @@ -48,7 +46,18 @@ + + No records found. + + + +
+ +
+ +
+
diff --git a/webapp-frontend/src/app/catalog/catalog.component.css b/webapp-frontend/src/app/catalog/catalog.component.scss similarity index 93% rename from webapp-frontend/src/app/catalog/catalog.component.css rename to webapp-frontend/src/app/catalog/catalog.component.scss index abcff4f9..0f9d8889 100644 --- a/webapp-frontend/src/app/catalog/catalog.component.css +++ b/webapp-frontend/src/app/catalog/catalog.component.scss @@ -30,9 +30,8 @@ } .spinner-container { - height: 360px; - width: 390px; - position: fixed; + height: 100px; + width: 100px; } .spinner-container mat-spinner { @@ -40,7 +39,7 @@ } .catalog-table { - width: 99%; + width: 100%; min-height: 150px; margin-top: 10px; background-color: transparent; @@ -49,3 +48,7 @@ .catalog-button-row button{ margin-right: 5px; } + +.display-none { + display: none; +} diff --git a/webapp-frontend/src/app/catalog/catalog.component.ts b/webapp-frontend/src/app/catalog/catalog.component.ts index 57814803..97dfd7ca 100644 --- a/webapp-frontend/src/app/catalog/catalog.component.ts +++ b/webapp-frontend/src/app/catalog/catalog.component.ts @@ -29,7 +29,7 @@ import { CatalogDataSource } from './catalog.datasource'; @Component({ selector: 'rd-app-catalog', templateUrl: './catalog.component.html', - styleUrls: ['./catalog.component.css'], + styleUrls: ['./catalog.component.scss'], }) export class CatalogComponent implements OnInit { @@ -38,31 +38,31 @@ export class CatalogComponent implements OnInit { @ViewChild(MatSort, {static: true}) sort: MatSort; constructor( - private appMgrSvc: AppMgrService, + private appMgrService: AppMgrService, private confirmDialogService: ConfirmDialogService, - private errorService: ErrorDialogService, - private notification: NotificationService) { } + private errorDiaglogService: ErrorDialogService, + private notificationService: NotificationService) { } ngOnInit() { - this.dataSource = new CatalogDataSource(this.appMgrSvc, this.sort ); + this.dataSource = new CatalogDataSource(this.appMgrService, this.sort, this.notificationService ); this.dataSource.loadTable(); } onConfigureApp(name: string): void { const aboutError = 'Configure not implemented (yet)'; - this.errorService.displayError(aboutError); + this.errorDiaglogService.displayError(aboutError); } onDeployApp(name: string): void { this.confirmDialogService.openConfirmDialog('Deploy application ' + name + '?') .afterClosed().subscribe( (res: any) => { if (res) { - this.appMgrSvc.deployXapp(name).subscribe( + this.appMgrService.deployXapp(name).subscribe( (response: HttpResponse) => { - this.notification.success('Deploy succeeded!'); + this.notificationService.success('Deploy succeeded!'); }, (error: HttpErrorResponse) => { - this.notification.warn('Deploy failed: ' + error.message); + this.notificationService.warn('Deploy failed: ' + error.message); } ); } diff --git a/webapp-frontend/src/app/catalog/catalog.datasource.ts b/webapp-frontend/src/app/catalog/catalog.datasource.ts index e47abfde..5c9ac947 100644 --- a/webapp-frontend/src/app/catalog/catalog.datasource.ts +++ b/webapp-frontend/src/app/catalog/catalog.datasource.ts @@ -19,24 +19,30 @@ */ import { CollectionViewer, DataSource } from '@angular/cdk/collections'; +import { HttpErrorResponse } from '@angular/common/http'; import { MatSort } from '@angular/material'; -import { merge } from 'rxjs'; -import { BehaviorSubject } from 'rxjs/BehaviorSubject'; import { Observable } from 'rxjs/Observable'; +import { BehaviorSubject } from 'rxjs/BehaviorSubject'; +import { merge } from 'rxjs'; import { of } from 'rxjs/observable/of'; import { catchError, finalize, map } from 'rxjs/operators'; import { AppMgrService } from '../services/app-mgr/app-mgr.service'; import { XMDeployableApp } from '../interfaces/app-mgr.types'; +import { NotificationService } from '../services/ui/notification.service'; export class CatalogDataSource extends DataSource { - private xAppsSubject = new BehaviorSubject([]); + private catalogSubject = new BehaviorSubject([]); private loadingSubject = new BehaviorSubject(false); public loading$ = this.loadingSubject.asObservable(); - constructor(private appMgrSvc: AppMgrService, private sort: MatSort) { + public rowCount = 1; // hide footer during intial load + + constructor(private appMgrSvc: AppMgrService, + private sort: MatSort, + private notificationService: NotificationService) { super(); } @@ -44,24 +50,31 @@ export class CatalogDataSource extends DataSource { this.loadingSubject.next(true); this.appMgrSvc.getDeployable() .pipe( - catchError(() => of([])), + catchError( (err: HttpErrorResponse) => { + console.log('CatalogDataSource failed: ' + err.message); + this.notificationService.error('Failed to get applications.'); + return of([]); + }), finalize(() => this.loadingSubject.next(false)) ) - .subscribe(xApps => this.xAppsSubject.next(xApps)); + .subscribe( (xApps: XMDeployableApp[]) => { + this.rowCount = xApps.length; + this.catalogSubject.next(xApps); + }); } connect(collectionViewer: CollectionViewer): Observable { const dataMutations = [ - this.xAppsSubject.asObservable(), + this.catalogSubject.asObservable(), this.sort.sortChange ]; return merge(...dataMutations).pipe(map(() => { - return this.getSortedData([...this.xAppsSubject.getValue()]); + return this.getSortedData([...this.catalogSubject.getValue()]); })); } disconnect(collectionViewer: CollectionViewer): void { - this.xAppsSubject.complete(); + this.catalogSubject.complete(); this.loadingSubject.complete(); } @@ -79,9 +92,8 @@ export class CatalogDataSource extends DataSource { }); } - private compare(a: string, b: string, isAsc: boolean) { + private compare(a: any, b: any, isAsc: boolean) { return (a < b ? -1 : 1) * (isAsc ? 1 : -1); } } - diff --git a/webapp-frontend/src/app/control/control.component.css b/webapp-frontend/src/app/control/control.component.scss similarity index 100% rename from webapp-frontend/src/app/control/control.component.css rename to webapp-frontend/src/app/control/control.component.scss diff --git a/webapp-frontend/src/app/control/control.component.ts b/webapp-frontend/src/app/control/control.component.ts index eec53e3d..8ba04a41 100644 --- a/webapp-frontend/src/app/control/control.component.ts +++ b/webapp-frontend/src/app/control/control.component.ts @@ -22,7 +22,7 @@ import { Component, OnInit } from '@angular/core'; @Component({ selector: 'rd-control', templateUrl: './control.component.html', - styleUrls: ['./control.component.css'] + styleUrls: ['./control.component.scss'] }) export class ControlComponent implements OnInit { diff --git a/webapp-frontend/src/app/interfaces/e2-mgr.types.ts b/webapp-frontend/src/app/interfaces/e2-mgr.types.ts index c007d869..bc29cfe9 100644 --- a/webapp-frontend/src/app/interfaces/e2-mgr.types.ts +++ b/webapp-frontend/src/app/interfaces/e2-mgr.types.ts @@ -44,10 +44,10 @@ export interface E2NodebIdentity { export interface E2GetNodebResponse { connectionStatus: string; // actually one-of, but model as string enb: object; // don't model this until needed - failureType: string; // actually one-of, butmodel as string + failureType: string; // actually one-of, but model as string gnb: object; // don't model this until needed ip: string; - nodeType: object; // actually one-of, but model as string + nodeType: string; // actually one-of, but model as string port: number; // actually integer ranName: string; setupFailure: object; // don't model this until needed diff --git a/webapp-frontend/src/app/login/login.component.css b/webapp-frontend/src/app/login/login.component.scss similarity index 100% rename from webapp-frontend/src/app/login/login.component.css rename to webapp-frontend/src/app/login/login.component.scss diff --git a/webapp-frontend/src/app/login/login.component.ts b/webapp-frontend/src/app/login/login.component.ts index f7acfdb8..c6f694f7 100644 --- a/webapp-frontend/src/app/login/login.component.ts +++ b/webapp-frontend/src/app/login/login.component.ts @@ -22,7 +22,7 @@ import { Component, OnInit } from '@angular/core'; @Component({ selector: 'rd-login', templateUrl: './login.component.html', - styleUrls: ['./login.component.css'] + styleUrls: ['./login.component.scss'] }) export class LoginComponent implements OnInit { diff --git a/webapp-frontend/src/app/navigation/sidenav-list/sidenav-list.component.html b/webapp-frontend/src/app/navigation/sidenav-list/sidenav-list.component.html index e4753e98..2b586ffa 100644 --- a/webapp-frontend/src/app/navigation/sidenav-list/sidenav-list.component.html +++ b/webapp-frontend/src/app/navigation/sidenav-list/sidenav-list.component.html @@ -32,7 +32,7 @@ assessment Stats - - assignment_ind Admin + + assignment_ind Users diff --git a/webapp-frontend/src/app/navigation/sidenav-list/sidenav-list.component.css b/webapp-frontend/src/app/navigation/sidenav-list/sidenav-list.component.scss similarity index 100% rename from webapp-frontend/src/app/navigation/sidenav-list/sidenav-list.component.css rename to webapp-frontend/src/app/navigation/sidenav-list/sidenav-list.component.scss diff --git a/webapp-frontend/src/app/navigation/sidenav-list/sidenav-list.component.ts b/webapp-frontend/src/app/navigation/sidenav-list/sidenav-list.component.ts index cb4470ed..9548440f 100644 --- a/webapp-frontend/src/app/navigation/sidenav-list/sidenav-list.component.ts +++ b/webapp-frontend/src/app/navigation/sidenav-list/sidenav-list.component.ts @@ -22,7 +22,7 @@ import { Component, OnInit, Output, EventEmitter } from '@angular/core'; @Component({ selector: 'rd-sidenav-list', templateUrl: './sidenav-list.component.html', - styleUrls: ['./sidenav-list.component.css'] + styleUrls: ['./sidenav-list.component.scss'] }) export class SidenavListComponent implements OnInit { @Output() sidenavClose = new EventEmitter(); diff --git a/webapp-frontend/src/app/ran-control/ran-connection-dialog.component.css b/webapp-frontend/src/app/ran-control/ran-connection-dialog.component.scss similarity index 100% rename from webapp-frontend/src/app/ran-control/ran-connection-dialog.component.css rename to webapp-frontend/src/app/ran-control/ran-connection-dialog.component.scss diff --git a/webapp-frontend/src/app/ran-control/ran-connection-dialog.component.ts b/webapp-frontend/src/app/ran-control/ran-connection-dialog.component.ts index c146084b..7f0c368e 100644 --- a/webapp-frontend/src/app/ran-control/ran-connection-dialog.component.ts +++ b/webapp-frontend/src/app/ran-control/ran-connection-dialog.component.ts @@ -29,7 +29,7 @@ import { HttpErrorResponse } from '@angular/common/http'; @Component({ selector: 'rd-ran-control-connect-dialog', templateUrl: './ran-connection-dialog.component.html', - styleUrls: ['./ran-connection-dialog.component.css'] + styleUrls: ['./ran-connection-dialog.component.scss'] }) export class RanControlConnectDialogComponent implements OnInit { diff --git a/webapp-frontend/src/app/ran-control/ran-control.component.html b/webapp-frontend/src/app/ran-control/ran-control.component.html index 860e137b..474542aa 100644 --- a/webapp-frontend/src/app/ran-control/ran-control.component.html +++ b/webapp-frontend/src/app/ran-control/ran-control.component.html @@ -25,10 +25,6 @@ -
- -
- @@ -61,10 +57,18 @@ {{ran.nodebStatus.connectionStatus}} - + + No records found. + + +
+
+ +
+ diff --git a/webapp-frontend/src/app/ran-control/ran-control.component.scss b/webapp-frontend/src/app/ran-control/ran-control.component.scss index af4629ae..46fecd52 100644 --- a/webapp-frontend/src/app/ran-control/ran-control.component.scss +++ b/webapp-frontend/src/app/ran-control/ran-control.component.scss @@ -29,6 +29,10 @@ transform: translate(149 56); } +.disconnect-all-button { + float: right; +} + .ran-control-table { width: 100%; min-height: 100px; @@ -36,8 +40,13 @@ background-color:transparent; } -.disconnect-all-button { - float: right; +.spinner-container { + height: 100px; + width: 100px; +} + +.spinner-container mat-spinner { + margin: 0 auto 0 auto; } .version__text { @@ -45,3 +54,7 @@ letter-spacing: 0.1rem; font-size: 10px; } + +.display-none { + display: none; +} diff --git a/webapp-frontend/src/app/ran-control/ran-control.component.ts b/webapp-frontend/src/app/ran-control/ran-control.component.ts index ca9240f4..e911d18d 100644 --- a/webapp-frontend/src/app/ran-control/ran-control.component.ts +++ b/webapp-frontend/src/app/ran-control/ran-control.component.ts @@ -37,13 +37,13 @@ export class RanControlComponent implements OnInit { dataSource: RANControlDataSource; constructor(private e2MgrSvc: E2ManagerService, - private errorSvc: ErrorDialogService, + private errorDialogService: ErrorDialogService, private confirmDialogService: ConfirmDialogService, - private notification: NotificationService, + private notificationService: NotificationService, public dialog: MatDialog) { } ngOnInit() { - this.dataSource = new RANControlDataSource(this.e2MgrSvc); + this.dataSource = new RANControlDataSource(this.e2MgrSvc, this.notificationService); this.dataSource.loadTable(); } @@ -66,13 +66,13 @@ export class RanControlComponent implements OnInit { this.e2MgrSvc.nodebDelete().subscribe( response => { if (response.status === 200) { - this.notification.success('Disconnect all RAN Connections Succeeded!'); + this.notificationService.success('Disconnect all RAN Connections Succeeded!'); this.dataSource.loadTable(); } }, (error => { httpErrRes = error; - this.errorSvc.displayError(aboutError + httpErrRes.message); + this.errorDialogService.displayError(aboutError + httpErrRes.message); }) ); } diff --git a/webapp-frontend/src/app/ran-control/ran-control.datasource.ts b/webapp-frontend/src/app/ran-control/ran-control.datasource.ts index c84ab1ea..d919f4ea 100644 --- a/webapp-frontend/src/app/ran-control/ran-control.datasource.ts +++ b/webapp-frontend/src/app/ran-control/ran-control.datasource.ts @@ -19,12 +19,14 @@ */ import { CollectionViewer, DataSource } from '@angular/cdk/collections'; +import { HttpErrorResponse } from '@angular/common/http'; import { Observable } from 'rxjs/Observable'; -import { catchError, finalize } from 'rxjs/operators'; -import { of } from 'rxjs/observable/of'; import { BehaviorSubject } from 'rxjs/BehaviorSubject'; -import { E2RanDetails, E2SetupRequest } from '../interfaces/e2-mgr.types'; +import { of } from 'rxjs/observable/of'; +import { catchError, finalize } from 'rxjs/operators'; +import { E2RanDetails } from '../interfaces/e2-mgr.types'; import { E2ManagerService } from '../services/e2-mgr/e2-mgr.service'; +import { NotificationService } from '../services/ui/notification.service'; export class RANControlDataSource extends DataSource { @@ -34,7 +36,10 @@ export class RANControlDataSource extends DataSource { public loading$ = this.loadingSubject.asObservable(); - constructor(private e2MgrSvcservice: E2ManagerService) { + public rowCount = 1; // hide footer during intial load + + constructor(private e2MgrSvcservice: E2ManagerService, + private notificationService: NotificationService) { super(); } @@ -42,10 +47,17 @@ export class RANControlDataSource extends DataSource { this.loadingSubject.next(true); this.e2MgrSvcservice.getRan() .pipe( - catchError(() => of([])), - finalize(() => this.loadingSubject.next(false)) + catchError( (err: HttpErrorResponse) => { + console.log('RANControlDataSource failed: ' + err.message); + this.notificationService.error('Failed to get RAN details.'); + return of([]); + }), + finalize( () => this.loadingSubject.next(false) ) ) - .subscribe((ranControl: E2RanDetails[]) => this.ranControlSubject.next(ranControl)); + .subscribe( (ranControl: E2RanDetails[] ) => { + this.rowCount = ranControl.length; + this.ranControlSubject.next(ranControl); + }); } connect(collectionViewer: CollectionViewer): Observable { diff --git a/webapp-frontend/src/app/rd-routing.module.ts b/webapp-frontend/src/app/rd-routing.module.ts index cc5a06b8..1c80641c 100644 --- a/webapp-frontend/src/app/rd-routing.module.ts +++ b/webapp-frontend/src/app/rd-routing.module.ts @@ -20,23 +20,24 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Routes, RouterModule } from '@angular/router'; -import { LoginComponent } from './login/login.component'; -import { CatalogComponent } from './catalog/catalog.component'; -import { StatsComponent } from './stats/stats.component'; -import { UserComponent } from './admin/user.component'; + import { AcXappComponent } from './ac-xapp/ac-xapp.component'; import { AnrXappComponent } from './anr-xapp/anr-xapp.component'; +import { CatalogComponent } from './catalog/catalog.component'; import { ControlComponent } from './control/control.component'; +import { LoginComponent } from './login/login.component'; +import { StatsComponent } from './stats/stats.component'; +import { UserComponent } from './user/user.component'; const routes: Routes = [ {path: '', component: LoginComponent}, {path: 'login', component: LoginComponent}, {path: 'catalog', component: CatalogComponent}, {path: 'control', component: ControlComponent}, - {path: 'stats', component: StatsComponent}, - {path: 'admin', component: UserComponent}, {path: 'ac', component: AcXappComponent}, {path: 'anr', component: AnrXappComponent}, + {path: 'stats', component: StatsComponent}, + {path: 'user', component: UserComponent}, ]; @NgModule({ diff --git a/webapp-frontend/src/app/rd.component.css b/webapp-frontend/src/app/rd.component.scss similarity index 100% rename from webapp-frontend/src/app/rd.component.css rename to webapp-frontend/src/app/rd.component.scss diff --git a/webapp-frontend/src/app/rd.component.ts b/webapp-frontend/src/app/rd.component.ts index 8323b55a..e3ebe1ba 100644 --- a/webapp-frontend/src/app/rd.component.ts +++ b/webapp-frontend/src/app/rd.component.ts @@ -23,7 +23,7 @@ import { UiService } from './services/ui/ui.service'; @Component({ selector: 'rd-root', templateUrl: './rd.component.html', - styleUrls: ['./rd.component.css'] + styleUrls: ['./rd.component.scss'] }) export class RdComponent implements OnInit { showMenu = false; diff --git a/webapp-frontend/src/app/rd.module.ts b/webapp-frontend/src/app/rd.module.ts index 0e4595c5..fb70a4a0 100644 --- a/webapp-frontend/src/app/rd.module.ts +++ b/webapp-frontend/src/app/rd.module.ts @@ -33,9 +33,10 @@ import { MatTooltipModule } from '@angular/material/tooltip'; import { ChartsModule } from 'ng2-charts'; import { MDBBootstrapModule } from 'angular-bootstrap-md'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { ToastrModule } from 'ngx-toastr'; import { AcXappComponent } from './ac-xapp/ac-xapp.component'; -import { AddDashboardUserDialogComponent } from './admin/add-dashboard-user-dialog/add-dashboard-user-dialog.component'; +import { AddDashboardUserDialogComponent } from './user/add-dashboard-user-dialog/add-dashboard-user-dialog.component'; import { AnrEditNcrDialogComponent } from './anr-xapp/anr-edit-ncr-dialog.component'; import { AnrXappComponent } from './anr-xapp/anr-xapp.component'; import { AppControlComponent } from './app-control/app-control.component'; @@ -48,7 +49,7 @@ import { ControlCardComponent } from './ui/control-card/control-card.component'; import { ControlComponent } from './control/control.component'; import { DashboardService } from './services/dashboard/dashboard.service'; import { E2ManagerService } from './services/e2-mgr/e2-mgr.service'; -import { EditDashboardUserDialogComponent } from './admin/edit-dashboard-user-dialog/edit-dashboard-user-dialog.component'; +import { EditDashboardUserDialogComponent } from './user/edit-dashboard-user-dialog/edit-dashboard-user-dialog.component'; import { ErrorDialogComponent } from './ui/error-dialog/error-dialog.component'; import { ErrorDialogService } from './services/ui/error-dialog.service'; import { FooterComponent } from './footer/footer.component'; @@ -62,8 +63,7 @@ import { SidenavListComponent } from './navigation/sidenav-list/sidenav-list.com import { StatCardComponent } from './ui/stat-card/stat-card.component'; import { StatsComponent } from './stats/stats.component'; import { UiService } from './services/ui/ui.service'; -import { UserComponent } from './admin/user.component'; -import { ToastrModule } from 'ngx-toastr'; +import { UserComponent } from './user/user.component'; @NgModule({ declarations: [ diff --git a/webapp-frontend/src/app/stats/stats.component.ts b/webapp-frontend/src/app/stats/stats.component.ts index 97399692..aa4cd4ab 100644 --- a/webapp-frontend/src/app/stats/stats.component.ts +++ b/webapp-frontend/src/app/stats/stats.component.ts @@ -7,9 +7,9 @@ * 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. @@ -20,11 +20,7 @@ import { Component, OnInit, ViewChildren, QueryList } from '@angular/core'; import { BaseChartDirective } from 'ng2-charts/ng2-charts'; import { StatsService } from '../services/stats/stats.service'; -import { MatSlideToggleChange } from "@angular/material/slide-toggle"; import { HttpClient } from '@angular/common/http'; -import { HttpHeaders } from "@angular/common/http"; -import { Observable } from "rxjs"; -import { HttpErrorResponse } from "@angular/common/http"; import { map } from 'rxjs/operators'; @Component({ @@ -68,7 +64,7 @@ export class StatsComponent implements OnInit { // the data minimum used for determining the ticks is Math.min(dataMin, suggestedMin) suggestedMin: 0, // the data maximum used for determining the ticks is Math.max(dataMax, suggestedMax) -// suggestedMax: 1000 + // suggestedMax: 1000 }, scaleLabel: { display: true, @@ -118,7 +114,7 @@ export class StatsComponent implements OnInit { // the data minimum used for determining the ticks is Math.min(dataMin, suggestedMin) suggestedMin: 0, // the data maximum used for determining the ticks is Math.max(dataMax, suggestedMax) -// suggestedMax: 1000 + // suggestedMax: 1000 }, scaleLabel: { display: true, @@ -168,7 +164,7 @@ export class StatsComponent implements OnInit { // the data minimum used for determining the ticks is Math.min(dataMin, suggestedMin) suggestedMin: 0, // the data maximum used for determining the ticks is Math.max(dataMax, suggestedMax) -// suggestedMax: 1000 + // suggestedMax: 1000 }, scaleLabel: { display: true, @@ -191,12 +187,14 @@ export class StatsComponent implements OnInit { ]; public x = 11; - public y = 11; - public z = 11; public loop = true; + public sliderLoadMax = Number(this.service.loadMax) || 0; + + public sliderDelayMax = Number(this.service.delayMax) || 0; + latencyClickData() { // this.latencyChartData = [{data: [Math.random() * 100, Math.random() * 100, Math.random() * 100, // Math.random() * 100, Math.random() * 100, Math.random() * 100, Math.random() * 100, Math.random() * 100, @@ -238,8 +236,8 @@ export class StatsComponent implements OnInit { this.loadChartLabels.shift(); child.datasets[0].data.shift(); - //const loadData = this.service.getLoad(); - //child.datasets[0].data.push(this.service.load); + // const loadData = this.service.getLoad(); + // child.datasets[0].data.push(this.service.load); child.datasets[0].data.push(metricsv['load']); this.loadChartLabels.push('' + this.x++); } @@ -247,8 +245,8 @@ export class StatsComponent implements OnInit { this.latencyChartLabels.shift(); child.datasets[0].data.shift(); - //const loadData = this.service.getLoad(); - //child.datasets[0].data.push(this.service.load); + // const loadData = this.service.getLoad(); + // child.datasets[0].data.push(this.service.load); child.datasets[0].data.push(metricsv['latency']); this.latencyChartLabels.push('' + this.x++); } @@ -256,8 +254,8 @@ export class StatsComponent implements OnInit { this.latencyChartLabels.shift(); child.datasets[0].data.shift(); - //const loadData = this.service.getLoad(); - //child.datasets[0].data.push(this.service.load); + // const loadData = this.service.getLoad(); + // child.datasets[0].data.push(this.service.load); child.datasets[0].data.push(metricsv['ricload']); this.latencyChartLabels.push('' + this.x++); } @@ -290,13 +288,9 @@ export class StatsComponent implements OnInit { }); this.cpuChartLabels = [...this.cpuChartLabels, label]; - console.log(this.cpuChartLabels); - console.log(this.cpuChartData); + // console.log(this.cpuChartLabels); + // console.log(this.cpuChartData); } - - public sliderLoadMax = Number(this.service.loadMax) || 0; - - public sliderDelayMax = Number(this.service.delayMax) || 0; formatLabel(value: number | null) { if (!value) { @@ -312,23 +306,21 @@ export class StatsComponent implements OnInit { constructor(private service: StatsService, private httpClient: HttpClient) { this.sliderLoadMax = Number(this.service.loadMax) || 0; - this.sliderDelayMax = Number(this.service.delayMax) || 0; - console.log('this.sliderLoadMax: ' + this.sliderLoadMax); - console.log('this.sliderDelayMax: ' + this.sliderDelayMax); + // console.log('this.sliderLoadMax: ' + this.sliderLoadMax); + // console.log('this.sliderDelayMax: ' + this.sliderDelayMax); } ngOnInit() { this.fetchLoad().subscribe(loadv => { - console.log('loadv: ' + loadv); - this.checked = loadv; - }); + // console.log('loadv: ' + loadv); + this.checked = loadv; + }); this.fetchDelay().subscribe(delayv => { - console.log('delayv: ' + delayv); + // console.log('delayv: ' + delayv); this.delay = delayv; }); this.fetchMetrics().subscribe(metricsv => { - console.log('metricsv.load: ' + metricsv['load']); - + // console.log('metricsv.load: ' + metricsv['load']); }); } @@ -337,12 +329,12 @@ export class StatsComponent implements OnInit { if (this.timeLeft > 0) { this.timeLeft--; this.fetchMetrics().subscribe(metricsv => { - console.log('metricsv.load: ' + metricsv['latency']); - console.log('metricsv.load: ' + metricsv['load']); - console.log('metricsv.load: ' + metricsv['ricload']); + // console.log('metricsv.load: ' + metricsv['latency']); + // console.log('metricsv.load: ' + metricsv['load']); + // console.log('metricsv.load: ' + metricsv['ricload']); this.loopLoadData(metricsv); }); - + } else { this.timeLeft = 60; } @@ -352,45 +344,45 @@ export class StatsComponent implements OnInit { pauseLoadTimer() { clearInterval(this.interval); } - + fetchMetrics() { return this.httpClient.get(this.service.hostURL + this.service.metricsPath, this.service.httpOptions).pipe(map(res => { - console.log(res); - console.log(res['load']); + // console.log(res); + // console.log(res['load']); return res; })); } fetchDelay() { return this.httpClient.get(this.service.hostURL + this.service.delayPath, this.service.httpOptions).pipe(map(res => { - console.log(res); - console.log(res['delay']); + // console.log(res); + // console.log(res['delay']); const delayv = res['delay']; - console.log(delayv); + // console.log(delayv); this.delay = delayv; return this.delay; })); } - + saveDelay() { - console.log(this.delay); + // console.log(this.delay); this.service.putDelay(this.delay); } - + fetchLoad() { return this.httpClient.get(this.service.hostURL + this.service.loadPath, this.service.httpOptions).pipe(map(res => { - console.log(res); - console.log(res['load']); + // console.log(res); + // console.log(res['load']); const loadv = res['load']; - console.log(loadv); + // console.log(loadv); this.load = loadv; return this.load; })); - + } - + saveLoad() { - console.log(this.load); + // console.log(this.load); this.service.putLoad(this.load); } diff --git a/webapp-frontend/src/app/ui/catalog-card/catalog-card.component.css b/webapp-frontend/src/app/ui/catalog-card/catalog-card.component.scss similarity index 100% rename from webapp-frontend/src/app/ui/catalog-card/catalog-card.component.css rename to webapp-frontend/src/app/ui/catalog-card/catalog-card.component.scss diff --git a/webapp-frontend/src/app/ui/catalog-card/catalog-card.component.ts b/webapp-frontend/src/app/ui/catalog-card/catalog-card.component.ts index da35f8dd..e7aeeb7b 100644 --- a/webapp-frontend/src/app/ui/catalog-card/catalog-card.component.ts +++ b/webapp-frontend/src/app/ui/catalog-card/catalog-card.component.ts @@ -7,9 +7,9 @@ * 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. @@ -24,7 +24,7 @@ import {UiService} from '../../services/ui/ui.service'; @Component({ selector: 'rd-app-catalog-card', templateUrl: './catalog-card.component.html', - styleUrls: ['./catalog-card.component.css'] + styleUrls: ['./catalog-card.component.scss'] }) export class CatalogCardComponent implements OnInit, OnDestroy { darkMode: boolean; diff --git a/webapp-frontend/src/app/ui/control-card/control-card.component.css b/webapp-frontend/src/app/ui/control-card/control-card.component.scss similarity index 100% rename from webapp-frontend/src/app/ui/control-card/control-card.component.css rename to webapp-frontend/src/app/ui/control-card/control-card.component.scss diff --git a/webapp-frontend/src/app/ui/control-card/control-card.component.ts b/webapp-frontend/src/app/ui/control-card/control-card.component.ts index e8599bca..4a463ada 100644 --- a/webapp-frontend/src/app/ui/control-card/control-card.component.ts +++ b/webapp-frontend/src/app/ui/control-card/control-card.component.ts @@ -24,7 +24,7 @@ import {UiService} from '../../services/ui/ui.service'; @Component({ selector: 'rd-control-card', templateUrl: './control-card.component.html', - styleUrls: ['./control-card.component.css'] + styleUrls: ['./control-card.component.scss'] }) export class ControlCardComponent implements OnInit, OnDestroy { darkMode: boolean; diff --git a/webapp-frontend/src/app/ui/stat-card/stat-card.component.css b/webapp-frontend/src/app/ui/stat-card/stat-card.component.scss similarity index 100% rename from webapp-frontend/src/app/ui/stat-card/stat-card.component.css rename to webapp-frontend/src/app/ui/stat-card/stat-card.component.scss diff --git a/webapp-frontend/src/app/ui/stat-card/stat-card.component.ts b/webapp-frontend/src/app/ui/stat-card/stat-card.component.ts index a34d4f87..b404bf8b 100644 --- a/webapp-frontend/src/app/ui/stat-card/stat-card.component.ts +++ b/webapp-frontend/src/app/ui/stat-card/stat-card.component.ts @@ -24,7 +24,7 @@ import {UiService} from '../../services/ui/ui.service'; @Component({ selector: 'rd-stat-card', templateUrl: './stat-card.component.html', - styleUrls: ['./stat-card.component.css'] + styleUrls: ['./stat-card.component.scss'] }) export class StatCardComponent implements OnInit, OnDestroy { darkMode: boolean; diff --git a/webapp-frontend/src/app/admin/add-dashboard-user-dialog/add-dashboard-user-dialog.component.html b/webapp-frontend/src/app/user/add-dashboard-user-dialog/add-dashboard-user-dialog.component.html similarity index 100% rename from webapp-frontend/src/app/admin/add-dashboard-user-dialog/add-dashboard-user-dialog.component.html rename to webapp-frontend/src/app/user/add-dashboard-user-dialog/add-dashboard-user-dialog.component.html diff --git a/webapp-frontend/src/app/admin/add-dashboard-user-dialog/add-dashboard-user-dialog.component.scss b/webapp-frontend/src/app/user/add-dashboard-user-dialog/add-dashboard-user-dialog.component.scss similarity index 100% rename from webapp-frontend/src/app/admin/add-dashboard-user-dialog/add-dashboard-user-dialog.component.scss rename to webapp-frontend/src/app/user/add-dashboard-user-dialog/add-dashboard-user-dialog.component.scss diff --git a/webapp-frontend/src/app/admin/add-dashboard-user-dialog/add-dashboard-user-dialog.component.ts b/webapp-frontend/src/app/user/add-dashboard-user-dialog/add-dashboard-user-dialog.component.ts similarity index 100% rename from webapp-frontend/src/app/admin/add-dashboard-user-dialog/add-dashboard-user-dialog.component.ts rename to webapp-frontend/src/app/user/add-dashboard-user-dialog/add-dashboard-user-dialog.component.ts diff --git a/webapp-frontend/src/app/admin/edit-dashboard-user-dialog/edit-dashboard-user-dialog.component.html b/webapp-frontend/src/app/user/edit-dashboard-user-dialog/edit-dashboard-user-dialog.component.html similarity index 100% rename from webapp-frontend/src/app/admin/edit-dashboard-user-dialog/edit-dashboard-user-dialog.component.html rename to webapp-frontend/src/app/user/edit-dashboard-user-dialog/edit-dashboard-user-dialog.component.html diff --git a/webapp-frontend/src/app/admin/edit-dashboard-user-dialog/edit-dashboard-user-dialog.component.scss b/webapp-frontend/src/app/user/edit-dashboard-user-dialog/edit-dashboard-user-dialog.component.scss similarity index 100% rename from webapp-frontend/src/app/admin/edit-dashboard-user-dialog/edit-dashboard-user-dialog.component.scss rename to webapp-frontend/src/app/user/edit-dashboard-user-dialog/edit-dashboard-user-dialog.component.scss diff --git a/webapp-frontend/src/app/admin/edit-dashboard-user-dialog/edit-dashboard-user-dialog.component.ts b/webapp-frontend/src/app/user/edit-dashboard-user-dialog/edit-dashboard-user-dialog.component.ts similarity index 100% rename from webapp-frontend/src/app/admin/edit-dashboard-user-dialog/edit-dashboard-user-dialog.component.ts rename to webapp-frontend/src/app/user/edit-dashboard-user-dialog/edit-dashboard-user-dialog.component.ts diff --git a/webapp-frontend/src/app/admin/user.component.html b/webapp-frontend/src/app/user/user.component.html similarity index 100% rename from webapp-frontend/src/app/admin/user.component.html rename to webapp-frontend/src/app/user/user.component.html diff --git a/webapp-frontend/src/app/admin/user.component.css b/webapp-frontend/src/app/user/user.component.scss similarity index 100% rename from webapp-frontend/src/app/admin/user.component.css rename to webapp-frontend/src/app/user/user.component.scss diff --git a/webapp-frontend/src/app/admin/user.component.spec.ts b/webapp-frontend/src/app/user/user.component.spec.ts similarity index 100% rename from webapp-frontend/src/app/admin/user.component.spec.ts rename to webapp-frontend/src/app/user/user.component.spec.ts diff --git a/webapp-frontend/src/app/admin/user.component.ts b/webapp-frontend/src/app/user/user.component.ts similarity index 90% rename from webapp-frontend/src/app/admin/user.component.ts rename to webapp-frontend/src/app/user/user.component.ts index 5d631c8d..482a1c0f 100644 --- a/webapp-frontend/src/app/admin/user.component.ts +++ b/webapp-frontend/src/app/user/user.component.ts @@ -23,7 +23,6 @@ import { MatSort } from '@angular/material/sort'; import { DashboardService } from '../services/dashboard/dashboard.service'; import { ErrorDialogService } from '../services/ui/error-dialog.service'; import { DashboardUser } from './../interfaces/dashboard.types'; -import { ConfirmDialogService } from './../services/ui/confirm-dialog.service'; import { NotificationService } from './../services/ui/notification.service'; import { UserDataSource } from './user.datasource'; import { AddDashboardUserDialogComponent } from './add-dashboard-user-dialog/add-dashboard-user-dialog.component'; @@ -32,7 +31,7 @@ import { EditDashboardUserDialogComponent } from './edit-dashboard-user-dialog/e @Component({ selector: 'rd-user', templateUrl: './user.component.html', - styleUrls: ['./user.component.css'] + styleUrls: ['./user.component.scss'] }) export class UserComponent implements OnInit { @@ -43,13 +42,12 @@ export class UserComponent implements OnInit { constructor( private dashboardSvc: DashboardService, - private confirmDialogService: ConfirmDialogService, private errorService: ErrorDialogService, - private notification: NotificationService, + private notificationService: NotificationService, public dialog: MatDialog) { } ngOnInit() { - this.dataSource = new UserDataSource(this.dashboardSvc, this.sort); + this.dataSource = new UserDataSource(this.dashboardSvc, this.sort, this.notificationService); this.dataSource.loadTable(); } @@ -64,7 +62,7 @@ export class UserComponent implements OnInit { } deleteUser() { - const aboutError = 'Not implemented yet'; + const aboutError = 'Not implemented (yet).'; this.errorService.displayError(aboutError); } diff --git a/webapp-frontend/src/app/admin/user.datasource.ts b/webapp-frontend/src/app/user/user.datasource.ts similarity index 64% rename from webapp-frontend/src/app/admin/user.datasource.ts rename to webapp-frontend/src/app/user/user.datasource.ts index 5b57d6b2..dcc03690 100644 --- a/webapp-frontend/src/app/admin/user.datasource.ts +++ b/webapp-frontend/src/app/user/user.datasource.ts @@ -19,49 +19,62 @@ */ import { CollectionViewer, DataSource } from '@angular/cdk/collections'; +import { HttpErrorResponse } from '@angular/common/http'; import { MatSort } from '@angular/material'; -import { merge } from 'rxjs'; -import { BehaviorSubject } from 'rxjs/BehaviorSubject'; import { Observable } from 'rxjs/Observable'; +import { BehaviorSubject } from 'rxjs/BehaviorSubject'; +import { merge } from 'rxjs'; import { of } from 'rxjs/observable/of'; import { catchError, finalize, map } from 'rxjs/operators'; import { DashboardUser } from '../interfaces/dashboard.types'; import { DashboardService } from '../services/dashboard/dashboard.service'; +import { NotificationService } from '../services/ui/notification.service'; export class UserDataSource extends DataSource { - private usersSubject = new BehaviorSubject([]); + private userSubject = new BehaviorSubject([]); private loadingSubject = new BehaviorSubject(false); public loading$ = this.loadingSubject.asObservable(); - constructor(private dashboardSvc: DashboardService, private sort: MatSort) { + public rowCount = 1; // hide footer during intial load + + constructor(private dashboardSvc: DashboardService, + private sort: MatSort, + private notificationService: NotificationService) { super(); - }; + } loadTable() { this.loadingSubject.next(true); this.dashboardSvc.getUsers() .pipe( - catchError(() => of([])), + catchError( (err: HttpErrorResponse) => { + console.log('UserDataSource failed: ' + err.message); + this.notificationService.error('Failed to get users.'); + return of([]); + }), finalize(() => this.loadingSubject.next(false)) ) - .subscribe(Users => this.usersSubject.next(Users)) + .subscribe( (users: DashboardUser[]) => { + this.rowCount = users.length; + this.userSubject.next(users); + }); } connect(collectionViewer: CollectionViewer): Observable { const dataMutations = [ - this.usersSubject.asObservable(), + this.userSubject.asObservable(), this.sort.sortChange ]; return merge(...dataMutations).pipe(map(() => { - return this.getSortedData([...this.usersSubject.getValue()]); + return this.getSortedData([...this.userSubject.getValue()]); })); } disconnect(collectionViewer: CollectionViewer): void { - this.usersSubject.complete(); + this.userSubject.complete(); this.loadingSubject.complete(); } @@ -73,16 +86,17 @@ export class UserDataSource extends DataSource { return data.sort((a: DashboardUser, b: DashboardUser) => { const isAsc = this.sort.direction === 'asc'; switch (this.sort.active) { - case 'id': return compare(a.id, b.id, isAsc); - case 'firstName': return compare(a.firstName, b.firstName, isAsc); - case 'lastName': return compare(a.lastName, b.lastName, isAsc); - case 'status': return compare(a.status, b.status, isAsc); + case 'id': return this.compare(a.id, b.id, isAsc); + case 'firstName': return this.compare(a.firstName, b.firstName, isAsc); + case 'lastName': return this.compare(a.lastName, b.lastName, isAsc); + case 'status': return this.compare(a.status, b.status, isAsc); default: return 0; } }); } -} -function compare(a, b, isAsc: boolean) { - return (a < b ? -1 : 1) * (isAsc ? 1 : -1); + private compare(a: any, b: any, isAsc: boolean) { + return (a < b ? -1 : 1) * (isAsc ? 1 : -1); + } + } diff --git a/webapp-frontend/src/tslint.json b/webapp-frontend/src/tslint.json index 52e2c1a5..30581c63 100644 --- a/webapp-frontend/src/tslint.json +++ b/webapp-frontend/src/tslint.json @@ -4,13 +4,13 @@ "directive-selector": [ true, "attribute", - "app", + "rd", "camelCase" ], "component-selector": [ true, "element", - "app", + "rd", "kebab-case" ] }