Add error handling to improve user experience 56/456/7
authorLott, Christopher (cl778h) <cl778h@att.com>
Fri, 28 Jun 2019 12:17:04 +0000 (08:17 -0400)
committerLott, Christopher (cl778h) <cl778h@att.com>
Fri, 5 Jul 2019 12:44:55 +0000 (08:44 -0400)
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) <cl778h@att.com>
56 files changed:
docs/release-notes.rst
webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/AcXappController.java
webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/AnrXappController.java
webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/AppManagerController.java
webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/CustomizedResponseEntityExceptionHandler.java [new file with mode: 0644]
webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/E2ManagerController.java
webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/SimpleErrorController.java
webapp-frontend/src/app/anr-xapp/anr-xapp.component.html
webapp-frontend/src/app/anr-xapp/anr-xapp.component.scss
webapp-frontend/src/app/anr-xapp/anr-xapp.component.ts
webapp-frontend/src/app/anr-xapp/anr-xapp.datasource.ts
webapp-frontend/src/app/app-control/app-control.component.html
webapp-frontend/src/app/app-control/app-control.component.scss [moved from webapp-frontend/src/app/app-control/app-control.component.css with 92% similarity]
webapp-frontend/src/app/app-control/app-control.component.ts
webapp-frontend/src/app/app-control/app-control.datasource.ts
webapp-frontend/src/app/catalog/catalog.component.html
webapp-frontend/src/app/catalog/catalog.component.scss [moved from webapp-frontend/src/app/catalog/catalog.component.css with 93% similarity]
webapp-frontend/src/app/catalog/catalog.component.ts
webapp-frontend/src/app/catalog/catalog.datasource.ts
webapp-frontend/src/app/control/control.component.scss [moved from webapp-frontend/src/app/control/control.component.css with 100% similarity]
webapp-frontend/src/app/control/control.component.ts
webapp-frontend/src/app/interfaces/e2-mgr.types.ts
webapp-frontend/src/app/login/login.component.scss [moved from webapp-frontend/src/app/login/login.component.css with 100% similarity]
webapp-frontend/src/app/login/login.component.ts
webapp-frontend/src/app/navigation/sidenav-list/sidenav-list.component.html
webapp-frontend/src/app/navigation/sidenav-list/sidenav-list.component.scss [moved from webapp-frontend/src/app/navigation/sidenav-list/sidenav-list.component.css with 100% similarity]
webapp-frontend/src/app/navigation/sidenav-list/sidenav-list.component.ts
webapp-frontend/src/app/ran-control/ran-connection-dialog.component.scss [moved from webapp-frontend/src/app/ran-control/ran-connection-dialog.component.css with 100% similarity]
webapp-frontend/src/app/ran-control/ran-connection-dialog.component.ts
webapp-frontend/src/app/ran-control/ran-control.component.html
webapp-frontend/src/app/ran-control/ran-control.component.scss
webapp-frontend/src/app/ran-control/ran-control.component.ts
webapp-frontend/src/app/ran-control/ran-control.datasource.ts
webapp-frontend/src/app/rd-routing.module.ts
webapp-frontend/src/app/rd.component.scss [moved from webapp-frontend/src/app/rd.component.css with 100% similarity]
webapp-frontend/src/app/rd.component.ts
webapp-frontend/src/app/rd.module.ts
webapp-frontend/src/app/stats/stats.component.ts
webapp-frontend/src/app/ui/catalog-card/catalog-card.component.scss [moved from webapp-frontend/src/app/ui/catalog-card/catalog-card.component.css with 100% similarity]
webapp-frontend/src/app/ui/catalog-card/catalog-card.component.ts
webapp-frontend/src/app/ui/control-card/control-card.component.scss [moved from webapp-frontend/src/app/ui/control-card/control-card.component.css with 100% similarity]
webapp-frontend/src/app/ui/control-card/control-card.component.ts
webapp-frontend/src/app/ui/stat-card/stat-card.component.scss [moved from webapp-frontend/src/app/ui/stat-card/stat-card.component.css with 100% similarity]
webapp-frontend/src/app/ui/stat-card/stat-card.component.ts
webapp-frontend/src/app/user/add-dashboard-user-dialog/add-dashboard-user-dialog.component.html [moved from webapp-frontend/src/app/admin/add-dashboard-user-dialog/add-dashboard-user-dialog.component.html with 100% similarity]
webapp-frontend/src/app/user/add-dashboard-user-dialog/add-dashboard-user-dialog.component.scss [moved from webapp-frontend/src/app/admin/add-dashboard-user-dialog/add-dashboard-user-dialog.component.scss with 100% similarity]
webapp-frontend/src/app/user/add-dashboard-user-dialog/add-dashboard-user-dialog.component.ts [moved from webapp-frontend/src/app/admin/add-dashboard-user-dialog/add-dashboard-user-dialog.component.ts with 100% similarity]
webapp-frontend/src/app/user/edit-dashboard-user-dialog/edit-dashboard-user-dialog.component.html [moved from webapp-frontend/src/app/admin/edit-dashboard-user-dialog/edit-dashboard-user-dialog.component.html with 100% similarity]
webapp-frontend/src/app/user/edit-dashboard-user-dialog/edit-dashboard-user-dialog.component.scss [moved from webapp-frontend/src/app/admin/edit-dashboard-user-dialog/edit-dashboard-user-dialog.component.scss with 100% similarity]
webapp-frontend/src/app/user/edit-dashboard-user-dialog/edit-dashboard-user-dialog.component.ts [moved from webapp-frontend/src/app/admin/edit-dashboard-user-dialog/edit-dashboard-user-dialog.component.ts with 100% similarity]
webapp-frontend/src/app/user/user.component.html [moved from webapp-frontend/src/app/admin/user.component.html with 100% similarity]
webapp-frontend/src/app/user/user.component.scss [moved from webapp-frontend/src/app/admin/user.component.css with 100% similarity]
webapp-frontend/src/app/user/user.component.spec.ts [moved from webapp-frontend/src/app/admin/user.component.spec.ts with 100% similarity]
webapp-frontend/src/app/user/user.component.ts [moved from webapp-frontend/src/app/admin/user.component.ts with 90% similarity]
webapp-frontend/src/app/user/user.datasource.ts [moved from webapp-frontend/src/app/admin/user.datasource.ts with 64% similarity]
webapp-frontend/src/tslint.json

