Committed by
Gerrit Code Review
[ONOS-3851] Implement default Web GUI page for CPMan
- Reduce the datapoints to 20, resolve cold start problem - Code refactoring for CpmanViewMessageHandler - Code refactoring for cpman.js - Show "No Data" message when client does not receive any data - Clean up cpman.css - Specify default colors for charting - Resolve ArrayIndexOutofBoundsException when the number returned dataset is less the number what we expected Change-Id: I67ab3160ab66f92eaffeffc2d61c7d0e17be0512
Showing
4 changed files
with
197 additions
and
71 deletions
| ... | @@ -27,7 +27,6 @@ import org.onosproject.cpman.ControlLoadSnapshot; | ... | @@ -27,7 +27,6 @@ import org.onosproject.cpman.ControlLoadSnapshot; |
| 27 | import org.onosproject.cpman.ControlMetricType; | 27 | import org.onosproject.cpman.ControlMetricType; |
| 28 | import org.onosproject.cpman.ControlPlaneMonitorService; | 28 | import org.onosproject.cpman.ControlPlaneMonitorService; |
| 29 | import org.onosproject.net.DeviceId; | 29 | import org.onosproject.net.DeviceId; |
| 30 | -import org.onosproject.net.device.DeviceService; | ||
| 31 | import org.onosproject.ui.RequestHandler; | 30 | import org.onosproject.ui.RequestHandler; |
| 32 | import org.onosproject.ui.UiMessageHandler; | 31 | import org.onosproject.ui.UiMessageHandler; |
| 33 | import org.onosproject.ui.chart.ChartModel; | 32 | import org.onosproject.ui.chart.ChartModel; |
| ... | @@ -38,14 +37,16 @@ import org.slf4j.LoggerFactory; | ... | @@ -38,14 +37,16 @@ import org.slf4j.LoggerFactory; |
| 38 | import java.util.Collection; | 37 | import java.util.Collection; |
| 39 | import java.util.Map; | 38 | import java.util.Map; |
| 40 | import java.util.Optional; | 39 | import java.util.Optional; |
| 40 | +import java.util.Set; | ||
| 41 | import java.util.concurrent.ExecutionException; | 41 | import java.util.concurrent.ExecutionException; |
| 42 | import java.util.concurrent.TimeUnit; | 42 | import java.util.concurrent.TimeUnit; |
| 43 | +import java.util.stream.LongStream; | ||
| 43 | 44 | ||
| 44 | import static org.onosproject.cpman.ControlResource.CONTROL_MESSAGE_METRICS; | 45 | import static org.onosproject.cpman.ControlResource.CONTROL_MESSAGE_METRICS; |
| 45 | import static org.onosproject.cpman.ControlResource.Type.CONTROL_MESSAGE; | 46 | import static org.onosproject.cpman.ControlResource.Type.CONTROL_MESSAGE; |
| 46 | 47 | ||
| 47 | /** | 48 | /** |
| 48 | - * CpmanViewMessageHandler class implementation. | 49 | + * Message handler for control plane monitoring view related messages. |
| 49 | */ | 50 | */ |
| 50 | public class CpmanViewMessageHandler extends UiMessageHandler { | 51 | public class CpmanViewMessageHandler extends UiMessageHandler { |
| 51 | 52 | ||
| ... | @@ -55,12 +56,14 @@ public class CpmanViewMessageHandler extends UiMessageHandler { | ... | @@ -55,12 +56,14 @@ public class CpmanViewMessageHandler extends UiMessageHandler { |
| 55 | private static final String CPMAN_DATA_RESP = "cpmanDataResponse"; | 56 | private static final String CPMAN_DATA_RESP = "cpmanDataResponse"; |
| 56 | private static final String CPMANS = "cpmans"; | 57 | private static final String CPMANS = "cpmans"; |
| 57 | 58 | ||
| 58 | - // TODO: we assume that server side always returns 60 data points | 59 | + // TODO: we assume that server side always returns 20 data points |
| 59 | // to feed 1 hour time slots, later this should make to be configurable | 60 | // to feed 1 hour time slots, later this should make to be configurable |
| 60 | - private static final int NUM_OF_DATA_POINTS = 60; | 61 | + private static final int NUM_OF_DATA_POINTS = 20; |
| 61 | 62 | ||
| 62 | private static final int MILLI_CONV_UNIT = 1000; | 63 | private static final int MILLI_CONV_UNIT = 1000; |
| 63 | 64 | ||
| 65 | + private long timestamp = 0L; | ||
| 66 | + | ||
| 64 | @Override | 67 | @Override |
| 65 | protected Collection<RequestHandler> createRequestHandlers() { | 68 | protected Collection<RequestHandler> createRequestHandlers() { |
| 66 | return ImmutableSet.of( | 69 | return ImmutableSet.of( |
| ... | @@ -83,50 +86,104 @@ public class CpmanViewMessageHandler extends UiMessageHandler { | ... | @@ -83,50 +86,104 @@ public class CpmanViewMessageHandler extends UiMessageHandler { |
| 83 | @Override | 86 | @Override |
| 84 | protected void populateChart(ChartModel cm, ObjectNode payload) { | 87 | protected void populateChart(ChartModel cm, ObjectNode payload) { |
| 85 | String uri = string(payload, "devId"); | 88 | String uri = string(payload, "devId"); |
| 89 | + ControlPlaneMonitorService cpms = get(ControlPlaneMonitorService.class); | ||
| 90 | + ClusterService cs = get(ClusterService.class); | ||
| 86 | if (!Strings.isNullOrEmpty(uri)) { | 91 | if (!Strings.isNullOrEmpty(uri)) { |
| 87 | - Map<ControlMetricType, Long[]> data = Maps.newHashMap(); | ||
| 88 | DeviceId deviceId = DeviceId.deviceId(uri); | 92 | DeviceId deviceId = DeviceId.deviceId(uri); |
| 89 | - ClusterService cs = get(ClusterService.class); | ||
| 90 | - ControlPlaneMonitorService cpms = get(ControlPlaneMonitorService.class); | ||
| 91 | - | ||
| 92 | if (cpms.availableResources(CONTROL_MESSAGE).contains(deviceId.toString())) { | 93 | if (cpms.availableResources(CONTROL_MESSAGE).contains(deviceId.toString())) { |
| 93 | - LocalDateTime ldt = null; | 94 | + Map<ControlMetricType, Long[]> data = generateMatrix(cpms, cs, deviceId); |
| 94 | - | 95 | + LocalDateTime ldt = new LocalDateTime(timestamp * MILLI_CONV_UNIT); |
| 95 | - try { | 96 | + |
| 96 | - for (ControlMetricType cmt : CONTROL_MESSAGE_METRICS) { | 97 | + populateMetrics(cm, data, ldt, NUM_OF_DATA_POINTS); |
| 97 | - ControlLoadSnapshot cls = cpms.getLoad(cs.getLocalNode().id(), | 98 | + } |
| 98 | - cmt, NUM_OF_DATA_POINTS, TimeUnit.MINUTES, | 99 | + } else { |
| 99 | - Optional.of(deviceId)).get(); | 100 | + Set<String> deviceIds = cpms.availableResources(CONTROL_MESSAGE); |
| 100 | - data.put(cmt, ArrayUtils.toObject(cls.recent())); | 101 | + for (String deviceId : deviceIds) { |
| 101 | - if (ldt == null) { | 102 | + Map<ControlMetricType, Long> data = |
| 102 | - ldt = new LocalDateTime(cls.time() * MILLI_CONV_UNIT); | 103 | + populateDeviceMetrics(cpms, cs, DeviceId.deviceId(deviceId)); |
| 103 | - } | 104 | + Map<String, Long> local = Maps.newHashMap(); |
| 104 | - } | 105 | + for (ControlMetricType cmt : CONTROL_MESSAGE_METRICS) { |
| 105 | - | 106 | + local.put(StringUtils.lowerCase(cmt.name()), data.get(cmt)); |
| 106 | - for (int i = 0; i < NUM_OF_DATA_POINTS; i++) { | ||
| 107 | - Map<String, Long> local = Maps.newHashMap(); | ||
| 108 | - for (ControlMetricType cmt : CONTROL_MESSAGE_METRICS) { | ||
| 109 | - local.put(StringUtils.lowerCase(cmt.name()), data.get(cmt)[i]); | ||
| 110 | - } | ||
| 111 | - | ||
| 112 | - local.put(LABEL, ldt.minusMinutes(NUM_OF_DATA_POINTS - i).toDateTime().getMillis()); | ||
| 113 | - | ||
| 114 | - populateMetric(cm.addDataPoint(ldt.minusMinutes(NUM_OF_DATA_POINTS - i) | ||
| 115 | - .toDateTime().getMillis()), local); | ||
| 116 | - } | ||
| 117 | - | ||
| 118 | - } catch (InterruptedException | ExecutionException e) { | ||
| 119 | - log.warn(e.getMessage()); | ||
| 120 | } | 107 | } |
| 108 | + // TODO: need to find a way to present device id using long type | ||
| 109 | + String shortId = StringUtils.substring(deviceId, | ||
| 110 | + deviceId.length() - 2, deviceId.length()); | ||
| 111 | + local.put(LABEL, Long.valueOf(shortId)); | ||
| 112 | + populateMetric(cm.addDataPoint(Long.valueOf(shortId)), local); | ||
| 121 | } | 113 | } |
| 114 | + } | ||
| 115 | + } | ||
| 116 | + | ||
| 117 | + private Map<ControlMetricType, Long> populateDeviceMetrics(ControlPlaneMonitorService cpms, | ||
| 118 | + ClusterService cs, DeviceId deviceId) { | ||
| 119 | + Map<ControlMetricType, Long> data = Maps.newHashMap(); | ||
| 120 | + for (ControlMetricType cmt : CONTROL_MESSAGE_METRICS) { | ||
| 121 | + ControlLoadSnapshot cls; | ||
| 122 | + try { | ||
| 123 | + cls = cpms.getLoad(cs.getLocalNode().id(), | ||
| 124 | + cmt, NUM_OF_DATA_POINTS, TimeUnit.MINUTES, | ||
| 125 | + Optional.of(deviceId)).get(); | ||
| 126 | + data.put(cmt, Math.round(LongStream.of(cls.recent()).average().getAsDouble())); | ||
| 127 | + timestamp = cls.time(); | ||
| 128 | + } catch (InterruptedException | ExecutionException e) { | ||
| 129 | + log.warn(e.getMessage()); | ||
| 130 | + } | ||
| 131 | + } | ||
| 132 | + return data; | ||
| 133 | + } | ||
| 134 | + | ||
| 135 | + private Map<ControlMetricType, Long[]> generateMatrix(ControlPlaneMonitorService cpms, | ||
| 136 | + ClusterService cs, DeviceId deviceId) { | ||
| 137 | + Map<ControlMetricType, Long[]> data = Maps.newHashMap(); | ||
| 138 | + for (ControlMetricType cmt : CONTROL_MESSAGE_METRICS) { | ||
| 139 | + ControlLoadSnapshot cls; | ||
| 140 | + try { | ||
| 141 | + cls = cpms.getLoad(cs.getLocalNode().id(), | ||
| 142 | + cmt, NUM_OF_DATA_POINTS, TimeUnit.MINUTES, | ||
| 143 | + Optional.of(deviceId)).get(); | ||
| 144 | + | ||
| 145 | + // TODO: in some cases, the number of returned dataset is | ||
| 146 | + // less than what we expected (expected -1) | ||
| 147 | + // As a workaround, we simply fill the slot with 0 values, | ||
| 148 | + // such a bug should be fixed with updated RRD4J lib... | ||
| 149 | + data.put(cmt, ArrayUtils.toObject(fillData(cls.recent(), NUM_OF_DATA_POINTS))); | ||
| 150 | + timestamp = cls.time(); | ||
| 151 | + } catch (InterruptedException | ExecutionException e) { | ||
| 152 | + log.warn(e.getMessage()); | ||
| 153 | + } | ||
| 154 | + } | ||
| 155 | + return data; | ||
| 156 | + } | ||
| 157 | + | ||
| 158 | + private long[] fillData(long[] origin, int expected) { | ||
| 159 | + if (origin.length == expected) { | ||
| 160 | + return origin; | ||
| 122 | } else { | 161 | } else { |
| 123 | - DeviceService ds = get(DeviceService.class); | 162 | + long[] filled = new long[expected]; |
| 124 | - ds.getAvailableDevices(); | 163 | + for (int i = 0; i < expected; i++) { |
| 164 | + if (i == 0) { | ||
| 165 | + filled[i] = 0; | ||
| 166 | + } else { | ||
| 167 | + filled[i] = origin[i - 1]; | ||
| 168 | + } | ||
| 169 | + } | ||
| 170 | + return filled; | ||
| 125 | } | 171 | } |
| 126 | } | 172 | } |
| 127 | 173 | ||
| 128 | - private void populateAllDevs(ChartModel.DataPoint dataPoint, Map<String, Long> data) { | 174 | + private void populateMetrics(ChartModel cm, Map<ControlMetricType, |
| 175 | + Long[]> data, LocalDateTime time, int numOfDp) { | ||
| 176 | + for (int i = 0; i < numOfDp; i++) { | ||
| 177 | + Map<String, Long> local = Maps.newHashMap(); | ||
| 178 | + for (ControlMetricType cmt : CONTROL_MESSAGE_METRICS) { | ||
| 179 | + local.put(StringUtils.lowerCase(cmt.name()), data.get(cmt)[i]); | ||
| 180 | + } | ||
| 129 | 181 | ||
| 182 | + local.put(LABEL, time.minusMinutes(numOfDp - i).toDateTime().getMillis()); | ||
| 183 | + | ||
| 184 | + populateMetric(cm.addDataPoint(time.minusMinutes(numOfDp - i) | ||
| 185 | + .toDateTime().getMillis()), local); | ||
| 186 | + } | ||
| 130 | } | 187 | } |
| 131 | 188 | ||
| 132 | private void populateMetric(ChartModel.DataPoint dataPoint, | 189 | private void populateMetric(ChartModel.DataPoint dataPoint, | ... | ... |
| ... | @@ -20,6 +20,7 @@ | ... | @@ -20,6 +20,7 @@ |
| 20 | 20 | ||
| 21 | #ov-cpman { | 21 | #ov-cpman { |
| 22 | padding: 20px; | 22 | padding: 20px; |
| 23 | + position: relative; | ||
| 23 | } | 24 | } |
| 24 | .light #ov-cpman { | 25 | .light #ov-cpman { |
| 25 | color: navy; | 26 | color: navy; |
| ... | @@ -40,22 +41,17 @@ | ... | @@ -40,22 +41,17 @@ |
| 40 | background-color: #444; | 41 | background-color: #444; |
| 41 | } | 42 | } |
| 42 | 43 | ||
| 43 | -#ov-cpman .my-button { | 44 | +#ov-cpman #chart-loader { |
| 44 | - cursor: pointer; | 45 | + position: absolute; |
| 45 | - padding: 4px; | 46 | + width: 200px; |
| 47 | + height: 50px; | ||
| 48 | + margin-left: -100px; | ||
| 49 | + margin-top: -25px; | ||
| 50 | + z-index: 900; | ||
| 51 | + top: 50%; | ||
| 46 | text-align: center; | 52 | text-align: center; |
| 47 | -} | 53 | + left: 50%; |
| 48 | - | 54 | + font-size: 25px; |
| 49 | -.light #ov-cpman .my-button { | 55 | + font-weight: bold; |
| 50 | - color: white; | 56 | + color: #ccc; |
| 51 | - background-color: #99d; | ||
| 52 | -} | ||
| 53 | -.dark #ov-cpman .my-button { | ||
| 54 | - color: black; | ||
| 55 | - background-color: #aaa; | ||
| 56 | -} | ||
| 57 | - | ||
| 58 | -#ov-cpman .number { | ||
| 59 | - font-size: 140%; | ||
| 60 | - text-align: right; | ||
| 61 | } | 57 | } |
| ... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
| 1 | <!-- partial HTML --> | 1 | <!-- partial HTML --> |
| 2 | <div id="ov-cpman"> | 2 | <div id="ov-cpman"> |
| 3 | - <div> | 3 | + <div id="chart-loader" ng-show="!devId && showLoader"> |
| 4 | + No Data | ||
| 5 | + </div> | ||
| 6 | + <div ng-show="!devId"> | ||
| 7 | + <canvas id="bar" class="chart chart-bar" chart-data="data" | ||
| 8 | + chart-labels="labels" chart-legend="true" chart-click="onClick" | ||
| 9 | + chart-series="series" chart-options="options" height="100%"> | ||
| 10 | + </canvas> | ||
| 11 | + </div> | ||
| 12 | + <div ng-show="devId"> | ||
| 13 | + <h2> | ||
| 14 | + Chart for Device {{devId || "(No device selected)"}} | ||
| 15 | + </h2> | ||
| 4 | <canvas id="line" class="chart chart-line" chart-data="data" | 16 | <canvas id="line" class="chart chart-line" chart-data="data" |
| 5 | - chart-labels="labels" chart-legend="true" chart-series="series"> | 17 | + chart-labels="labels" chart-legend="true" |
| 18 | + chart-series="series" chart-options="options" height="100%"> | ||
| 6 | </canvas> | 19 | </canvas> |
| 7 | </div> | 20 | </div> |
| 8 | </div> | 21 | </div> | ... | ... |
| ... | @@ -21,27 +21,48 @@ | ... | @@ -21,27 +21,48 @@ |
| 21 | 'use strict'; | 21 | 'use strict'; |
| 22 | 22 | ||
| 23 | // injected references | 23 | // injected references |
| 24 | - var $log, $scope, $location, ks, fs, cbs; | 24 | + var $log, $scope, $location, ks, fs, cbs, ns; |
| 25 | 25 | ||
| 26 | - var labels = new Array(60); | 26 | + var hasDeviceId; |
| 27 | - var data = new Array(new Array(60), new Array(60), new Array(60), | 27 | + |
| 28 | - new Array(60), new Array(60), new Array(60)); | 28 | + var labels = new Array(1); |
| 29 | + var data = new Array(6); | ||
| 30 | + for (var i = 0; i < 6; i++) { | ||
| 31 | + data[i] = new Array(1); | ||
| 32 | + } | ||
| 33 | + | ||
| 34 | + var date, max, merged; | ||
| 35 | + | ||
| 36 | + function ceil(num) { | ||
| 37 | + if (isNaN(num)) { | ||
| 38 | + return 0; | ||
| 39 | + } | ||
| 40 | + var pre = num.toString().length - 1 | ||
| 41 | + var pow = Math.pow(10, pre); | ||
| 42 | + return (Math.ceil(num / pow)) * pow; | ||
| 43 | + } | ||
| 29 | 44 | ||
| 30 | angular.module('ovCpman', ["chart.js"]) | 45 | angular.module('ovCpman', ["chart.js"]) |
| 31 | .controller('OvCpmanCtrl', | 46 | .controller('OvCpmanCtrl', |
| 32 | - ['$log', '$scope', '$location', 'FnService', 'ChartBuilderService', | 47 | + ['$log', '$scope', '$location', 'FnService', 'ChartBuilderService', 'NavService', |
| 33 | 48 | ||
| 34 | - function (_$log_, _$scope_, _$location_, _fs_, _cbs_) { | 49 | + function (_$log_, _$scope_, _$location_, _fs_, _cbs_, _ns_) { |
| 35 | var params; | 50 | var params; |
| 36 | $log = _$log_; | 51 | $log = _$log_; |
| 37 | $scope = _$scope_; | 52 | $scope = _$scope_; |
| 38 | $location = _$location_; | 53 | $location = _$location_; |
| 39 | fs = _fs_; | 54 | fs = _fs_; |
| 40 | cbs = _cbs_; | 55 | cbs = _cbs_; |
| 56 | + ns = _ns_; | ||
| 41 | 57 | ||
| 42 | params = $location.search(); | 58 | params = $location.search(); |
| 59 | + | ||
| 43 | if (params.hasOwnProperty('devId')) { | 60 | if (params.hasOwnProperty('devId')) { |
| 44 | $scope.devId = params['devId']; | 61 | $scope.devId = params['devId']; |
| 62 | + hasDeviceId = true; | ||
| 63 | + } else { | ||
| 64 | + $scope.type = 'StackedBar'; | ||
| 65 | + hasDeviceId = false; | ||
| 45 | } | 66 | } |
| 46 | 67 | ||
| 47 | cbs.buildChart({ | 68 | cbs.buildChart({ |
| ... | @@ -50,31 +71,70 @@ | ... | @@ -50,31 +71,70 @@ |
| 50 | query: params | 71 | query: params |
| 51 | }); | 72 | }); |
| 52 | 73 | ||
| 53 | - var idx = 0; | ||
| 54 | - var date; | ||
| 55 | $scope.$watch('chartData', function () { | 74 | $scope.$watch('chartData', function () { |
| 56 | - idx = 0; | ||
| 57 | if (!fs.isEmptyObject($scope.chartData)) { | 75 | if (!fs.isEmptyObject($scope.chartData)) { |
| 58 | - $scope.chartData.forEach(function (cm) { | 76 | + $scope.showLoader = false; |
| 77 | + var length = $scope.chartData.length; | ||
| 78 | + labels = new Array(length); | ||
| 79 | + for (var i = 0; i < 6; i++) { | ||
| 80 | + data[i] = new Array(length); | ||
| 81 | + } | ||
| 82 | + | ||
| 83 | + $scope.chartData.forEach(function (cm, idx) { | ||
| 59 | data[0][idx] = cm.inbound_packet; | 84 | data[0][idx] = cm.inbound_packet; |
| 60 | data[1][idx] = cm.outbound_packet; | 85 | data[1][idx] = cm.outbound_packet; |
| 61 | data[2][idx] = cm.flow_mod_packet; | 86 | data[2][idx] = cm.flow_mod_packet; |
| 62 | data[3][idx] = cm.flow_removed_packet; | 87 | data[3][idx] = cm.flow_removed_packet; |
| 63 | data[4][idx] = cm.request_packet; | 88 | data[4][idx] = cm.request_packet; |
| 64 | data[5][idx] = cm.reply_packet; | 89 | data[5][idx] = cm.reply_packet; |
| 65 | - date = new Date(cm.label); | 90 | + |
| 66 | - labels[idx] = date.getHours() + ":" + date.getMinutes(); | 91 | + if(hasDeviceId) { |
| 67 | - idx++; | 92 | + date = new Date(cm.label); |
| 93 | + labels[idx] = date.getHours() + ":" + date.getMinutes(); | ||
| 94 | + } else { | ||
| 95 | + labels[idx] = cm.label; | ||
| 96 | + } | ||
| 68 | }); | 97 | }); |
| 69 | } | 98 | } |
| 99 | + | ||
| 100 | + merged = [].concat.apply([], data); | ||
| 101 | + max = Math.max.apply(null, merged); | ||
| 102 | + $scope.labels = labels; | ||
| 103 | + $scope.data = data; | ||
| 104 | + $scope.options = { | ||
| 105 | + scaleOverride : true, | ||
| 106 | + scaleSteps : 10, | ||
| 107 | + scaleStepWidth : ceil(max) / 10, | ||
| 108 | + scaleStartValue : 0 | ||
| 109 | + }; | ||
| 110 | + $scope.onClick = function (points, evt) { | ||
| 111 | + if (points[0]) { | ||
| 112 | + // TODO: this will be replaced with real device id | ||
| 113 | + var tmpId = 'of:000000000000020' + points[0].label; | ||
| 114 | + ns.navTo('cpman', { devId: tmpId }); | ||
| 115 | + $log.log(points[0].label); | ||
| 116 | + } | ||
| 117 | + }; | ||
| 70 | }); | 118 | }); |
| 71 | 119 | ||
| 72 | $scope.series = ['INBOUND', 'OUTBOUND', 'FLOW-MOD', | 120 | $scope.series = ['INBOUND', 'OUTBOUND', 'FLOW-MOD', |
| 73 | 'FLOW-REMOVED', 'STATS-REQUEST', 'STATS-REPLY']; | 121 | 'FLOW-REMOVED', 'STATS-REQUEST', 'STATS-REPLY']; |
| 74 | $scope.labels = labels; | 122 | $scope.labels = labels; |
| 75 | - | ||
| 76 | $scope.data = data; | 123 | $scope.data = data; |
| 77 | 124 | ||
| 125 | + $scope.chartColors = [ | ||
| 126 | + '#286090', | ||
| 127 | + '#F7464A', | ||
| 128 | + '#46BFBD', | ||
| 129 | + '#FDB45C', | ||
| 130 | + '#97BBCD', | ||
| 131 | + '#4D5360', | ||
| 132 | + '#8c4f9f' | ||
| 133 | + ]; | ||
| 134 | + Chart.defaults.global.colours = $scope.chartColors; | ||
| 135 | + | ||
| 136 | + $scope.showLoader = true; | ||
| 137 | + | ||
| 78 | $log.log('OvCpmanCtrl has been created'); | 138 | $log.log('OvCpmanCtrl has been created'); |
| 79 | }]); | 139 | }]); |
| 80 | 140 | ... | ... |
-
Please register or login to post a comment