blob: b04bd531689f8e3688b5c00f4aacaca5db8d3234 [file] [log] [blame]
/*
* Copyright 2015-present Open Networking Laboratory
*
* 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.
*
*/
/*
ONOS GUI -- Topology Overlay Module.
Provides overlay capabilities, allowing ONOS apps to provide additional
custom data/behavior for the topology view.
*/
(function () {
'use strict';
// constants
var tos = 'TopoOverlayService: ';
// injected refs
var $log, $timeout, fs, gs, wss, ns, tss, tps, api;
// internal state
var overlays = {},
current = null,
reset = true;
function error(fn, msg) {
$log.error(tos + fn + '(): ' + msg);
}
function warn(fn, msg) {
$log.warn(tos + fn + '(): ' + msg);
}
function mkGlyphId(oid, gid) {
return (gid[0] === '*') ? oid + '-' + gid.slice(1) : gid;
}
function handleGlyphs(o) {
var gdata = fs.isO(o.glyphs),
oid = o.overlayId,
gid = o.glyphId || 'unknown',
data = {},
note = [];
o._glyphId = mkGlyphId(oid, gid);
o.mkGid = function (g) {
return mkGlyphId(oid, g);
};
o.mkId = function (s) {
return oid + '-' + s;
};
// process glyphs if defined
if (gdata) {
angular.forEach(gdata, function (value, key) {
var fullkey = oid + '-' + key;
data['_' + fullkey] = value.vb;
data[fullkey] = value.d;
note.push('*' + key);
});
gs.registerGlyphs(data);
$log.debug('registered overlay glyphs:', oid, note);
}
}
function register(overlay) {
var r = 'register',
over = fs.isO(overlay),
kb = over ? fs.isO(overlay.keyBindings) : null,
id = over ? over.overlayId : '';
if (!id) {
return error(r, 'not a recognized overlay');
}
if (overlays[id]) {
return warn(r, 'already registered: "' + id + '"');
}
overlays[id] = overlay;
handleGlyphs(overlay);
if (kb) {
if (!fs.isA(kb._keyOrder)) {
warn(r, 'no _keyOrder array defined on keyBindings');
} else {
kb._keyOrder.forEach(function (k) {
if (k !== '-' && !kb[k]) {
warn(r, 'no "' + k + '" property defined on keyBindings');
}
});
}
}
$log.debug(tos + 'registered overlay: ' + id, overlay);
}
// returns the list of overlay identifiers
function list() {
return d3.map(overlays).keys();
}
// add a radio button for each registered overlay
// return an overlay id to index map
function augmentRbset(rset, switchFn) {
var map = {},
idx = 1;
angular.forEach(overlays, function (ov) {
rset.push({
gid: ov._glyphId,
tooltip: (ov.tooltip || '(no tooltip)'),
cb: function () {
tbSelection(ov.overlayId, switchFn);
}
});
map[ov.overlayId] = idx++;
});
return map;
}
// an overlay was selected via toolbar radio button press from user
function tbSelection(id, switchFn) {
var same = current && current.overlayId === id,
payload = {},
actions;
function doop(op) {
var oid = current.overlayId;
$log.debug('Overlay:', op, oid);
current[op]();
payload[op] = oid;
}
if (reset || !same) {
reset = false;
current && doop('deactivate');
current = overlays[id];
current && doop('activate');
actions = current && fs.isO(current.keyBindings);
switchFn(id, actions);
wss.sendEvent('topoSelectOverlay', payload);
// Ensure summary and details panels are updated immediately..
wss.sendEvent('requestSummary');
tss.updateDetail();
}
}
var coreButtons = {
showDeviceView: {
gid: 'switch',
tt: 'Show Device View',
path: 'device'
},
showFlowView: {
gid: 'flowTable',
tt: 'Show Flow View for this Device',
path: 'flow'
},
showPortView: {
gid: 'portTable',
tt: 'Show Port View for this Device',
path: 'port'
},
showGroupView: {
gid: 'groupTable',
tt: 'Show Group View for this Device',
path: 'group'
},
showMeterView: {
gid: 'meterTable',
tt: 'Show Meter View for this Device',
path: 'meter'
}
};
// retrieves a button definition from the current overlay and generates
// a button descriptor to be added to the panel, with the data baked in
function _getButtonDef(id, data) {
var btns = current && current.buttons,
b = btns && btns[id],
cb = fs.isF(b.cb),
f = cb ? function () { cb(data); } : function () {};
return b ? {
id: current.mkId(id),
gid: current.mkGid(b.gid),
tt: b.tt,
cb: f
} : null;
}
// install core buttons, and include any additional from the current overlay
function installButtons(buttons, data, devId) {
buttons.forEach(function (id) {
var btn = coreButtons[id],
gid = btn && btn.gid,
tt = btn && btn.tt,
path = btn && btn.path;
if (btn) {
tps.addAction({
id: 'core-' + id,
gid: gid,
tt: tt,
cb: function () { ns.navTo(path, {devId: devId }); }
});
} else if (btn = _getButtonDef(id, data)) {
tps.addAction(btn);
}
});
}
function addDetailButton(id) {
var b = _getButtonDef(id);
if (b) {
tps.addAction({
id: current.mkId(id),
gid: current.mkGid(b.gid),
cb: b.cb,
tt: b.tt
});
}
}
// === -----------------------------------------------------
// Hooks for overlays
function _hook(x) {
var h = current && current.hooks;
return h && fs.isF(h[x]);
}
function escapeHook() {
var eh = _hook('escape');
return eh ? eh() : false;
}
function emptySelectHook() {
var cb = _hook('empty');
cb && cb();
}
function singleSelectHook(data) {
var cb = _hook('single');
cb && cb(data);
}
function multiSelectHook(selectOrder) {
var cb = _hook('multi');
cb && cb(selectOrder);
}
function mouseOverHook(what) {
var cb = _hook('mouseover');
cb && cb(what);
}
function mouseOutHook() {
var cb = _hook('mouseout');
cb && cb();
}
// Temporary function to allow overlays to modify link detail data
// in the client. (In the near future, this will be done on the server).
function modifyLinkDataHook(data, extra) {
var cb = _hook('modifylinkdata');
return cb && extra ? cb(data, extra) : data;
}
// === -----------------------------------------------------
// Event (from server) Handlers
function setApi(_api_, _tss_) {
api = _api_;
tss = _tss_;
}
//process highlight event with optional delay
function showHighlights(data) {
function doHighlight() {
_showHighlights(data);
}
if (data.delay) {
$timeout(doHighlight, data.delay);
} else {
doHighlight();
}
}
function _showHighlights(data) {
var less;
/*
API to topoForce
clearLinkTrafficStyle()
removeLinkLabels()
findLinkById( id )
findNodeById( id )
updateLinks()
updateNodes()
supLayers( bool, [less] )
unsupNode( id, [less] )
unsupLink( key, [less] )
*/
api.clearNodeDeco();
api.removeNodeBadges();
api.clearLinkTrafficStyle();
api.removeLinkLabels();
// handle element suppression
if (data.subdue) {
less = data.subdue === 'min';
api.supLayers(true, less);
} else {
api.supLayers(false);
api.supLayers(false, true);
}
data.hosts.forEach(function (host) {
var hdata = api.findNodeById(host.id),
badgeData = host.badge || null;
if (hdata && hdata.el && !hdata.el.empty()) {
hdata.badge = badgeData;
if (!host.subdue) {
api.unsupNode(hdata.id, less);
}
// TODO: further highlighting?
} else {
$log.warn('HILITE: no host element:', host.id);
}
});
data.devices.forEach(function (device) {
var ddata = api.findNodeById(device.id),
badgeData = device.badge || null;
if (ddata && ddata.el && !ddata.el.empty()) {
ddata.badge = badgeData;
if (!device.subdue) {
api.unsupNode(ddata.id, less);
}
// TODO: further highlighting?
} else {
$log.warn('HILITE: no device element:', device.id);
}
});
data.links.forEach(function (link) {
var ldata = api.findLinkById(link.id),
lab = link.label,
units, portcls, magnitude;
if (ldata && ldata.el && !ldata.el.empty()) {
if (!link.subdue) {
api.unsupLink(ldata.key, less);
}
ldata.el.classed(link.css, true);
ldata.label = lab;
// TODO: this needs to be pulled out into traffic overlay
// inject additional styling for port-based traffic
if (fs.endsWith(lab, 'bps')) {
units = lab.substring(lab.length-4);
portcls = 'port-traffic-' + units;
// for GBps
if (units.substring(0,1) === 'G') {
magnitude = fs.parseBitRate(lab);
if (magnitude >= 9) {
portcls += '-choked'
}
}
ldata.el.classed(portcls, true);
}
} else {
$log.warn('HILITE: no link element:', link.id);
}
});
api.updateNodes();
api.updateLinks();
}
// ========================================================================
angular.module('ovTopo')
.factory('TopoOverlayService',
['$log', '$timeout', 'FnService', 'GlyphService', 'WebSocketService',
'NavService', 'TopoPanelService',
function (_$log_, _$timeout_, _fs_, _gs_, _wss_, _ns_, _tps_) {
$log = _$log_;
$timeout = _$timeout_;
fs = _fs_;
gs = _gs_;
wss = _wss_;
ns = _ns_;
tps = _tps_;
return {
register: register,
setApi: setApi,
list: list,
augmentRbset: augmentRbset,
mkGlyphId: mkGlyphId,
tbSelection: tbSelection,
installButtons: installButtons,
addDetailButton: addDetailButton,
resetOnToolbarDestroy: function () { reset = true; },
hooks: {
escape: escapeHook,
emptySelect: emptySelectHook,
singleSelect: singleSelectHook,
multiSelect: multiSelectHook,
mouseOver: mouseOverHook,
mouseOut: mouseOutHook,
modifyLinkData: modifyLinkDataHook
},
showHighlights: showHighlights
}
}]);
}());