index ecc4145..7a9e784 100644 (file)
 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
 
index 279d504..46a4aab 100644 (file)
@@ -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());
        }
 
 }
index 9faeff7..2449caa 100644 (file)
@@ -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());
        }
 
 }
index e8ad8a9..9c356ca 100644 (file)
@@ -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 (file)
index 0000000..95bcfb4
--- /dev/null
@@ -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: <blockquote>HTTP server received an invalid response from a
+ * server it consulted when acting as a proxy or gateway.</blockquote>
+ *
+ * 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:<br>
+ * 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<ErrorTransport>(
+                               new ErrorTransport(ex.getRawStatusCode(), ex.getResponseBodyAsString(), ex), HttpStatus.BAD_GATEWAY);
+       }
+
+}
index 631515b..a4533b6 100644 (file)
@@ -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<ErrorTransport>(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<RanDetailsTransport> getRanDetails() {
                logger.debug("getRanDetails");
-               List<NodebIdentity> 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<ErrorTransport>(new ErrorTransport(ex.getRawStatusCode(), ex.toString()),
-                                       HttpStatus.BAD_GATEWAY);
-               }
+               // TODO: remove mock when e2mgr delivers the getNodebIdList() method
+               List<NodebIdentity> nodebIdList = mockNodebIdList.isEmpty() ? e2NodebApi.getNodebIdList() : mockNodebIdList;
                List<RanDetailsTransport> 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<NodebIdentity> getNodebIdList() {
                logger.debug("getNodebIdList");
-               try {
-                       return e2NodebApi.getNodebIdList();
-               } catch (HttpStatusCodeException ex) {
-                       logger.warn("getNodebIdList failed: {}", ex.toString());
-                       return new ResponseEntity<ErrorTransport>(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<ErrorTransport>(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<ErrorTransport>(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<ErrorTransport>(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<ErrorTransport>(new ErrorTransport(ex.getRawStatusCode(), ex.toString()),
-                                       HttpStatus.BAD_GATEWAY);
-               }
+               e2NodebApi.x2Setup(setupRequest);
+               response.setStatus(e2NodebApi.getApiClient().getStatusCode().value());
        }
 
 }
index a1678be..859c913 100644 (file)
  */
 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<String, Object> error(HttpServletRequest aRequest) {
                Map<String, Object> 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;
index 09e676d..4cdd55e 100644 (file)
       <input matInput placeholder="Neighbor Cell NRPCI" #neighborCellNrpci>
   </mat-form-field>
 
-  <div class="spinner-container" *ngIf="dataSource.loading$ | async">
-    <mat-spinner></mat-spinner>
-  </div>
-
   <table mat-table class="ncr-table mat-elevation-z8" [dataSource]="dataSource" matSort>
 
     <ng-container matColumnDef="cellIdentifierNrcgi">
            </mat-cell>
          </ng-container>
 
-    <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
+    <ng-container matColumnDef="noRecordsFound">
+      <mat-footer-cell *matFooterCellDef>No records found.</mat-footer-cell>
+    </ng-container>
 
+    <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
     <mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
+    <mat-footer-row *matFooterRowDef="['noRecordsFound']" [ngClass]="{'display-none': dataSource.rowCount > 0}"></mat-footer-row>
 
   </table>
 
-  <div class="version__text">
-      ANR client version {{anrClientVersion}}
+  <div class="spinner-container" *ngIf="dataSource.loading$ | async">
+    <mat-spinner></mat-spinner>
   </div>
+
 </div>
index 094a32d..5d8c3ff 100644 (file)
 }
 
 .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
index 3440482..ba24071 100644 (file)
@@ -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() {
index bc04b6b..cbf353f 100644 (file)
  */
 
 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<ANRNeighborCellRelation> {
 
-    private relationsSubject = new BehaviorSubject<ANRNeighborCellRelation[]>([]);
+  private relationsSubject = new BehaviorSubject<ANRNeighborCellRelation[]>([]);
 
-    private loadingSubject = new BehaviorSubject<boolean>(false);
+  private loadingSubject = new BehaviorSubject<boolean>(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<ANRNeighborCellRelation[]> {
-      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<ANRNeighborCellRelation[]> {
+    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<ANRNeighborCellRelation> {
       }
     });
   }
-
 }
 
-function compare(a, b, isAsc) {
+function compare(a: any, b: any, isAsc: boolean) {
   return (a < b ? -1 : 1) * (isAsc ? 1 : -1);
 }
index f8a9d4c..6c8a453 100644 (file)
@@ -20,9 +20,7 @@
 
 <div class="app-control__section">
   <h3 class="app-control__header">xApp Control</h3>
-  <div class="spinner-container" *ngIf="dataSource.loading$ | async">
-    <mat-spinner></mat-spinner>
-  </div>
+
   <table mat-table [dataSource]="dataSource" matSort multiTemplateDataRows class="app-control-table mat-elevation-z8">
 
     <ng-container matColumnDef="xapp">
       </td>
     </ng-container>
 
+    <ng-container matColumnDef="noRecordsFound">
+      <mat-footer-cell *matFooterCellDef>No records found.</mat-footer-cell>
+    </ng-container>
+
     <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
     <mat-row *matRowDef="let element; columns: displayedColumns;"
       [class.example-expanded-row]="expandedElement === element"
       (click)="expandedElement = expandedElement === element ? null : element"></mat-row>
     <tr mat-row *matRowDef="let row; columns: ['expandedDetail']" class="message-row"></tr>
+    <mat-footer-row *matFooterRowDef="['noRecordsFound']" [ngClass]="{'display-none': dataSource.rowCount > 0}"></mat-footer-row>
+
   </table>
+
+  <div class="spinner-container" *ngIf="dataSource.loading$ | async">
+    <mat-spinner></mat-spinner>
+  </div>
+
 </div>
 }
 
 .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;
+}
index f87b9b5..b172152 100644 (file)
@@ -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.');
               }
             }
           );
index cf0c3dc..fa98dfa 100644 (file)
  */
 
 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<XappControlRow> {
 
-  private xAppInstancesSubject = new BehaviorSubject<XappControlRow[]>([]);
+  private appControlSubject = new BehaviorSubject<XappControlRow[]>([]);
 
   private loadingSubject = new BehaviorSubject<boolean>(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<XappControlRow> {
       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<XappControlRow> {
     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<XappControlRow[]> {
     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<XappControlRow> {
   }
 }
 
-function compare(a, b, isAsc) {
+function compare(a: any, b: any, isAsc: boolean) {
   return (a < b ? -1 : 1) * (isAsc ? 1 : -1);
 }
index 6f2e06d..2a9b72d 100644 (file)
@@ -19,9 +19,7 @@
   -->
 <div class="catalog__section">
   <h3 class="catalog__header">xApp Catalog</h3>
-  <div class="spinner-container" *ngIf="dataSource.loading$ | async">
-    <mat-spinner></mat-spinner>
-  </div>
+
   <table mat-table [dataSource]="dataSource" matSort class="catalog-table mat-elevation-z8">
 
     <ng-container matColumnDef="name">
       </mat-cell>
     </ng-container>
 
+    <ng-container matColumnDef="noRecordsFound">
+      <mat-footer-cell *matFooterCellDef>No records found.</mat-footer-cell>
+    </ng-container>
+
     <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
     <mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
+    <mat-footer-row *matFooterRowDef="['noRecordsFound']" [ngClass]="{'display-none': dataSource.rowCount > 0}"></mat-footer-row>
+
   </table>
+
+  <div class="spinner-container" *ngIf="dataSource.loading$ | async">
+    <mat-spinner></mat-spinner>
+  </div>
+
 </div>
@@ -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;
+}
index 5781480..97dfd7c 100644 (file)
@@ -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<object>) => {
-              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);
             }
           );
         }
index e47abfd..5c9ac94 100644 (file)
  */
 
 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<XMDeployableApp> {
 
-  private xAppsSubject = new BehaviorSubject<XMDeployableApp[]>([]);
+  private catalogSubject = new BehaviorSubject<XMDeployableApp[]>([]);
 
   private loadingSubject = new BehaviorSubject<boolean>(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<XMDeployableApp> {
     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<XMDeployableApp[]> {
     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<XMDeployableApp> {
     });
   }
 
-  private compare(a: string, b: string, isAsc: boolean) {
+  private compare(a: any, b: any, isAsc: boolean) {
     return (a < b ? -1 : 1) * (isAsc ? 1 : -1);
   }
 
 }
-
index eec53e3..8ba04a4 100644 (file)
@@ -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 {
 
index c007d86..bc29cfe 100644 (file)
@@ -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
index f7acfdb..c6f694f 100644 (file)
@@ -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 {
 
index e4753e9..2b586ff 100644 (file)
@@ -32,7 +32,7 @@
   <a mat-list-item routerLink="/stats" (click)="onSidenavClose()">
       <mat-icon>assessment</mat-icon> <span class="nav-caption">Stats</span>
   </a>
-  <a mat-list-item routerLink="/admin" (click)="onSidenavClose()">
-      <mat-icon>assignment_ind</mat-icon> <span class="nav-caption">Admin</span>
+  <a mat-list-item routerLink="/user" (click)="onSidenavClose()">
+      <mat-icon>assignment_ind</mat-icon> <span class="nav-caption">Users</span>
   </a>
 </mat-nav-list>
index cb4470e..9548440 100644 (file)
@@ -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();
index c146084..7f0c368 100644 (file)
@@ -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 {
index 860e137..474542a 100644 (file)
   <button mat-raised-button color="warn" class="disconnect-all-button"
     (click)="disconnectAllRANConnections()">Disconnect All</button>
 
-  <div class="spinner-container" *ngIf="dataSource.loading$ | async">
-    <mat-spinner></mat-spinner>
-  </div>
-
   <table mat-table class="ran-control-table mat-elevation-z8" [dataSource]="dataSource">
 
     <ng-container matColumnDef="nbId">
       <mat-cell *matCellDef="let ran">{{ran.nodebStatus.connectionStatus}}</mat-cell>
     </ng-container>
 
-    <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
+    <ng-container matColumnDef="noRecordsFound">
+      <mat-footer-cell *matFooterCellDef>No records found.</mat-footer-cell>
+    </ng-container>
 
+    <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
     <mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
+    <mat-footer-row *matFooterRowDef="['noRecordsFound']" [ngClass]="{'display-none': dataSource.rowCount > 0}"></mat-footer-row>
 
   </table>
 
+  <div class="spinner-container" *ngIf="dataSource.loading$ | async">
+    <mat-spinner></mat-spinner>
+  </div>
+
 </div>
index af4629a..46fecd5 100644 (file)
     transform: translate(149 56);
 }
 
+.disconnect-all-button {
+    float: right;
+}
+
 .ran-control-table {
     width: 100%;
     min-height: 100px;
     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;
+}
index ca9240f..e911d18 100644 (file)
@@ -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);
             })
           );
         }
