# documentation
.tox
docs/_build/*
+
+# JSON internal data
+dashboard/webapp-backend/app-stats.json
--- /dev/null
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2020 AT&T Intellectual Property
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+ */
+package org.oransc.ric.portal.dashboard;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.ListIterator;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.onap.portalsdk.core.onboarding.exception.PortalAPIException;
+import org.onap.portalsdk.core.restful.domain.EcompUser;
+import org.oransc.ric.portal.dashboard.model.StatsDetailsTransport;
+import org.oransc.ric.portal.dashboard.exception.StatsManagerException;
+import org.oransc.ric.portal.dashboard.model.IDashboardResponse;
+import org.oransc.ric.portal.dashboard.model.AppStats;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+import org.springframework.stereotype.Service;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * Provides simple xApp stats-management services.
+ *
+ * This first implementation serializes xApp stat details to a file.
+ *
+ * Migrate to a database someday?
+ */
+
+public class AppStatsManager {
+
+ private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ // This default value is only useful for development and testing.
+ public static final String STATS_FILE_PATH = "app-stats.json";
+
+ private final File statsFile;
+ private final List<AppStats> stats;
+ private int appMaxId = -1;
+
+ /**
+ * Development/test-only constructor that uses default file path.
+ *
+ * @param clear If true, start empty and remove any existing file.
+ *
+ * @throws IOException On file error
+ */
+ public AppStatsManager(boolean clear) throws IOException {
+ this(STATS_FILE_PATH);
+ if (clear) {
+ logger.debug("ctor: removing file {}", statsFile.getAbsolutePath());
+ File f = new File(AppStatsManager.STATS_FILE_PATH);
+ if (f.exists())
+ Files.delete(f.toPath());
+ stats.clear();
+ }
+ }
+
+ /**
+ * Constructor that accepts a file path
+ *
+ * @param statsFilePath File path
+ * @throws IOException If file cannot be read
+ */
+ public AppStatsManager(final String statsFilePath) throws IOException {
+ logger.debug("ctor: statsfile {}", statsFilePath);
+ if (statsFilePath == null)
+ throw new IllegalArgumentException("Missing or empty stats file property");
+ statsFile = new File(statsFilePath);
+ logger.debug("ctor: managing stats in file {}", statsFile.getAbsolutePath());
+ if (statsFile.exists()) {
+ final ObjectMapper mapper = new ObjectMapper();
+ stats = mapper.readValue(statsFile, new TypeReference<List<AppStats>>() {
+ });
+ for (AppStats st: stats) {
+ if (st.getStatsDetails().getAppId()>appMaxId)
+ appMaxId = st.getStatsDetails().getAppId();
+ }
+ } else {
+ stats = new ArrayList<>();
+ }
+ }
+
+ /**
+ * Gets the current app metric stats.
+ *
+ * @return List of App stat objects, possibly empty
+ */
+ public List<AppStats> getStats() {
+ return this.stats;
+ }
+
+ /**
+ * Gets the current app metric stats by instance key.
+ *
+ * @param instanceKey Desired instance key
+ * @return List of App stat objects by instance key, possibly empty
+ */
+ public List<AppStats> getStatsByInstance(String instanceKey) {
+ List<AppStats> statsByInstance = new ArrayList<AppStats>();
+ for (AppStats st : this.stats) {
+ if (st.getInstanceKey().equals(instanceKey)) {
+ logger.debug("getStatsByInstance: match on instance key {}", instanceKey);
+ statsByInstance.add(st);
+ }
+ }
+ return statsByInstance;
+ }
+
+ /**
+ * Gets the stats with the specified app Id and instance key
+ *
+ * @param appId Desired app Id
+ * @param instanceKey Desired instance key
+ * @return Stats object; null if Id is not known
+ */
+ public AppStats getStatsById(String instanceKey, int appId) {
+
+ for (AppStats st : this.stats) {
+ if (st.getInstanceKey().equals(instanceKey) && st.getStatsDetails().getAppId() == appId) {
+ logger.debug("getStatsById: match on app id {} with instance key {}", appId, instanceKey);
+ return st;
+ }
+ }
+ logger.debug("getStatsById: no match on app id with instance key {}{}", appId, instanceKey);
+ return null;
+
+ }
+
+ private void saveStats() throws IOException {
+ final ObjectMapper mapper = new ObjectMapper();
+ mapper.writeValue(statsFile, stats);
+ }
+
+ /*
+ * Allow at most one thread to create a stats at one time.
+ * Before creating new stat, checks for composite key (appname,url) uniqueness for an instance key
+ */
+ public synchronized AppStats createStats(String instanceKey, StatsDetailsTransport statsSetupRequest)
+ throws StatsManagerException, IOException {
+ logger.debug("createStats: appId {}, instanceKey {}", statsSetupRequest.getAppId(), instanceKey);
+
+ for (AppStats st : stats) {
+ if (st.getInstanceKey().equals(instanceKey)
+ && st.getStatsDetails().getAppName().equals(statsSetupRequest.getAppName())
+ && st.getStatsDetails().getMetricUrl().equals(statsSetupRequest.getMetricUrl())) {
+ String msg = "App exists with name " + statsSetupRequest.getAppName() + " and url "+statsSetupRequest.getMetricUrl()+ " on instance key " + instanceKey;
+ logger.warn(msg);
+ throw new StatsManagerException(msg);
+ }
+ }
+
+ AppStats newAppStat = null;
+ //Assigns appId to be 1 more than the largest value stored in memory
+ appMaxId = appMaxId+1;
+ newAppStat = new AppStats(instanceKey,
+ new StatsDetailsTransport(appMaxId, statsSetupRequest.getAppName(), statsSetupRequest.getMetricUrl()));
+ stats.add(newAppStat);
+ saveStats();
+ return newAppStat;
+ }
+
+ /*
+ * Allow at most one thread to modify a stats at one time. We still have
+ * last-edit-wins of course.
+ */
+ public synchronized void updateStats(String instanceKey, StatsDetailsTransport statsSetupRequest)
+ throws StatsManagerException, IOException {
+ logger.debug("updateStats: appId {}, instanceKey {}", statsSetupRequest.getAppId(), instanceKey);
+ boolean editStatsObjectFound = false;
+
+ for (AppStats st : stats) {
+ if (st.getInstanceKey().equals(instanceKey)
+ && st.getStatsDetails().getAppId() == statsSetupRequest.getAppId()) {
+ AppStats newAppStat = new AppStats(instanceKey, statsSetupRequest);
+ stats.remove(st);
+ stats.add(newAppStat);
+ editStatsObjectFound = true;
+ saveStats();
+ break;
+ }
+ }
+ if (!editStatsObjectFound) {
+ String msg = "Stats to be updated does not exist ";
+ logger.warn(msg);
+ throw new StatsManagerException(msg);
+ }
+ }
+
+ public synchronized AppStats deleteStats(String instanceKey, int appId) throws StatsManagerException, IOException {
+ logger.debug("deleteStats: appId {}, instanceKey {}", appId, instanceKey);
+ boolean deleteStatsObjectFound = false;
+ AppStats stat = null;
+ for (AppStats st : stats) {
+ if (st.getInstanceKey().equals(instanceKey) && st.getStatsDetails().getAppId() == appId) {
+ stat = st;
+ stats.remove(stat);
+ deleteStatsObjectFound = true;
+ try {
+ saveStats();
+ break;
+ } catch (Exception e) {
+ throw new StatsManagerException(e.toString());
+ }
+
+ }
+ }
+ if (!deleteStatsObjectFound) {
+ String msg = "deleteStats: no match on app id {} of instance key {}";
+ logger.warn(msg, appId, instanceKey);
+ throw new StatsManagerException(msg);
+ }
+ return stat;
+ }
+}
public static final String ENDPOINT_PREFIX = "/api";
// Spring path parameters
public static final String RIC_INSTANCE_KEY = "ric";
+ public static final String APP_ID = "appid";
// Factor out method names used in multiple controllers
public static final String VERSION_METHOD = "version";
- // Apps with metric panels
- public static final String APP_NAME_MC = "MC";
- public static final String APP_NAME_ML = "ML";
// The role names are defined by ONAP Portal.
// The prefix "ROLE_" is required by Spring.
// These are used in Java code annotations that require constants.
import java.lang.invoke.MethodHandles;
import org.oransc.ric.portal.dashboard.DashboardUserManager;
+import org.oransc.ric.portal.dashboard.AppStatsManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Profile;
/**
- * Creates an instance of the user manager.
+ * Creates an instance of the user manager and app stats manager.
*/
@Configuration
@Profile("!test")
// Populated by the autowired constructor
private final String userfile;
+ private final String statsfile;
@Autowired
- public AdminConfiguration(@Value("${userfile}") final String userfile) {
+ public AdminConfiguration(@Value("${userfile}") final String userfile,
+ @Value("${statsfile}") final String statsfile) {
logger.debug("ctor userfile '{}'", userfile);
+ logger.debug("ctor statsfile '{}'", statsfile);
this.userfile = userfile;
+ this.statsfile = statsfile;
+
}
@Bean
return new DashboardUserManager(userfile);
}
+ @Bean
+ // The bean (method) name must be globally unique
+ public AppStatsManager statsManager() throws IOException {
+ return new AppStatsManager(statsfile);
+ }
+
}
*/
package org.oransc.ric.portal.dashboard.controller;
+import java.io.IOException;
import java.lang.invoke.MethodHandles;
+import java.util.ArrayList;
import java.util.List;
import org.onap.portalsdk.core.restful.domain.EcompUser;
import org.oransc.ric.portal.dashboard.DashboardApplication;
import org.oransc.ric.portal.dashboard.DashboardConstants;
import org.oransc.ric.portal.dashboard.DashboardUserManager;
+import org.oransc.ric.portal.dashboard.AppStatsManager;
+import org.oransc.ric.portal.dashboard.exception.StatsManagerException;
+import org.oransc.ric.portal.dashboard.model.IDashboardResponse;
+import org.oransc.ric.portal.dashboard.model.StatsDetailsTransport;
import org.oransc.ric.portal.dashboard.model.RicRegion;
import org.oransc.ric.portal.dashboard.model.RicRegionList;
import org.oransc.ric.portal.dashboard.model.RicRegionTransport;
+import org.oransc.ric.portal.dashboard.model.AppStats;
import org.oransc.ric.portal.dashboard.model.SuccessTransport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.annotation.Secured;
+import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
public static final String USER_METHOD = "user";
public static final String VERSION_METHOD = DashboardConstants.VERSION_METHOD;
public static final String XAPPMETRICS_METHOD = "metrics";
-
- @Value("${metrics.url.mc}")
- private String mcAppMetricsUrl;
+ public static final String STATAPPMETRIC_METHOD = "appmetric";
@Value("${metrics.url.ml}")
private String mlAppMetricsUrl;
@Autowired
private DashboardUserManager dashboardUserManager;
+ @Autowired
+ private AppStatsManager appStatsManager;
+
@Autowired
private RicRegionList instanceConfig;
return instanceConfig.getSimpleInstances();
}
- @ApiOperation(value = "Gets the kibana metrics URL for the specified app.", response = SuccessTransport.class)
- @GetMapping(XAPPMETRICS_METHOD)
+ @ApiOperation(value = "Gets all xApp statistics reporting details.", response = StatsDetailsTransport.class, responseContainer = "List")
+ @GetMapping(DashboardConstants.RIC_INSTANCE_KEY + "/{" + DashboardConstants.RIC_INSTANCE_KEY + "}/"
+ + STATAPPMETRIC_METHOD)
+ @Secured({ DashboardConstants.ROLE_ADMIN, DashboardConstants.ROLE_STANDARD })
+ public List<AppStats> getStats(@PathVariable(DashboardConstants.RIC_INSTANCE_KEY) String instanceKey) {
+ logger.debug("getStats for instance {}", instanceKey);
+ return appStatsManager.getStatsByInstance(instanceKey);
+ }
+
+ @ApiOperation(value = "Gets a xApp's metrics status by Id.", response = StatsDetailsTransport.class, responseContainer = "List")
+ @GetMapping(DashboardConstants.RIC_INSTANCE_KEY + "/{" + DashboardConstants.RIC_INSTANCE_KEY + "}/"
+ + STATAPPMETRIC_METHOD + "/" + DashboardConstants.APP_ID + "/{" + DashboardConstants.APP_ID + "}")
@Secured({ DashboardConstants.ROLE_ADMIN, DashboardConstants.ROLE_STANDARD })
- public ResponseEntity<Object> getAppMetricsUrl(@RequestParam String app) {
- String metricsUrl = null;
- if (DashboardConstants.APP_NAME_MC.equals(app))
- metricsUrl = mcAppMetricsUrl;
- else if (DashboardConstants.APP_NAME_ML.equals(app))
- metricsUrl = mlAppMetricsUrl;
- logger.debug("getAppMetricsUrl: app {} metricsurl {}", app, metricsUrl);
- if (metricsUrl != null)
- return new ResponseEntity<>(new SuccessTransport(HttpStatus.OK.ordinal(), metricsUrl), HttpStatus.OK);
- return ResponseEntity.badRequest().body("Client provided app name is invalid as: " + app);
+ public AppStats getStatsById(@PathVariable(DashboardConstants.RIC_INSTANCE_KEY) String instanceKey,
+ @PathVariable(DashboardConstants.APP_ID) int appId) {
+ logger.debug("getStatsById for instance {} by app id {}", instanceKey, appId);
+ return appStatsManager.getStatsById(instanceKey, appId);
}
-}
+ @ApiOperation(value = "Creates xApp metrics status.")
+ @PostMapping(DashboardConstants.RIC_INSTANCE_KEY + "/{" + DashboardConstants.RIC_INSTANCE_KEY + "}/"
+ + STATAPPMETRIC_METHOD)
+ @Secured({ DashboardConstants.ROLE_ADMIN })
+ public IDashboardResponse createStats(@PathVariable(DashboardConstants.RIC_INSTANCE_KEY) String instanceKey,
+ @RequestBody StatsDetailsTransport statsSetupRequest) throws StatsManagerException, IOException {
+ logger.debug("createStats with instance {} request {}", instanceKey, statsSetupRequest);
+ return appStatsManager.createStats(instanceKey, statsSetupRequest);
+ }
+
+ @ApiOperation(value = "Updates xApp metrics status.")
+ @PutMapping(DashboardConstants.RIC_INSTANCE_KEY + "/{" + DashboardConstants.RIC_INSTANCE_KEY + "}/"
+ + STATAPPMETRIC_METHOD)
+ @Secured({ DashboardConstants.ROLE_ADMIN })
+ public ResponseEntity<String> updateStats(@PathVariable(DashboardConstants.RIC_INSTANCE_KEY) String instanceKey,
+ @RequestBody StatsDetailsTransport statsSetupRequest) throws StatsManagerException, IOException {
+ logger.debug("updateStats for instance {} request {}", instanceKey, statsSetupRequest);
+ appStatsManager.updateStats(instanceKey, statsSetupRequest);
+ return ResponseEntity.ok(null);
+ }
+
+ @ApiOperation(value = "Deletes xApp metric status.")
+ @DeleteMapping(DashboardConstants.RIC_INSTANCE_KEY + "/{" + DashboardConstants.RIC_INSTANCE_KEY + "}/"
+ + STATAPPMETRIC_METHOD + "/" + DashboardConstants.APP_ID + "/{" + DashboardConstants.APP_ID + "}")
+ @Secured({ DashboardConstants.ROLE_ADMIN })
+ public ResponseEntity<String> deleteStats(@PathVariable(DashboardConstants.RIC_INSTANCE_KEY) String instanceKey,
+ @PathVariable(DashboardConstants.APP_ID) int appId) throws StatsManagerException, IOException {
+ logger.debug("deleteStats instance {} request {}", instanceKey, appId);
+ appStatsManager.deleteStats(instanceKey, appId);
+ return ResponseEntity.ok(null);
+ }
+}
\ No newline at end of file
import java.lang.invoke.MethodHandles;
+import org.oransc.ric.portal.dashboard.exception.StatsManagerException;
import org.oransc.ric.portal.dashboard.exception.UnknownInstanceException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
ex.toString());
return ResponseEntity.badRequest().body(getShortExceptionMessage(ex));
}
+
+ /**
+ * Logs a warning if a StatsManagerException is thrown.
+ *
+ * @param ex
+ * The exception
+ * @param request
+ * The original request
+ * @return A response entity with status code 400 and an unstructured message.
+ */
+ @ExceptionHandler({ StatsManagerException.class })
+ public final ResponseEntity<String> handleStatsManagerException(Exception ex, WebRequest request) {
+ log.warn("handleStatsManagerException: request {}, exception {}", request.getDescription(false),
+ ex.toString());
+ return ResponseEntity.badRequest().body(getShortExceptionMessage(ex));
+ }
}
--- /dev/null
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 - 2020 AT&T Intellectual Property
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+ */
+package org.oransc.ric.portal.dashboard.exception;
+
+public class StatsManagerException extends Exception {
+
+ private static final long serialVersionUID = -3887065423512216019L;
+
+ public StatsManagerException() {
+ super();
+ }
+
+ public StatsManagerException(String s) {
+ super(s);
+ }
+}
--- /dev/null
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2020 AT&T Intellectual Property
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+ */
+package org.oransc.ric.portal.dashboard.model;
+
+public class AppStats implements IDashboardResponse {
+ private String instanceKey;
+ private StatsDetailsTransport statsDetails;
+
+ public AppStats() {
+ super();
+ }
+
+ public AppStats(String instanceKey, StatsDetailsTransport statsDetails) {
+ super();
+ this.instanceKey = instanceKey;
+ this.statsDetails = statsDetails;
+ }
+
+ public StatsDetailsTransport getStatsDetails() {
+ return statsDetails;
+ }
+
+ public void setStatsDetails(StatsDetailsTransport statsDetails) {
+ this.statsDetails = statsDetails;
+ }
+
+ public String getInstanceKey() {
+ return instanceKey;
+ }
+
+ public void setInstanceKey(String instanceKey) {
+ this.instanceKey = instanceKey;
+ }
+
+ @Override
+ public String toString() {
+ return this.getClass().getSimpleName() + "[instance=" + instanceKey + ", statsDetails=" + statsDetails + "]";
+ }
+
+}
--- /dev/null
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2020 AT&T Intellectual Property
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+ */
+package org.oransc.ric.portal.dashboard.model;
+
+public class StatsDetailsTransport implements IDashboardResponse {
+
+ private int appId;
+ private String appName;
+ private String metricUrl;
+
+ public StatsDetailsTransport(int appId, String appName, String metricUrl) {
+ this.appId = appId;
+ this.appName = appName;
+ this.metricUrl = metricUrl;
+ }
+
+ public int getAppId() {
+ return appId;
+ }
+
+ public void setAppId(int appId) {
+ this.appId = appId;
+ }
+
+ public StatsDetailsTransport() {
+ }
+
+ @Override
+ public String toString() {
+ return this.getClass().getName() + "[appId=" + getAppId() + ", appName=" + getAppName() + ", metricUrl="
+ + getMetricUrl() + "]";
+ }
+
+ public String getAppName() {
+ return appName;
+ }
+
+ public void setAppName(String appName) {
+ this.appName = appName;
+ }
+
+ public String getMetricUrl() {
+ return metricUrl;
+ }
+
+ public void setMetricUrl(String metricUrl) {
+ this.metricUrl = metricUrl;
+ }
+}
package org.oransc.ric.portal.dashboard.config;
import java.io.IOException;
+import java.io.PrintWriter;
import java.lang.invoke.MethodHandles;
+import java.util.Collection;
import java.util.HashSet;
+import java.util.Locale;
import java.util.Set;
import org.onap.portalsdk.core.onboarding.exception.PortalAPIException;
import org.onap.portalsdk.core.restful.domain.EcompRole;
import org.onap.portalsdk.core.restful.domain.EcompUser;
import org.oransc.ric.portal.dashboard.DashboardUserManager;
+import org.oransc.ric.portal.dashboard.AppStatsManager;
+import org.oransc.ric.portal.dashboard.exception.StatsManagerException;
+import org.oransc.ric.portal.dashboard.model.AppStats;
+import org.oransc.ric.portal.dashboard.model.StatsDetailsTransport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletResponse;
+
/**
- * Creates a user manager with mock data.
+ * Creates user manager and stats manager with mock data.
*/
@Configuration
@Profile("test")
return mgr;
}
+ @Bean
+ // The bean (method) name must be globally unique
+ public AppStatsManager statsManager() throws IOException, StatsManagerException {
+ logger.debug("statsManager: adding mock data");
+ AppStatsManager mgr = new AppStatsManager(true);
+ String instanceKey = RICInstanceMockConfiguration.INSTANCE_KEY_1;
+ StatsDetailsTransport statsDetails = new StatsDetailsTransport();
+ statsDetails.setAppId(0);
+ statsDetails.setAppName("MachLearn");
+ statsDetails.setMetricUrl("https://www.example.com");
+ mgr.createStats(instanceKey, statsDetails);
+ return mgr;
+ }
+
}
import java.lang.invoke.MethodHandles;
import java.net.URI;
-import java.util.HashMap;
import java.util.List;
-import java.util.Map;
-
import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestMethodOrder;
+import org.junit.jupiter.api.MethodOrderer;
import org.onap.portalsdk.core.restful.domain.EcompUser;
import org.oransc.ric.portal.dashboard.DashboardConstants;
+import org.oransc.ric.portal.dashboard.config.RICInstanceMockConfiguration;
+import org.oransc.ric.portal.dashboard.model.AppStats;
import org.oransc.ric.portal.dashboard.model.RicInstanceKeyName;
+import org.oransc.ric.portal.dashboard.model.StatsDetailsTransport;
import org.oransc.ric.portal.dashboard.model.SuccessTransport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.ParameterizedTypeReference;
+import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
+@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class AdminControllerTest extends AbstractControllerTest {
private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
Assertions.assertTrue(response.getStatusCode().is4xxClientError());
}
+ @Order(1)
@Test
- public void getxAppMetricsUrlTest() {
- Map<String, String> metricsQueryParms = new HashMap<String, String>();
- URI uri;
+ public void getAppStatsTest() {
+ URI uri = buildUri(null, AdminController.CONTROLLER_PATH, DashboardConstants.RIC_INSTANCE_KEY, "i1",
+ AdminController.STATAPPMETRIC_METHOD);
+ logger.info("Invoking uri {}", uri);
+ ResponseEntity<List<AppStats>> response = testRestTemplateAdminRole().exchange(uri, HttpMethod.GET, null,
+ new ParameterizedTypeReference<List<AppStats>>() {
+ });
+ Assertions.assertFalse(response.getBody().isEmpty());
+ Assertions.assertNotEquals(-1, response.getBody().get(0).getStatsDetails().getAppId());
+ }
- metricsQueryParms.clear();
- metricsQueryParms.put("app", DashboardConstants.APP_NAME_MC);
- uri = buildUri(metricsQueryParms, AdminController.CONTROLLER_PATH, AdminController.XAPPMETRICS_METHOD);
- logger.debug("Invoking {}", uri);
- ResponseEntity<SuccessTransport> successResponse = testRestTemplateStandardRole().exchange(uri, HttpMethod.GET,
- null, SuccessTransport.class);
- Assertions.assertFalse(successResponse.getBody().getData().toString().isEmpty());
- Assertions.assertTrue(successResponse.getStatusCode().is2xxSuccessful());
+ @Order(2)
+ @Test
+ public void createAppStatsTest() {
+ URI uri = buildUri(null, AdminController.CONTROLLER_PATH, DashboardConstants.RIC_INSTANCE_KEY,
+ RICInstanceMockConfiguration.INSTANCE_KEY_1, AdminController.STATAPPMETRIC_METHOD);
+ logger.info("Invoking uri {}", uri);
+ StatsDetailsTransport statsDetails = new StatsDetailsTransport();
+ statsDetails.setAppName("MachLearn-2");
+ statsDetails.setMetricUrl("https://www.example2.com");
+ AppStats st = testRestTemplateAdminRole().postForObject(uri, statsDetails, AppStats.class);
+ Assertions.assertFalse(st.getStatsDetails().getAppName().isEmpty());
+ statsDetails.setAppName("MachLearn-2-next");
+ statsDetails.setMetricUrl("https://www.example2-next.com");
+ AppStats stNext = testRestTemplateAdminRole().postForObject(uri, statsDetails, AppStats.class);
+ Assertions.assertTrue(st.getStatsDetails().getAppId() < stNext.getStatsDetails().getAppId());
+ }
- metricsQueryParms.clear();
- metricsQueryParms.put("app", DashboardConstants.APP_NAME_ML);
- logger.debug("Invoking {}", uri);
- successResponse = testRestTemplateStandardRole().exchange(uri, HttpMethod.GET, null, SuccessTransport.class);
- Assertions.assertFalse(successResponse.getBody().getData().toString().isEmpty());
- Assertions.assertTrue(successResponse.getStatusCode().is2xxSuccessful());
+ @Order(3)
+ @Test
+ public void updateAppStatsTest() {
+ URI uri = buildUri(null, AdminController.CONTROLLER_PATH, DashboardConstants.RIC_INSTANCE_KEY,
+ RICInstanceMockConfiguration.INSTANCE_KEY_1, AdminController.STATAPPMETRIC_METHOD);
+ logger.info("Invoking uri {}", uri);
+ ResponseEntity<List<AppStats>> response = testRestTemplateAdminRole().exchange(uri, HttpMethod.GET, null,
+ new ParameterizedTypeReference<List<AppStats>>() {
+ });
+ int statToUpdate = 0;
+ if (response.getBody() != null) {
+ statToUpdate = response.getBody().get(0).getStatsDetails().getAppId();
+ }
+ StatsDetailsTransport statsDetails = new StatsDetailsTransport();
+ statsDetails.setAppId(statToUpdate);
+ statsDetails.setAppName("MachLearn-1");
+ statsDetails.setMetricUrl("https://www.example1.com");
+ HttpEntity<StatsDetailsTransport> entity = new HttpEntity<>(statsDetails);
+ ResponseEntity<String> stringResponse = testRestTemplateAdminRole().exchange(uri, HttpMethod.PUT, entity,
+ String.class);
+ Assertions.assertTrue(stringResponse.getStatusCode().is2xxSuccessful());
}
+ @Order(4)
@Test
- public void getxAppMetricsUrlTestFail() {
- Map<String, String> metricsQueryParms = new HashMap<String, String>();
- // Providing a bogus value for application name in query parameter to test
- // failure
- metricsQueryParms.put("app", "ABCD");
- URI uri = buildUri(metricsQueryParms, AdminController.CONTROLLER_PATH, AdminController.XAPPMETRICS_METHOD);
- logger.debug("Invoking {}", uri);
- ResponseEntity<String> errorResponse = testRestTemplateStandardRole().exchange(uri, HttpMethod.GET, null,
+ public void deleteAppStatsTest() {
+ URI uri = buildUri(null, AdminController.CONTROLLER_PATH, DashboardConstants.RIC_INSTANCE_KEY,
+ RICInstanceMockConfiguration.INSTANCE_KEY_1, AdminController.STATAPPMETRIC_METHOD);
+ ResponseEntity<List<AppStats>> response = testRestTemplateAdminRole().exchange(uri, HttpMethod.GET, null,
+ new ParameterizedTypeReference<List<AppStats>>() {
+ });
+ int statToDelete = 0;
+ if (response.getBody() != null) {
+ statToDelete = response.getBody().get(0).getStatsDetails().getAppId();
+ }
+ uri = buildUri(null, AdminController.CONTROLLER_PATH, DashboardConstants.RIC_INSTANCE_KEY,
+ RICInstanceMockConfiguration.INSTANCE_KEY_1, AdminController.STATAPPMETRIC_METHOD,
+ DashboardConstants.APP_ID, String.valueOf(statToDelete));
+ logger.info("Invoking uri {}", uri);
+ ResponseEntity<String> stringResponse = testRestTemplateAdminRole().exchange(uri, HttpMethod.DELETE, null,
String.class);
- logger.debug("{}", errorResponse.getBody().toString());
- Assertions.assertTrue(errorResponse.getStatusCode().is4xxClientError());
+ Assertions.assertTrue(stringResponse.getStatusCode().is2xxSuccessful());
}
@Test
ranPort: string;
ranType: string;
}
+
+export interface StatsDialogFormData {
+ appName: string;
+ metricUrl: string;
+}
+
+export interface StatsDetails {
+ appId: number;
+ appName: string;
+ metricUrl: string;
+}
+
+export interface AppStats {
+ instanceKey: string
+ statsDetails: StatsDetails;
+}
+
import { SidenavListComponent } from './navigation/sidenav-list/sidenav-list.component';
import { StatCardComponent } from './ui/stat-card/stat-card.component';
import { StatsComponent } from './stats/stats.component';
+import { StatsDialogComponent } from './stats/stats-dialog.component';
import { UserComponent } from './user/user.component';
// RD services
SidenavListComponent,
StatCardComponent,
StatsComponent,
+ StatsDialogComponent,
UserComponent,
InstanceSelectorDialogComponent
],
EditDashboardUserDialogComponent,
ErrorDialogComponent,
InstanceSelectorDialogComponent,
- LoadingDialogComponent
+ LoadingDialogComponent,
+ StatsDialogComponent
],
providers: [
AppMgrService,
* limitations under the License.
* ========================LICENSE_END===================================
*/
-import { HttpClient, HttpErrorResponse, HttpHeaders, HttpParams } from '@angular/common/http';
+import { HttpClient, HttpErrorResponse, HttpHeaders, HttpParams, HttpResponse } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { DashboardService } from '../dashboard/dashboard.service';
+import { StatsDetails, AppStats } from '../../interfaces/e2-mgr.types';
@Injectable({
providedIn: 'root'
export class StatsService {
private component = 'admin';
+ private appmetricPath = 'appmetric';
+ private appId = 'appid';
baseJSONServerUrl = 'http://localhost:3000';
dataMetrics = [{}];
});
}
+ getAppMetrics(instanceKey: string): Observable<Array<StatsDetails>> {
+ const path = this.dashboardSvc.buildPath(this.component, instanceKey, this.appmetricPath);
+ return this.httpClient.get<Array<StatsDetails>>(path);
+ }
+
+ getAppMetricsById(instanceKey: string, appId: number): Observable<AppStats> {
+ const path = this.dashboardSvc.buildPath(this.component, instanceKey, this.appmetricPath, this.appId, appId);
+ return this.httpClient.get<AppStats>(path);
+ }
+
+ setupAppMetrics(instanceKey: string, req: StatsDetails): Observable<HttpResponse<Object>> {
+ const path = this.dashboardSvc.buildPath(this.component, instanceKey, this.appmetricPath);
+ return this.httpClient.post(path, req, { observe: 'response' });
+ }
+
+ editAppMetrics(instanceKey: string, req: StatsDetails): Observable<HttpResponse<Object>> {
+ const path = this.dashboardSvc.buildPath(this.component, instanceKey, this.appmetricPath);
+ return this.httpClient.put(path, req, { observe: 'response' });
+ }
+
+ deleteAppMetrics(instanceKey: string, appId: number): Observable<HttpResponse<Object>> {
+ const path = this.dashboardSvc.buildPath(this.component, instanceKey, this.appmetricPath, this.appId, appId);
+ return this.httpClient.delete(path, { observe: 'response' });
+ }
+
saveConfig(key: string, value: string) {
if (key === 'jsonURL') {
this.baseJSONServerUrl = value;
--- /dev/null
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2020 AT&T Intellectual Property
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+ */
+
+import { CollectionViewer, DataSource } from '@angular/cdk/collections';
+import { HttpErrorResponse } from '@angular/common/http';
+import { Observable } from 'rxjs/Observable';
+import { BehaviorSubject } from 'rxjs/BehaviorSubject';
+import { of } from 'rxjs/observable/of';
+import { catchError, finalize } from 'rxjs/operators';
+import { E2RanDetails, StatsDetails } from '../interfaces/e2-mgr.types';
+import { E2ManagerService } from '../services/e2-mgr/e2-mgr.service';
+import { NotificationService } from '../services/ui/notification.service';
+import { StatsService } from '../services/stats/stats.service';
+
+export class StatsDataSource extends DataSource<StatsDetails> {
+
+ private statsSubject = new BehaviorSubject<StatsDetails[]>([]);
+
+ private loadingSubject = new BehaviorSubject<boolean>(false);
+
+ public loading$ = this.loadingSubject.asObservable();
+
+ public rowCount = 1; // hide footer during intial load
+
+ constructor(private statsService: StatsService,
+ private notificationService: NotificationService) {
+ super();
+ }
+
+ loadTable(instanceKey: string) {
+ this.loadingSubject.next(true);
+ this.statsService.getAppMetrics(instanceKey)
+ .pipe(
+ catchError( (her: HttpErrorResponse) => {
+ console.log('StatsDataSource failed: ' + her.message);
+ this.notificationService.error('Failed to get Stats details: ' + her.message);
+ return of([]);
+ }),
+ finalize( () => this.loadingSubject.next(false) )
+ )
+ .subscribe( (stat: StatsDetails[] ) => {
+ this.rowCount = stat.length;
+ this.statsSubject.next(stat);
+ });
+ }
+
+ connect(collectionViewer: CollectionViewer): Observable<StatsDetails[]> {
+ return this.statsSubject.asObservable();
+ }
+
+ disconnect(collectionViewer: CollectionViewer): void {
+ this.statsSubject.complete();
+ this.loadingSubject.complete();
+ }
+
+}
--- /dev/null
+<!--
+ ========================LICENSE_START=================================
+ O-RAN-SC
+ %%
+ Copyright (C) 2019 AT&T Intellectual Property
+ %%
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ ========================LICENSE_END===================================
+ -->
+
+<div mat-dialog-title>
+ App Metrics Visualization
+</div>
+<form [formGroup]="statsDialogForm" novalidate autocomplete="off" (ngSubmit)="setupStats(statsDialogForm.value)">
+ <div mat-dialog-content>
+ <mat-form-field class="input-display-block">
+ <input matInput type="text" placeholder="App Name" formControlName="appName">
+ <mat-hint align="end">Example: MC</mat-hint>
+ <mat-error *ngIf="validateControl('appName') && hasError('appName', 'required')">App Name is required</mat-error>
+ </mat-form-field>
+ <mat-form-field class="input-display-block">
+ <input matInput type="text" placeholder="Metrics Url" formControlName="metricUrl">
+ <mat-error *ngIf="validateControl('metricUrl') && hasError('metricUrl', 'required')">Metrics Url is required</mat-error>
+ </mat-form-field>
+ </div>
+ <div mat-dialog-actions class="modal-footer justify-content-center">
+ <button class="mat-raised-button" [mat-dialog-close]="false">Cancel</button>
+ <button class="mat-raised-button mat-primary" [disabled]="!statsDialogForm.valid || processing">OK</button>
+ </div>
+</form>
--- /dev/null
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 AT&T Intellectual Property
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+ */
+
+ /* used to place form fields on separate lines/rows in dialog */
+.input-display-block {
+ display: block;
+}
+
+
--- /dev/null
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2020 AT&T Intellectual Property
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+ */
+import { HttpErrorResponse, HttpResponse } from '@angular/common/http';
+import { Component, Inject, OnInit } from '@angular/core';
+import { FormControl, FormGroup } from '@angular/forms';
+import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
+import { Observable } from 'rxjs';
+import { finalize } from 'rxjs/operators';
+import { StatsDialogFormData, StatsDetails } from '../interfaces/e2-mgr.types';
+import { ErrorDialogService } from '../services/ui/error-dialog.service';
+import { LoadingDialogService } from '../services/ui/loading-dialog.service';
+import { NotificationService } from '../services/ui/notification.service';
+import { StatsService } from '../services/stats/stats.service';
+
+@Component({
+ selector: 'stats-dialog',
+ templateUrl: './stats-dialog.component.html',
+ styleUrls: ['./stats-dialog.component.scss']
+})
+
+export class StatsDialogComponent implements OnInit {
+
+ public statsDialogForm: FormGroup;
+ public processing = false;
+
+ constructor(
+ private dialogRef: MatDialogRef<StatsDialogComponent>,
+ private service: StatsService,
+ private errorService: ErrorDialogService,
+ private loadingDialogService: LoadingDialogService,
+ private notifService: NotificationService,
+ @Inject(MAT_DIALOG_DATA) private data) {
+ // opens with empty fields; accepts no data to display
+ }
+
+ ngOnInit() {
+ this.statsDialogForm = new FormGroup({
+ appName: this.data? new FormControl(this.data.appName) : new FormControl(''),
+ metricUrl: this.data? new FormControl(this.data.metricUrl) : new FormControl('')
+ });
+ }
+
+ setupStats = (statsFormValue: StatsDialogFormData) => {
+ if (!this.statsDialogForm.valid) {
+ console.log('Invalid dialog form data, appname and/or metrics url failed to pass its validation checks');
+ return;
+ }
+ this.processing = true;
+ const setupRequest: StatsDetails = {
+ appId: this.data.appId,
+ appName: statsFormValue.appName.trim(),
+ metricUrl: statsFormValue.metricUrl.trim()
+ };
+ this.loadingDialogService.startLoading('Setting up app metrics list');
+ let observable: Observable<HttpResponse<Object>>;
+ if(!(this.data.isEdit==='true'))
+ observable = this.service.setupAppMetrics(this.data.instanceKey, setupRequest);
+ else
+ observable = this.service.editAppMetrics(this.data.instanceKey, setupRequest);
+ observable
+ .pipe(
+ finalize(() => this.loadingDialogService.stopLoading())
+ )
+ .subscribe(
+ (response: any) => {
+ this.processing = false;
+ this.notifService.success('App metrics setup!');
+ this.dialogRef.close(true);
+ },
+ ((her: HttpErrorResponse) => {
+ this.processing = false;
+ // the error field carries the server's response
+ let msg = her.message;
+ if (her.error && her.error.message) {
+ msg = her.error.message;
+ }
+ this.errorService.displayError('App Metrics setup request failed: ' + msg);
+ // keep the dialog open
+ })
+ );
+ }
+
+ hasError(controlName: string, errorName: string) {
+ if (this.statsDialogForm.controls[controlName].hasError(errorName)) {
+ return true;
+ }
+ return false;
+ }
+
+ validateControl(controlName: string) {
+ if (this.statsDialogForm.controls[controlName].invalid && this.statsDialogForm.controls[controlName].touched) {
+ return true;
+ }
+ return false;
+ }
+
+}
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.
limitations under the License.
========================LICENSE_END===================================
-->
-<div class="stats__section">
-
- <h3 class="rd-global-page-title">Platform stats</h3>
-
- <mat-tab-group>
- <mat-tab label="MC xApp">
- <mat-grid-list cols="3" rowHeight="3:2">
- <mat-grid-tile>
- <mat-card>
- <iframe [src]="metricsUrlMc"></iframe>
- </mat-card>
- </mat-grid-tile>
- </mat-grid-list>
- </mat-tab>
- <mat-tab label="ML xApp">
- <mat-grid-list cols="3" rowHeight="3:2">
- <mat-grid-tile>
- <mat-card>
- <iframe [src]="metricsUrlMl"></iframe>
- </mat-card>
- </mat-grid-tile>
- </mat-grid-list>
- </mat-tab>
- </mat-tab-group>
-
-</div>
+ <div class="stats__section">
+ <h3 class="rd-global-page-title">Platform stats</h3>
+ <button mat-raised-button (click)="setupAppMetrics()">Add App Metrics..</button>
+ <table mat-table class="stats-table mat-elevation-z8" [dataSource]="dataSource">
+ <ng-template #noValue></ng-template>
+
+
+ <ng-container matColumnDef="appName">
+ <mat-header-cell *matHeaderCellDef>App Name</mat-header-cell>
+ <mat-cell *matCellDef="let stats">
+ <div *ngIf="stats.statsDetails.appName; else noValue">{{stats.statsDetails.appName}}</div>
+ </mat-cell>
+ </ng-container>
+
+ <ng-container matColumnDef="metricUrl">
+ <mat-header-cell *matHeaderCellDef>Metrics Url</mat-header-cell>
+ <mat-cell *matCellDef="let stats">
+ <div *ngIf="stats.statsDetails.metricUrl; else noValue">{{stats.statsDetails.metricUrl}}</div>
+ </mat-cell>
+ </ng-container>
+
+ <ng-container matColumnDef="editmetricUrl">
+ <mat-header-cell *matHeaderCellDef>Action</mat-header-cell>
+ <mat-cell *matCellDef="let stats">
+ <div *ngIf="stats; else noValue"><button mat-icon-button (click)="editAppMetrics(stats)">
+ <mat-icon matTooltip="Edit name, url">edit</mat-icon>
+ </button></div>
+ <div *ngIf="stats; else noValue"><button mat-icon-button (click)="viewAppMetrics(stats)">
+ <mat-icon matTooltip="View metrics">pageview</mat-icon>
+ </button></div>
+ <div *ngIf="stats; else noValue"><button mat-icon-button (click)="deleteAppMetrics(stats)">
+ <mat-icon matTooltip="Delete metrics">delete</mat-icon>
+ </button></div>
+ </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 diameter=50></mat-spinner>
+ </div>
+
+ <mat-form-field [hidden]="true">
+ <input matInput type="number" [formControl]="selected">
+ </mat-form-field>
+
+ <mat-tab-group [selectedIndex]="selected.value" (selectedIndexChange)="selected.setValue($event)" (selectedTabChange)="onTabChanged($event)">
+ <mat-tab *ngFor="let tab of tabs; let index = index" [label]="tab.statsDetails.appName">
+ <iframe [src]="metricsUrl"></iframe>
+ </mat-tab>
+ </mat-tab-group>
+ </div>
.stats__section {
}
+.stats-table {
+ width: 100%;
+ min-height: 150px;
+ margin-top: 10px;
+ background-color:transparent;
+}
+
+.spinner-container mat-spinner {
+ margin: 130px auto 0 auto;
+}
+
.bar-chart-card {
height: 100%;
width: 100%;
width: 500px;
height: 420px;
border: 1px solid black;
-}
\ No newline at end of file
+}
+
+.display-none {
+ display: none;
+}
+
+.mat-column-editmetricUrl {
+ white-space: unset ;
+ max-width: 18% ;
+}
+
+.mat-column-appName {
+ white-space: unset ;
+ max-width: 28% ;
+}
+
* 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.
* limitations under the License.
* ========================LICENSE_END===================================
*/
-import { Component, OnInit, ViewChildren, QueryList } from '@angular/core';
-import { BaseChartDirective } from 'ng2-charts/ng2-charts';
+import { Component, OnInit } from '@angular/core';
import { StatsService } from '../services/stats/stats.service';
-import { HttpClient } from '@angular/common/http';
-import { DashboardSuccessTransport } from '../interfaces/dashboard.types';
+import { ConfirmDialogService } from '../services/ui/confirm-dialog.service';
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
+import { MatDialog, MatTabChangeEvent } from '@angular/material';
+import { NotificationService } from '../services/ui/notification.service';
+import { UiService } from '../services/ui/ui.service';
+import { InstanceSelectorService } from '../services/instance-selector/instance-selector.service';
+import { StatsDataSource } from './stats-datasource';
+import { Subscription } from 'rxjs';
+import { StatsDialogComponent } from './stats-dialog.component';
+import { AppStats } from '../interfaces/e2-mgr.types';
+import {FormControl} from '@angular/forms';
+import { RicInstance} from '../interfaces/dashboard.types';
@Component({
selector: 'rd-stats',
})
export class StatsComponent implements OnInit {
- @ViewChildren(BaseChartDirective) charts: QueryList<BaseChartDirective>;
checked = false;
- metricsUrlMc: SafeResourceUrl;
- metricsUrlMl: SafeResourceUrl;
+ darkMode: boolean;
+ panelClass: string;
+ displayedColumns: string[] = ['appName', 'metricUrl', 'editmetricUrl'];
+ dataSource: StatsDataSource;
+ private instanceChange: Subscription;
+ private instanceKey: string;
+ metricsUrl: SafeResourceUrl;
+ tabs = [];
+ showTabs = false;
+ selected = new FormControl(0);
- constructor(private service: StatsService,
- private httpClient: HttpClient,
- private sanitize: DomSanitizer) {
+ constructor(private statsservice: StatsService,
+ private sanitize: DomSanitizer,
+ private confirmDialogService: ConfirmDialogService,
+ private notificationService: NotificationService,
+ public instanceSelectorService: InstanceSelectorService,
+ public dialog: MatDialog,
+ public ui: UiService) {
}
ngOnInit() {
- this.service.getAppMetricsUrl('MC').subscribe((res: DashboardSuccessTransport) => {
- this.metricsUrlMc = this.sanitize.bypassSecurityTrustResourceUrl(res.data);
+ this.dataSource = new StatsDataSource(this.statsservice, this.notificationService);
+
+ this.ui.darkModeState.subscribe((isDark) => {
+ this.darkMode = isDark;
+ });
+
+ this.instanceChange = this.instanceSelectorService.getSelectedInstance().subscribe((instance: RicInstance) => {
+ if (instance.key) {
+ this.instanceKey = instance.key;
+ this.dataSource.loadTable(instance.key);
+ }
});
- this.service.getAppMetricsUrl('ML').subscribe((res: DashboardSuccessTransport) => {
- this.metricsUrlMl = this.sanitize.bypassSecurityTrustResourceUrl(res.data);
+
+ }
+
+ ngOnDestroy() {
+ this.instanceChange.unsubscribe();
+ }
+
+ setupAppMetrics() {
+ if (this.darkMode) {
+ this.panelClass = 'dark-theme';
+ } else {
+ this.panelClass = '';
+ }
+ const dialogRef = this.dialog.open(StatsDialogComponent, {
+ panelClass: this.panelClass,
+ width: '450px',
+ data: {
+ instanceKey: this.instanceKey
+ }
});
+ dialogRef.afterClosed()
+ .subscribe((result: boolean) => {
+ if (result) {
+ this.dataSource.loadTable(this.instanceKey);
+ }
+ });
}
+ editAppMetrics(stats?) {
+ const dialogRef = this.dialog.open(StatsDialogComponent, {
+ hasBackdrop: false,
+ data: {
+ instanceKey: this.instanceKey,
+ appName: stats.statsDetails.appName ? stats.statsDetails.appName : '',
+ metricUrl: stats.statsDetails.metricUrl ? stats.statsDetails.metricUrl : '',
+ appId: stats.statsDetails.appId ? stats.statsDetails.appId : 0,
+ isEdit: 'true'
+ }
+ });
+ dialogRef.afterClosed()
+ .subscribe((result: boolean) => {
+ if (result) {
+ this.dataSource.loadTable(this.instanceKey);
+ }
+ });
+ }
+
+ viewAppMetrics(stats?) {
+ this.statsservice.getAppMetricsById(this.instanceKey, stats.statsDetails.appId) .subscribe((res: AppStats) => {
+ this.metricsUrl = this.sanitize.bypassSecurityTrustResourceUrl(res.statsDetails.metricUrl);
+ let tabNotThere:boolean = true;
+ if (this.tabs.length <= 0) {
+ this.tabs.push(res);
+ this.selected.setValue(this.tabs.length - 1);
+ }
+ else {
+ for(let i=0; i<this.tabs.length; i++){
+ if (this.tabs[i].statsDetails.appId == res.statsDetails.appId) {
+ this.tabs[i].statsDetails.appName = res.statsDetails.appName;
+ this.tabs[i].statsDetails.metricUrl = res.statsDetails.metricUrl;
+ this.selected.setValue(i);
+ tabNotThere = false;
+ break;
+ }
+ }
+ if (tabNotThere) {
+ this.tabs.push(res);
+ this.selected.setValue(this.tabs.length - 1);
+ }
+ }
+ });
+ }
+
+ onTabChanged(event: MatTabChangeEvent) {
+ if (event.index>=0)
+ this.viewAppMetrics(this.tabs[event.index]);
+ }
+
+ deleteAppMetrics(stats?) {
+ this.confirmDialogService.openConfirmDialog('Are you sure you want to delete this entry?')
+ .afterClosed().subscribe((res: boolean) => {
+ if (res) {
+
+ this.statsservice.deleteAppMetrics(this.instanceKey, stats.statsDetails.appId).subscribe(() => {
+ for(let i=0; i<this.tabs.length; i++){
+ if (this.tabs[i].instanceKey === this.instanceKey && this.tabs[i].statsDetails.appId == stats.statsDetails.appId) {
+ this.tabs.splice(i, 1);
+ if (this.tabs.length>0) {
+ if (this.tabs[i] == null)
+ i=i-1;
+ this.viewAppMetrics(this.tabs[i]);
+ }
+ break;
+ }
+ }
+ this.dataSource.loadTable(this.instanceKey);
+ });
+ }
+ });
+ }
}
* Drop ENDC and X2 setup requests to E2 Manager
* Upgrade to Spring-Boot 2.2.4.RELEASE
* Set the first instance as the default one
+* Add methods to create, update and delete xApp stat metric URLs
Version 2.0.0, 5 Feb 2020
--------------------------