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
---------------------------
* 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
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;
*/
@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());
}
}
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.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;
@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());
}
}
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;
@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());
}
}
--- /dev/null
+/*-
+ * ===============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);
+ }
+
+}
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;
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;
@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());
}
@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());
}
}
*/
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;
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.
*
@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;
@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;
<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>
}
.spinner-container {
- height: 360px;
- width: 390px;
- position: fixed;
-}
+ height: 100px;
+ width: 100px;
+ }
.spinner-container mat-spinner {
margin: 130px auto 0 auto;
letter-spacing: 0.1rem;
font-size: 10px;
}
+
+.display-none {
+ display: none;
+ }
+
\ No newline at end of file
export class AnrXappComponent implements AfterViewInit, OnInit {
dataSource: ANRXappDataSource;
- anrClientVersion: string;
gNodeBIds: string[];
@ViewChild('ggNodeB', {static: true}) ggNodeB: ElementRef;
@ViewChild('servingCellNrcgi', {static: true}) servingCellNrcgi: ElementRef;
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() {
*/
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);
}
});
}
-
}
-function compare(a, b, isAsc) {
+function compare(a: any, b: any, isAsc: boolean) {
return (a < b ? -1 : 1) * (isAsc ? 1 : -1);
}
<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 {
tr.message-row {
height: 0;
}
+
+.display-none {
+ display: none;
+}
@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 {
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();
}
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.');
}
}
);
*/
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,
txMessages: [],
};
- constructor(private appMgrSvc: AppMgrService, private sort: MatSort) {
+ constructor(private appMgrSvc: AppMgrService,
+ private sort: MatSort,
+ private notificationService: NotificationService) {
super();
}
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) {
}
}
-function compare(a, b, isAsc) {
+function compare(a: any, b: any, isAsc: boolean) {
return (a < b ? -1 : 1) * (isAsc ? 1 : -1);
}
-->
<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>
}
.spinner-container {
- height: 360px;
- width: 390px;
- position: fixed;
+ height: 100px;
+ width: 100px;
}
.spinner-container mat-spinner {
}
.catalog-table {
- width: 99%;
+ width: 100%;
min-height: 150px;
margin-top: 10px;
background-color: transparent;
.catalog-button-row button{
margin-right: 5px;
}
+
+.display-none {
+ display: none;
+}
@Component({
selector: 'rd-app-catalog',
templateUrl: './catalog.component.html',
- styleUrls: ['./catalog.component.css'],
+ styleUrls: ['./catalog.component.scss'],
})
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);
}
);
}
*/
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();
}
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();
}
});
}
- private compare(a: string, b: string, isAsc: boolean) {
+ private compare(a: any, b: any, isAsc: boolean) {
return (a < b ? -1 : 1) * (isAsc ? 1 : -1);
}
}
-
@Component({
selector: 'rd-control',
templateUrl: './control.component.html',
- styleUrls: ['./control.component.css']
+ styleUrls: ['./control.component.scss']
})
export class ControlComponent implements OnInit {
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
@Component({
selector: 'rd-login',
templateUrl: './login.component.html',
- styleUrls: ['./login.component.css']
+ styleUrls: ['./login.component.scss']
})
export class LoginComponent implements OnInit {
<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>
@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();
@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 {
<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>
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 {
letter-spacing: 0.1rem;
font-size: 10px;
}
+
+.display-none {
+ display: none;
+}
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();
}
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);
})
);
}
*/
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> {
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();
}
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[]> {
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({
@Component({
selector: 'rd-root',
templateUrl: './rd.component.html',
- styleUrls: ['./rd.component.css']
+ styleUrls: ['./rd.component.scss']
})
export class RdComponent implements OnInit {
showMenu = false;
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';
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';
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: [
* 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({
// 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,
// 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,
// 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,
];
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,
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++);
}
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++);
}
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++);
}
});
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) {
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']);
});
}
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;
}
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);
}
* 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.
@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;
@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;
@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;
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';
@Component({
selector: 'rd-user',
templateUrl: './user.component.html',
- styleUrls: ['./user.component.css']
+ styleUrls: ['./user.component.scss']
})
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();
}
}
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();
}
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);
+ }
+
}
"directive-selector": [
true,
"attribute",
- "app",
+ "rd",
"camelCase"
],
"component-selector": [
true,
"element",
- "app",
+ "rd",
"kebab-case"
]
}