index c84ab1e..d919f4e 100644 (file)
  */
 
 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<E2RanDetails> {
 
@@ -34,7 +36,10 @@ export class RANControlDataSource extends DataSource<E2RanDetails> {
 
   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<E2RanDetails> {
     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<E2RanDetails[]> {
index cc5a06b..1c80641 100644 (file)
 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({
index 8323b55..e3ebe1b 100644 (file)
@@ -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;
index 0e4595c..fb70a4a 100644 (file)
@@ -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: [
index 9739969..aa4cd4a 100644 (file)
@@ -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.
 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<any[]>(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<any[]>(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<any[]>(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);
     }
 
index da35f8d..e7aeeb7 100644 (file)
@@ -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;
index e8599bc..4a463ad 100644 (file)
@@ -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;
index a34d4f8..b404bf8 100644 (file)
@@ -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;
@@ -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);
   }
 
  */
 
 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<DashboardUser> {
 
-  private usersSubject = new BehaviorSubject<DashboardUser[]>([]);
+  private userSubject = new BehaviorSubject<DashboardUser[]>([]);
 
   private loadingSubject = new BehaviorSubject<boolean>(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<DashboardUser[]> {
     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<DashboardUser> {
     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);
+  }
+
 }
index 52e2c1a..30581c6 100644 (file)
@@ -4,13 +4,13 @@
         "directive-selector": [
             true,
             "attribute",
-            "app",
+            "rd",
             "camelCase"
         ],
         "component-selector": [
             true,
             "element",
-            "app",
+            "rd",
             "kebab-case"
         ]
     }