[ONOS-6375] Add detailed view for mapping management app
Change-Id: Ib182e5e394173c98705e8653121c92e602764e20
diff --git a/apps/mappingmanagement/web/src/main/java/org/onosproject/mapping/web/gui/MappingsViewMessageHandler.java b/apps/mappingmanagement/web/src/main/java/org/onosproject/mapping/web/gui/MappingsViewMessageHandler.java
index b46e3cd..84532dd 100644
--- a/apps/mappingmanagement/web/src/main/java/org/onosproject/mapping/web/gui/MappingsViewMessageHandler.java
+++ b/apps/mappingmanagement/web/src/main/java/org/onosproject/mapping/web/gui/MappingsViewMessageHandler.java
@@ -15,14 +15,19 @@
*/
package org.onosproject.mapping.web.gui;
+import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
import org.onosproject.mapping.MappingEntry;
import org.onosproject.mapping.MappingService;
import org.onosproject.mapping.MappingTreatment;
-import org.onosproject.mapping.MappingValue;
import org.onosproject.mapping.addresses.MappingAddress;
+import org.onosproject.mapping.instructions.MappingInstruction;
+import org.onosproject.mapping.instructions.MulticastMappingInstruction;
+import org.onosproject.mapping.instructions.MulticastMappingInstruction.MulticastType;
+import org.onosproject.mapping.instructions.UnicastMappingInstruction;
+import org.onosproject.mapping.instructions.UnicastMappingInstruction.UnicastType;
import org.onosproject.net.DeviceId;
import org.onosproject.ui.RequestHandler;
import org.onosproject.ui.UiMessageHandler;
@@ -33,10 +38,11 @@
import org.onosproject.ui.table.cell.HexLongFormatter;
import java.util.Collection;
-import java.util.List;
import static org.onosproject.mapping.MappingStore.Type.MAP_CACHE;
import static org.onosproject.mapping.MappingStore.Type.MAP_DATABASE;
+import static org.onosproject.mapping.instructions.MappingInstruction.Type.MULTICAST;
+import static org.onosproject.mapping.instructions.MappingInstruction.Type.UNICAST;
/**
* Message handler for mapping management view related messages.
@@ -47,7 +53,12 @@
private static final String MAPPING_DATA_RESP = "mappingDataResponse";
private static final String MAPPINGS = "mappings";
+ private static final String MAPPING_DETAIL_REQ = "mappingDetailsRequest";
+ private static final String MAPPING_DETAIL_RESP = "mappingDetailsResponse";
+ private static final String DETAILS = "details";
+
private static final String ID = "id";
+ private static final String MAPPING_ID = "mappingId";
private static final String MAPPING_KEY = "mappingKey";
private static final String MAPPING_VALUE = "mappingValue";
private static final String MAPPING_ACTION = "mappingAction";
@@ -55,6 +66,13 @@
private static final String STATE = "state";
private static final String DATABASE = "database";
private static final String CACHE = "cache";
+ private static final String MAPPING_TREATMENTS = "mappingTreatments";
+
+ private static final String MAPPING_ADDRESS = "address";
+ private static final String UNICAST_WEIGHT = "unicastWeight";
+ private static final String UNICAST_PRIORITY = "unicastPriority";
+ private static final String MULTICAST_WEIGHT = "multicastWeight";
+ private static final String MULTICAST_PRIORITY = "multicastPriority";
private static final String COMMA = ", ";
private static final String OX = "0x";
@@ -68,7 +86,10 @@
@Override
protected Collection<RequestHandler> createRequestHandlers() {
- return ImmutableSet.of(new MappingMessageRequest());
+ return ImmutableSet.of(
+ new MappingMessageRequest(),
+ new DetailRequestHandler()
+ );
}
/**
@@ -99,7 +120,6 @@
tm.setFormatter(TYPE, EnumFormatter.INSTANCE);
tm.setFormatter(STATE, EnumFormatter.INSTANCE);
tm.setFormatter(MAPPING_KEY, new MappingKeyFormatter());
- tm.setFormatter(MAPPING_VALUE, new MappingValueFormatter());
return tm;
}
@@ -126,8 +146,7 @@
.cell(STATE, mapping.state())
.cell(TYPE, type)
.cell(MAPPING_ACTION, mapping.value().action())
- .cell(MAPPING_KEY, mapping)
- .cell(MAPPING_VALUE, mapping);
+ .cell(MAPPING_KEY, mapping);
}
}
@@ -144,37 +163,121 @@
if (address == null) {
return NULL_ADDRESS_MSG;
}
- StringBuilder sb = new StringBuilder("Mapping address: ");
- sb.append(address.toString());
- return sb.toString();
+ return address.toString();
}
}
/**
- * A formatter for formatting mapping value.
+ * Handler for detailed mapping message requests.
*/
- private final class MappingValueFormatter implements CellFormatter {
+ private final class DetailRequestHandler extends RequestHandler {
- @Override
- public String format(Object value) {
- MappingEntry mapping = (MappingEntry) value;
- MappingValue mappingValue = mapping.value();
- List<MappingTreatment> treatments = mappingValue.treatments();
-
- StringBuilder sb = new StringBuilder("Treatments: ");
- formatTreatments(sb, treatments);
-
- return sb.toString();
+ private DetailRequestHandler() {
+ super(MAPPING_DETAIL_REQ);
}
- private void formatTreatments(StringBuilder sb,
- List<MappingTreatment> treatments) {
- if (!treatments.isEmpty()) {
- for (MappingTreatment t : treatments) {
- sb.append(t).append(COMMA);
+ private MappingEntry findMappingById(String mappingId) {
+ MappingService ms = get(MappingService.class);
+ Iterable<MappingEntry> dbEntries = ms.getAllMappingEntries(MAP_DATABASE);
+ Iterable<MappingEntry> cacheEntries = ms.getAllMappingEntries(MAP_CACHE);
+
+ for (MappingEntry entry : dbEntries) {
+ if (entry.id().toString().equals(mappingId)) {
+ return entry;
}
}
+
+ for (MappingEntry entry : cacheEntries) {
+ if (entry.id().toString().equals(mappingId)) {
+ return entry;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Generates a node object of a given mapping treatment.
+ *
+ * @param treatment mapping treatment
+ * @return node object
+ */
+ private ObjectNode getTreatmentNode(MappingTreatment treatment) {
+ ObjectNode data = objectNode();
+
+ data.put(MAPPING_ADDRESS, treatment.address().toString());
+
+ for (MappingInstruction instruct : treatment.instructions()) {
+ if (instruct.type() == UNICAST) {
+ UnicastMappingInstruction unicastInstruct =
+ (UnicastMappingInstruction) instruct;
+ if (unicastInstruct.subtype() == UnicastType.WEIGHT) {
+ data.put(UNICAST_WEIGHT,
+ ((UnicastMappingInstruction.WeightMappingInstruction)
+ unicastInstruct).weight());
+ }
+ if (unicastInstruct.subtype() == UnicastType.PRIORITY) {
+ data.put(UNICAST_PRIORITY,
+ ((UnicastMappingInstruction.PriorityMappingInstruction)
+ unicastInstruct).priority());
+ }
+ }
+
+ if (instruct.type() == MULTICAST) {
+ MulticastMappingInstruction multicastInstruct =
+ (MulticastMappingInstruction) instruct;
+ if (multicastInstruct.subtype() == MulticastType.WEIGHT) {
+ data.put(MULTICAST_WEIGHT,
+ ((MulticastMappingInstruction.WeightMappingInstruction)
+ multicastInstruct).weight());
+ }
+ if (multicastInstruct.subtype() == MulticastType.PRIORITY) {
+ data.put(MULTICAST_PRIORITY,
+ ((MulticastMappingInstruction.PriorityMappingInstruction)
+ multicastInstruct).priority());
+ }
+ }
+
+ // TODO: extension address will be handled later
+ }
+
+ return data;
+ }
+
+ @Override
+ public void process(ObjectNode payload) {
+ String mappingId = string(payload, MAPPING_ID);
+ String type = string(payload, TYPE);
+ String strippedFlowId = mappingId.replaceAll(OX, EMPTY);
+
+ MappingEntry mapping = findMappingById(strippedFlowId);
+ if (mapping != null) {
+ ArrayNode arrayNode = arrayNode();
+
+ for (MappingTreatment treatment : mapping.value().treatments()) {
+ arrayNode.add(getTreatmentNode(treatment));
+ }
+
+ ObjectNode detailsNode = objectNode();
+ detailsNode.put(MAPPING_ID, mappingId);
+ detailsNode.put(STATE, mapping.state().name());
+ detailsNode.put(TYPE, type);
+ detailsNode.put(MAPPING_ACTION, mapping.value().action().toString());
+
+ ObjectNode keyNode = objectNode();
+ keyNode.put(MAPPING_ADDRESS, mapping.key().address().toString());
+
+ ObjectNode valueNode = objectNode();
+ valueNode.set(MAPPING_TREATMENTS, arrayNode);
+
+ detailsNode.set(MAPPING_KEY, keyNode);
+ detailsNode.set(MAPPING_VALUE, valueNode);
+
+ ObjectNode rootNode = objectNode();
+ rootNode.set(DETAILS, detailsNode);
+ sendMessage(MAPPING_DETAIL_RESP, rootNode);
+ }
}
}
}
diff --git a/apps/mappingmanagement/web/src/main/resources/app/view/mapping/mapping.css b/apps/mappingmanagement/web/src/main/resources/app/view/mapping/mapping.css
index 16f65cd..0c5c611 100644
--- a/apps/mappingmanagement/web/src/main/resources/app/view/mapping/mapping.css
+++ b/apps/mappingmanagement/web/src/main/resources/app/view/mapping/mapping.css
@@ -35,3 +35,55 @@
text-align: left;
padding-left: 36px;
}
+
+/* More in generic panel.css */
+
+#mapping-details-panel.floatpanel {
+ z-index: 0;
+}
+
+#mapping-details-panel .container {
+ padding: 8px 12px;
+}
+
+#mapping-details-panel .close-btn {
+ position: absolute;
+ right: 12px;
+ top: 12px;
+ cursor: pointer;
+}
+
+#mapping-details-panel .dev-icon {
+ display: inline-block;
+ padding: 0 6px 0 0;
+ vertical-align: middle;
+}
+
+#mapping-details-panel h2 {
+ display: inline-block;
+ margin: 8px 0;
+}
+
+#mapping-details-panel .top-content table {
+ font-size: 12pt;
+}
+
+#mapping-details-panel td.label {
+ font-weight: bold;
+ text-align: right;
+ padding-right: 6px;
+}
+
+#mapping-details-panel .bottom table {
+ border-spacing: 0;
+}
+
+#mapping-details-panel .bottom th {
+ letter-spacing: 0.02em;
+}
+
+#mapping-details-panel .bottom th,
+#mapping-details-panel .bottom td {
+ padding: 6px 12px;
+ text-align: center;
+}
\ No newline at end of file
diff --git a/apps/mappingmanagement/web/src/main/resources/app/view/mapping/mapping.html b/apps/mappingmanagement/web/src/main/resources/app/view/mapping/mapping.html
index 0971144..0572210 100644
--- a/apps/mappingmanagement/web/src/main/resources/app/view/mapping/mapping.html
+++ b/apps/mappingmanagement/web/src/main/resources/app/view/mapping/mapping.html
@@ -58,7 +58,7 @@
</td>
</tr>
- <tr ng-repeat-start="mapping in tableData | filter:queryFilter track by $index"
+ <tr ng-repeat="mapping in tableData | filter:queryFilter track by $index"
ng-click="selectCallback($event, mapping)"
ng-class="{selected: mapping.id === selId}"
ng-repeat-complete row-id="{{mapping.id}}">
@@ -68,10 +68,8 @@
<td>{{mapping.mappingKey}}</td>
<td>{{mapping.mappingAction}}</td>
</tr>
- <tr row-id="{{mapping.id}}" ng-repeat-end ng-hide="brief">
- <td class="mappingValue" colspan="5">{{mapping.mappingValue}}</td>
- </tr>
</table>
</div>
</div>
+ <mapping-details-panel></mapping-details-panel>
</div>
diff --git a/apps/mappingmanagement/web/src/main/resources/app/view/mapping/mapping.js b/apps/mappingmanagement/web/src/main/resources/app/view/mapping/mapping.js
index f047ebc..bae6b89 100644
--- a/apps/mappingmanagement/web/src/main/resources/app/view/mapping/mapping.js
+++ b/apps/mappingmanagement/web/src/main/resources/app/view/mapping/mapping.js
@@ -21,19 +21,205 @@
'use strict';
// injected references
- var $log, $scope, $location, tbs;
+ var $log, $scope, $location, fs, tbs, mast, ps, wss, is, ks;
+
+ // internal state
+ var detailsPanel,
+ pStartY,
+ pHeight,
+ top,
+ topTable,
+ keyDiv,
+ valueDiv,
+ topKeyTable,
+ bottomValueTable,
+ iconDiv,
+ nameDiv,
+ wSize;
+
+ // constants
+ var topPdg = 28,
+ ctnrPdg = 24,
+ scrollSize = 17,
+ portsTblPdg = 50,
+ htPdg = 479,
+ wtPdg = 532,
+
+ pName = 'mapping-details-panel',
+ detailsReq = 'mappingDetailsRequest',
+ detailsResp = 'mappingDetailsResponse',
+
+ propOrder = [
+ 'mappingId', 'type', 'state', 'mappingAction'
+ ],
+ friendlyProps = [
+ 'Mapping ID', 'Store Type', 'State', 'Mapping Action'
+ ],
+
+ treatmentPropOrder = [
+ 'address', 'unicastPriority', 'unicastWeight', 'multicastPriority',
+ 'multicastWeight'
+ ],
+ treatmentFriendlyProps = [
+ 'Address', 'U P', 'U W', 'M P', 'M W'
+ ];
+
+ function closePanel() {
+ if (detailsPanel.isVisible()) {
+ $scope.selId = null;
+ detailsPanel.hide();
+ return true;
+ }
+ return false;
+ }
+
+ function addCloseBtn(div) {
+ is.loadEmbeddedIcon(div, 'close', 20);
+ div.on('click', closePanel);
+ }
+
+ function handleEscape() {
+ return closePanel();
+ }
+
+ function setUpPanel() {
+ var container, closeBtn, tblDiv;
+ detailsPanel.empty();
+
+ container = detailsPanel.append('div').classed('container', true);
+ top = container.append('div').classed('top', true);
+ keyDiv = container.append('div').classed('top', true);
+ valueDiv = container.append('div').classed('bottom', true);
+ closeBtn = top.append('div').classed('close-btn', true);
+ addCloseBtn(closeBtn);
+ iconDiv = top.append('div').classed('dev-icon', true);
+ top.append('h2');
+ topTable = top.append('div').classed('top-content', true)
+ .append('table');
+ top.append('hr');
+ keyDiv.append('h2').html('Mapping Key');
+ topKeyTable = keyDiv.append('div').classed('top-content', true)
+ .append('table');
+ keyDiv.append('hr');
+ valueDiv.append('h2').html('Mapping Value');
+ bottomValueTable = valueDiv.append('table');
+
+ // TODO: add more details later
+ }
+
+ function addProp(tbody, index, value) {
+ var tr = tbody.append('tr');
+
+ function addCell(cls, txt) {
+ tr.append('td').attr('class', cls).html(txt);
+ }
+ addCell('label', friendlyProps[index] + ' :');
+ addCell('value', value);
+ }
+
+ function populateTable(tbody, label, value) {
+ var tr = tbody.append('tr');
+
+ function addCell(cls, txt) {
+ tr.append('td').attr('class', cls).html(txt);
+ }
+ addCell('label', label + ' :');
+ addCell('value', value);
+ }
+
+ function populateTop(details) {
+ is.loadEmbeddedIcon(iconDiv, 'mappingTable', 40);
+ top.select('h2').html(details.mappingId);
+
+ var tbody = topTable.append('tbody');
+
+ var topKeyTablebody = topKeyTable.append('tbody');
+ var keyObject = details['mappingKey'];
+ var address = keyObject.address;
+
+ var bottomValueTableheader = bottomValueTable.append('thead').append('tr')
+ var bottomValueTablebody = bottomValueTable.append('tbody');
+ var valueObject = details['mappingValue'];
+ var treatments = valueObject.mappingTreatments;
+
+ propOrder.forEach(function (prop, i) {
+ addProp(tbody, i, details[prop]);
+ });
+
+ topKeyTablebody.append('tr').append('td').attr('class', 'value').html(address);
+
+ treatmentFriendlyProps.forEach(function (col) {
+ bottomValueTableheader.append('th').html(col);
+ });
+ treatments.forEach(function (sel) {
+ populateTreatmentTable(bottomValueTablebody, sel);
+ });
+ }
+
+ function populateTreatmentTable(tbody, treatment) {
+ var tr = tbody.append('tr');
+ treatmentPropOrder.forEach(function (prop) {
+ addTreatmentProp(tr, treatment[prop]);
+ });
+ }
+
+ function addTreatmentProp(tr, value) {
+ function addCell(cls, txt) {
+ tr.append('td').attr('class', cls).html(txt);
+ }
+ addCell('value', value);
+ }
+
+ function createDetailsPane() {
+ detailsPanel = ps.createPanel(pName, {
+ width: wSize.width,
+ margin: 0,
+ hideMargin: 0
+ });
+ detailsPanel.el().style({
+ position: 'absolute',
+ top: pStartY + 'px'
+ });
+ $scope.hidePanel = function () { detailsPanel.hide(); };
+ detailsPanel.hide();
+ }
+
+ function populateDetails(details) {
+ setUpPanel();
+ populateTop(details);
+
+ //ToDo add more details
+ detailsPanel.height(pHeight);
+ detailsPanel.width(wtPdg);
+ }
+
+ function respDetailsCb(data) {
+ $log.debug("Got response from server :", data);
+ $scope.panelData = data.details;
+ $scope.$apply();
+ }
angular.module('ovMapping', [])
.controller('OvMappingCtrl',
- ['$log', '$scope', '$location', 'TableBuilderService',
+ ['$log', '$scope', '$location', 'FnService', 'TableBuilderService',
+ 'MastService', 'PanelService', 'KeyService', 'IconService',
+ 'WebSocketService',
- function (_$log_, _$scope_, _$location_, _tbs_) {
- var params;
+ function (_$log_, _$scope_, _$location_, _fs_, _tbs_, _mast_, _ps_,
+ _ks_, _is_, _wss_) {
+ var params,
+ handlers = {};
$log = _$log_;
$scope = _$scope_;
$location = _$location_;
+ fs = _fs_;
tbs = _tbs_;
+ mast = _mast_;
+ ps = _ps_;
+ ks = _ks_;
+ is = _is_;
+ wss = _wss_;
params = $location.search();
if (params.hasOwnProperty('devId')) {
@@ -43,9 +229,90 @@
tbs.buildTable({
scope: $scope,
tag: 'mapping',
+ selCb: selCb,
query: params
});
+ // details panel handlers
+ handlers[detailsResp] = respDetailsCb;
+ wss.bindHandlers(handlers);
+
+ function selCb($event, row) {
+ if ($scope.selId) {
+ wss.sendEvent(detailsReq, {mappingId: row.id, type: row.type});
+ } else {
+ $scope.hidePanel();
+ }
+ $log.debug('Got a click on:', row);
+ }
+
+ $scope.$on('$destroy', function () {
+ wss.unbindHandlers(handlers);
+ });
+
$log.log('OvMappingCtrl has been created');
- }]);
+ }])
+
+ .directive('mappingDetailsPanel',
+ ['$rootScope', '$window', '$timeout', 'KeyService',
+ function ($rootScope, $window, $timeout, ks) {
+ return function (scope) {
+ var unbindWatch;
+
+ function heightCalc() {
+ pStartY = fs.noPxStyle(d3.select('.tabular-header'), 'height')
+ + mast.mastHeight() + topPdg;
+ wSize = fs.windowSize(pStartY);
+ pHeight = wSize.height;
+ }
+
+ function initPanel() {
+ heightCalc();
+ createDetailsPane();
+ }
+
+ // Safari has a bug where it renders the fixed-layout table wrong
+ // if you ask for the window's size too early
+ if (scope.onos.browser === 'safari') {
+ $timeout(initPanel);
+ } else {
+ initPanel();
+ }
+ // create key bindings to handle panel
+ ks.keyBindings({
+ esc: [handleEscape, 'Close the details panel'],
+ _helpFormat: ['esc']
+ });
+ ks.gestureNotes([
+ ['click', 'Select a row to show cluster node details'],
+ ['scroll down', 'See available cluster nodes']
+ ]);
+ // if the panelData changes
+ scope.$watch('panelData', function () {
+ if (!fs.isEmptyObject(scope.panelData)) {
+ populateDetails(scope.panelData);
+ detailsPanel.show();
+ }
+ });
+ // if the window size changes
+ unbindWatch = $rootScope.$watchCollection(
+ function () {
+ return {
+ h: $window.innerHeight,
+ w: $window.innerWidth
+ };
+ }, function () {
+ if (!fs.isEmptyObject(scope.panelData)) {
+ heightCalc();
+ populateDetails(scope.panelData);
+ }
+ }
+ );
+
+ scope.$on('$destroy', function () {
+ unbindWatch();
+ ps.destroyPanel(pName);
+ });
+ };
+ }]);
}());