Simon Hunt

GUI -- TopoView - Implemented much of the node selection logic. (WIP)

- introduced topoSelect.js.

Change-Id: Ic843c7d8dc2249fe0cb8c33de60dce12c07aea44
......@@ -77,6 +77,7 @@
width: panelWidth,
height: panelHeight,
isVisible: panelIsVisible,
classed: classed,
el: panelEl
};
......@@ -146,6 +147,10 @@
return p.on;
}
function classed(cls, bool) {
return p.el.classed(cls, bool);
}
function panelEl() {
return p.el;
}
......
......@@ -34,6 +34,7 @@
$log = _$log_;
fs = _fs_;
// TODO: change 'force' ref to be 'force.alpha' ref.
function createDragBehavior(force, selectCb, atDragEnd,
dragEnabled, clickEnabled) {
var draggedThreshold = d3.scale.linear()
......
......@@ -82,9 +82,10 @@
<script src="view/topo/topo.js"></script>
<script src="view/topo/topoEvent.js"></script>
<script src="view/topo/topoForce.js"></script>
<script src="view/topo/topoInst.js"></script>
<script src="view/topo/topoModel.js"></script>
<script src="view/topo/topoPanel.js"></script>
<script src="view/topo/topoInst.js"></script>
<script src="view/topo/topoSelect.js"></script>
<script src="view/device/device.js"></script>
<!-- TODO: inject javascript refs server-side -->
......
......@@ -72,71 +72,121 @@
#topo-p-summary {
/* Base css from panel.css */
}
/* --- Topo Detail Panel --- */
#topo-p-detail {
/* Base css from panel.css */
top: 320px;
}
#topo-p-summary svg {
/* --- general topo-panel styling --- */
.topo-p svg {
display: inline-block;
width: 42px;
height: 42px;
}
#topo-p-summary h2 {
.light .topo-p svg .glyph {
fill: #222;
}
.dark .topo-p svg .glyph.overlay {
fill: #222;
}
.dark .topo-p svg .glyph {
fill: #ddd;
}
.light .topo-p svg .glyph.overlay {
fill: #fff;
}
.topo-p h2 {
position: absolute;
margin: 0 4px;
top: 20px;
left: 50px;
}
.light #topo-p-summary h2 {
.light .topo-p h2 {
color: black;
}
.dark #topo-p-summary h2 {
.dark .topo-p h2 {
color: #ddd;
}
#topo-p-summary h3 {
.topo-p h3 {
margin: 0 4px;
top: 20px;
left: 50px;
}
.light #topo-p-summary h3 {
.light .topo-p h3 {
color: black;
}
.dark #topo-p-summary h3 {
.dark .topo-p h3 {
color: #ddd;
}
#topo-p-summary p, table {
.topo-p p, table {
margin: 4px 4px;
}
#topo-p-summary td.label {
.topo-p td.label {
font-style: italic;
padding-right: 12px;
/* works for both light and dark themes ... */
color: #777;
}
#topo-p-summary td.value {
.topo-p td.value {
}
#topo-p-summary hr {
.topo-p hr {
height: 1px;
border: 0;
}
.light #topo-p-summary hr {
.light .topo-p hr {
background-color: #ccc;
color: #ccc;
}
.dark #topo-p-summary hr {
.dark .topo-p hr {
background-color: #888;
color: #888;
}
/* --- Topo Detail Panel --- */
.topo-p .actionBtn {
margin: 6px 12px;
padding: 2px 6px;
font-size: 9pt;
cursor: pointer;
width: 200px;
text-align: center;
border-radius: 4px;
}
.light .topo-p .actionBtn {
border: 2px solid #ddd;
color: #eee;
background: #888;
}
.dark .topo-p .actionBtn {
border: 2px solid #222;
color: #888;
background: #444;
}
.light .topo-p .actionBtn:hover {
color: #eee;
background: #444;
}
.dark .topo-p .actionBtn:hover {
color: #eee;
background: #666;
}
/* TODO: add CSS rules */
/* --- Topo Instance Panel --- */
......
......@@ -66,7 +66,7 @@
//E: [equalizeMasters, 'Equalize mastership roles'],
//esc: handleEscape,
esc: handleEscape,
_helpFormat: [
['O', 'I', 'D', '-', 'H', 'M', 'B', 'P' ],
......@@ -85,12 +85,29 @@
];
}
// --- Keystroke functions -------------------------------------------
function toggleInstances() {
tis.toggle();
tfs.updateDeviceColors();
}
function resetZoom() {
zoomer.reset();
}
function handleEscape() {
$log.debug("TODO: handle-ESCAPE...");
// if showingAffinity: cancelAffinity
// else if showingDetails: deselectAll
// else if oiBox visible: hide oiBox
// else if summary panel visible: cancel Summary
// else: hoverMode = hoverModeNone
}
// --- Glyphs, Icons, and the like -----------------------------------
......@@ -124,10 +141,6 @@
});
}
function resetZoom() {
zoomer.reset();
}
// callback invoked when the SVG view has been resized..
function svgResized(s) {
......
......@@ -27,7 +27,7 @@
'use strict';
// injected refs
var $log, wss, wes, tps, tis, tfs;
var $log, wss, wes, tps, tis, tfs, tss;
// internal state
var wsock, evApis;
......@@ -37,9 +37,13 @@
function bindApis() {
evApis = {
showSummary: tps,
showDetails: tss,
addInstance: tis,
updateInstance: tis,
removeInstance: tis,
addDevice: tfs,
updateDevice: tfs,
removeDevice: tfs,
......@@ -100,14 +104,16 @@
.factory('TopoEventService',
['$log', '$location', 'WebSocketService', 'WsEventService',
'TopoPanelService', 'TopoInstService', 'TopoForceService',
'TopoSelectService',
function (_$log_, $loc, _wss_, _wes_, _tps_, _tis_, _tfs_) {
function (_$log_, $loc, _wss_, _wes_, _tps_, _tis_, _tfs_, _tss_) {
$log = _$log_;
wss = _wss_;
wes = _wes_;
tps = _tps_;
tis = _tis_;
tfs = _tfs_;
tss = _tss_;
bindApis();
......
......@@ -23,7 +23,7 @@
'use strict';
// injected refs
var $log, fs, sus, is, ts, flash, tis, tms, icfg, uplink;
var $log, fs, sus, is, ts, flash, tis, tms, tss, icfg, uplink;
// configuration
var labelConfig = {
......@@ -77,10 +77,7 @@
showOffline = true, // whether offline devices are displayed
oblique = false, // whether we are in the oblique view
nodeLock = false, // whether nodes can be dragged or not (locked)
dim, // the dimensions of the force layout [w,h]
hovered, // the node over which the mouse is hovering
selections = {}, // what is currently selected
selectOrder = []; // the order in which we made selections
dim; // the dimensions of the force layout [w,h]
// SVG elements;
var linkG, linkLabelG, nodeG;
......@@ -311,8 +308,6 @@
.attr('stroke', linkConfig[th].baseColor);
}
function removeLinkElement(d) {
var idx = fs.find(d.key, network.links, 'key'),
removed;
......@@ -418,34 +413,10 @@
});
}
function requestTrafficForMode() {
$log.debug('TODO: requestTrafficForMode()...');
}
// ==========================
// === Devices and hosts - D3 rendering
function nodeMouseOver(m) {
if (!m.dragStarted) {
$log.debug("MouseOver()...", m);
if (hovered != m) {
hovered = m;
requestTrafficForMode();
}
}
}
function nodeMouseOut(m) {
if (!m.dragStarted) {
if (hovered) {
hovered = null;
requestTrafficForMode();
}
$log.debug("MouseOut()...", m);
}
}
// Returns the newly computed bounding box of the rectangle
function adjustRectToFitText(n) {
......@@ -568,10 +539,11 @@
}
function unpin() {
if (hovered) {
sendUpdateMeta(hovered, true);
hovered.fixed = false;
hovered.el.classed('fixed', false);
var hov = tss.hovered();
if (hov) {
sendUpdateMeta(hov, true);
hov.fixed = false;
hov.el.classed('fixed', false);
fResume();
}
}
......@@ -668,8 +640,8 @@
opacity: 0
})
.call(drag)
.on('mouseover', nodeMouseOver)
.on('mouseout', nodeMouseOut)
.on('mouseover', tss.nodeMouseOver)
.on('mouseout', tss.nodeMouseOut)
.transition()
.attr('opacity', 1);
......@@ -998,72 +970,6 @@
}
function updateDetailPanel() {
// TODO update detail panel
$log.debug("TODO: updateDetailPanel() ...");
}
// ==========================
// === SELECTION / DESELECTION
function selectObject(obj) {
var el = this,
ev = d3.event.sourceEvent,
n;
if (zoomingOrPanning(ev)) {
return;
}
if (el) {
n = d3.select(el);
} else {
node.each(function (d) {
if (d == obj) {
n = d3.select(el = this);
}
});
}
if (!n) return;
if (ev.shiftKey && n.classed('selected')) {
deselectObject(obj.id);
updateDetailPanel();
return;
}
if (!ev.shiftKey) {
deselectAll();
}
selections[obj.id] = { obj: obj, el: el };
selectOrder.push(obj.id);
n.classed('selected', true);
updateDeviceColors(obj);
updateDetailPanel();
}
function deselectObject(id) {
var obj = selections[id];
if (obj) {
d3.select(obj.el).classed('selected', false);
delete selections[id];
fs.removeFromArray(id, selectOrder);
updateDeviceColors(obj.obj);
}
}
function deselectAll() {
// deselect all nodes in the network...
node.classed('selected', false);
selections = {};
selectOrder = [];
updateDeviceColors();
updateDetailPanel();
}
// ==========================
// === MOUSE GESTURE HANDLERS
......@@ -1103,12 +1009,22 @@
};
}
function mkSelectApi(uplink) {
return {
node: function () { return node; },
zoomingOrPanning: zoomingOrPanning,
updateDeviceColors: updateDeviceColors,
sendEvent: uplink.sendEvent
};
}
angular.module('ovTopo')
.factory('TopoForceService',
['$log', 'FnService', 'SvgUtilService', 'IconService', 'ThemeService',
'FlashService', 'TopoInstService', 'TopoModelService',
'TopoSelectService',
function (_$log_, _fs_, _sus_, _is_, _ts_, _flash_, _tis_, _tms_) {
function (_$log_, _fs_, _sus_, _is_, _ts_, _flash_, _tis_, _tms_, _tss_) {
$log = _$log_;
fs = _fs_;
sus = _sus_;
......@@ -1117,6 +1033,7 @@
flash = _flash_;
tis = _tis_;
tms = _tms_;
tss = _tss_;
icfg = is.iconConfig();
......@@ -1131,6 +1048,7 @@
$log.debug('initForce().. dim = ' + dim);
tms.initModel(mkModelApi(uplink), dim);
tss.initSelect(mkSelectApi(uplink));
settings = angular.extend({}, defaultSettings, opts);
......@@ -1154,7 +1072,7 @@
.on('tick', tick);
drag = sus.createDragBehavior(force,
selectObject, atDragEnd, dragEnabled, clickEnabled);
tss.selectObject, atDragEnd, dragEnabled, clickEnabled);
}
function newDim(_dim_) {
......
......@@ -24,18 +24,21 @@
'use strict';
// injected refs
var $log, fs, rnd, api;
var $log, fs, rnd;
// api to topoForce
var api;
/*
projection()
network {...}
restyleLinkElement( ldata )
removeLinkElement( ldata )
*/
// shorthand
var lu, rlk, nodes, links;
// api:
// projection: func()
// network {...}
// restyleLinkElement: func(ldata)
// removeLinkElement: func(ldata)
var dim; // dimensions of layout, as [w,h]
var dim; // dimensions of layout [w,h]
// configuration 'constants'
var defaultLinkType = 'direct',
......
......@@ -26,7 +26,8 @@
var $log, ps, gs;
// constants
var idSum = 'topo-p-summary',
var pCls = 'topo-p',
idSum = 'topo-p-summary',
idDet = 'topo-p-detail',
panelOpts = {
width: 260
......@@ -36,60 +37,134 @@
var summaryPanel,
detailPanel;
// ==========================
// *** SHOW SUMMARY ***
function showSummary(data) {
populateSummary(data);
showSummaryPanel();
// === -----------------------------------------------------
// Utility functions
function addSep(tbody) {
tbody.append('tr').append('td').attr('colspan', 2).append('hr');
}
function addProp(tbody, label, value) {
var tr = tbody.append('tr');
function addCell(cls, txt) {
tr.append('td').attr('class', cls).text(txt);
}
addCell('label', label + ' :');
addCell('value', value);
}
function listProps(tbody, data) {
data.propOrder.forEach(function(p) {
if (p === '-') {
addSep(tbody);
} else {
addProp(tbody, p, data.props[p]);
}
});
}
function dpa(x) {
return detailPanel.append(x);
}
function spa(x) {
return summaryPanel.append(x);
}
// === -----------------------------------------------------
// Functions for populating the summary panel
function populateSummary(data) {
summaryPanel.empty();
var svg = summaryPanel.append('svg'),
title = summaryPanel.append('h2'),
table = summaryPanel.append('table'),
var svg = spa('svg'),
title = spa('h2'),
table = spa('table'),
tbody = table.append('tbody');
gs.addGlyph(svg, 'node', 40);
gs.addGlyph(svg, 'bird', 24, true, [8,12]);
title.text(data.id);
listProps(tbody, data);
}
data.propOrder.forEach(function(p) {
if (p === '-') {
addSep(tbody);
} else {
addProp(tbody, p, data.props[p]);
// === -----------------------------------------------------
// Functions for populating the detail panel
function displaySingle(data) {
detailPanel.empty();
var svg = dpa('svg'),
title = dpa('h2'),
table = dpa('table'),
tbody = table.append('tbody');
gs.addGlyph(svg, (data.type || 'unknown'), 40);
title.text(data.id);
listProps(tbody, data);
dpa('hr');
}
function displayMulti(ids) {
detailPanel.empty();
var title = dpa('h3'),
table = dpa('table'),
tbody = table.append('tbody');
title.text('Selected Nodes');
ids.forEach(function (d, i) {
addProp(tbody, i+1, d);
});
dpa('hr');
}
function addSep(tbody) {
tbody.append('tr').append('td').attr('colspan', 2).append('hr');
function addAction(text, cb) {
dpa('div')
.classed('actionBtn', true)
.text(text)
.on('click', cb);
}
function addProp(tbody, label, value) {
var tr = tbody.append('tr');
// === -----------------------------------------------------
// Event Handlers
function addCell(cls, txt) {
tr.append('td').attr('class', cls).text(txt);
}
addCell('label', label + ' :');
addCell('value', value);
function showSummary(data) {
populateSummary(data);
showSummaryPanel();
}
// === -----------------------------------------------------
// === LOGIC For showing/hiding summary and detail panels...
function showSummaryPanel() {
summaryPanel.show();
// TODO: augment, once we have the details pane also
}
function showDetailPanel() {
// TODO: augment with summary-accomodation-logic
detailPanel.show();
}
function hideDetailPanel() {
detailPanel.hide();
}
// ==========================
function initPanels() {
summaryPanel = ps.createPanel(idSum, panelOpts);
detailPanel = ps.createPanel(idDet, panelOpts);
summaryPanel.classed(pCls, true);
detailPanel.classed(pCls, true);
}
function destroyPanels() {
......@@ -112,7 +187,18 @@
return {
initPanels: initPanels,
destroyPanels: destroyPanels,
showSummary: showSummary
showSummary: showSummary,
displaySingle: displaySingle,
displayMulti: displayMulti,
addAction: addAction,
showDetailPanel: showDetailPanel,
hideDetailPanel: hideDetailPanel,
detailVisible: function () { return detailPanel.isVisible(); },
summaryVisible: function () { return summaryPanel.isVisible(); }
};
}]);
}());
......
/*
* Copyright 2015 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 Selection Module.
Defines behavior when selecting nodes.
*/
(function () {
'use strict';
// injected refs
var $log, fs, tps;
// api to topoForce
var api;
/*
node() // get ref to D3 selection of nodes
zoomingOrPanning( ev )
updateDeviceColors( [dev] )
sendEvent( type, {payload} )
*/
// internal state
var hovered, // the node over which the mouse is hovering
selections = {}, // currently selected nodes (by id)
selectOrder = [], // the order in which we made selections
haveDetails = false, // do we have details of one or more nodes?
useDetails = true; // should we show details if we have 'em?
// ==========================
function nSel() {
return selectOrder.length;
}
function getSel(idx) {
return selections[selectOrder[idx]];
}
function allSelectionsClass(cls) {
for (var i=0, n=nSel(); i<n; i++) {
if (getSel(i).obj.class !== cls) {
return false;
}
}
return true;
}
// ==========================
function nodeMouseOver(m) {
if (!m.dragStarted) {
$log.debug("MouseOver()...", m);
if (hovered != m) {
hovered = m;
requestTrafficForMode();
}
}
}
function nodeMouseOut(m) {
if (!m.dragStarted) {
if (hovered) {
hovered = null;
requestTrafficForMode();
}
$log.debug("MouseOut()...", m);
}
}
// ==========================
function selectObject(obj) {
var el = this,
ev = d3.event.sourceEvent,
n;
if (api.zoomingOrPanning(ev)) {
return;
}
if (el) {
n = d3.select(el);
} else {
api.node().each(function (d) {
if (d == obj) {
n = d3.select(el = this);
}
});
}
if (!n) return;
if (ev.shiftKey && n.classed('selected')) {
deselectObject(obj.id);
updateDetail();
return;
}
if (!ev.shiftKey) {
deselectAll();
}
selections[obj.id] = { obj: obj, el: el };
selectOrder.push(obj.id);
n.classed('selected', true);
api.updateDeviceColors(obj);
updateDetail();
debugSel();
}
function deselectObject(id) {
var obj = selections[id];
if (obj) {
d3.select(obj.el).classed('selected', false);
delete selections[id];
fs.removeFromArray(id, selectOrder);
api.updateDeviceColors(obj.obj);
}
debugSel();
}
function deselectAll() {
// deselect all nodes in the network...
api.node().classed('selected', false);
selections = {};
selectOrder = [];
api.updateDeviceColors();
updateDetail();
debugSel();
}
function debugSel() {
$log.debug(' ..... Selected now >> ', selectOrder);
}
// === -----------------------------------------------------
function requestDetails() {
var data = getSel(0).obj;
api.sendEvent('requestDetails', {
id: data.id,
class: data.class
});
}
// === -----------------------------------------------------
function updateDetail() {
var nSel = selectOrder.length;
if (!nSel) {
emptySelect();
} else if (nSel === 1) {
singleSelect();
} else {
multiSelect();
}
}
function emptySelect() {
haveDetails = false;
tps.hideDetailPanel();
cancelTraffic();
}
function singleSelect() {
// NOTE: detail is shown from 'showDetails' event callback
requestDetails();
cancelTraffic();
requestTrafficForMode();
}
function multiSelect() {
haveDetails = true;
// display the selected nodes in the detail panel
tps.displayMulti(selectOrder);
// always add the 'show traffic' action
tps.addAction('Show Related Traffic', showRelatedIntentsAction);
// add other actions, based on what is selected...
if (nSel() === 2 && allSelectionsClass('host')) {
tps.addAction('Create Host-to-Host Flow', addHostIntentAction);
} else if (nSel() >= 2 && allSelectionsClass('host')) {
tps.addAction('Create Multi-Source Flow', addMultiSourceIntentAction);
}
cancelTraffic();
requestTrafficForMode();
}
// === -----------------------------------------------------
// Event Handlers
function showDetails(data) {
haveDetails = true;
// display the data for the single selected node
tps.displaySingle(data);
// always add the 'show traffic' action
tps.addAction('Show Related Traffic', showRelatedIntentsAction);
// add other actions, based on what is selected...
if (data.type === 'switch') {
tps.addAction('Show Device Flows', showDeviceLinkFlowsAction);
}
// only show the details panel if the user hasn't "hidden" it
if (useDetails) {
tps.showDetailPanel();
}
}
// === -----------------------------------------------------
// TODO: migrate these to topoTraffic.js
function cancelTraffic() {
$log.debug('TODO: cancelTraffic');
}
function requestTrafficForMode() {
$log.debug('TODO: requestTrafficForMode');
}
function showRelatedIntentsAction () {
$log.debug('TODO: showRelatedIntentsAction');
}
function addHostIntentAction () {
$log.debug('TODO: addHostIntentAction');
}
function addMultiSourceIntentAction () {
$log.debug('TODO: addMultiSourceIntentAction');
}
function showDeviceLinkFlowsAction () {
$log.debug('TODO: showDeviceLinkFlowsAction');
}
// === -----------------------------------------------------
// === MODULE DEFINITION ===
angular.module('ovTopo')
.factory('TopoSelectService',
['$log', 'FnService', 'TopoPanelService',
function (_$log_, _fs_, _tps_) {
$log = _$log_;
fs = _fs_;
tps = _tps_;
function initSelect(_api_) {
api = _api_;
}
function destroySelect() { }
return {
initSelect: initSelect,
destroySelect: destroySelect,
showDetails: showDetails,
nodeMouseOver: nodeMouseOver,
nodeMouseOut: nodeMouseOut,
selectObject: selectObject,
deselectObject: deselectObject,
deselectAll: deselectAll,
hovered: function () { return hovered; }
};
}]);
}());
......@@ -88,7 +88,7 @@ describe('factory: fw/layer/panel.js', function () {
var p = ps.createPanel('foo');
expect(fs.areFunctions(p, [
'show', 'hide', 'toggle', 'empty', 'append',
'width', 'height', 'isVisible', 'el'
'width', 'height', 'isVisible', 'classed', 'el'
])).toBeTruthy();
});
......
......@@ -34,7 +34,16 @@ describe('factory: view/topo/topoPanel.js', function() {
it('should define api functions', function () {
expect(fs.areFunctions(tps, [
'initPanels', 'destroyPanels', 'showSummary'
'initPanels',
'destroyPanels',
'showSummary',
'displaySingle',
'displayMulti',
'addAction',
'showDetailPanel',
'hideDetailPanel',
'detailVisible',
'summaryVisible'
])).toBeTruthy();
});
......
/*
* Copyright 2015 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 -- Topo View -- Topo Selection Service - Unit Tests
*/
describe('factory: view/topo/topoSelect.js', function() {
var $log, fs, tss;
beforeEach(module('ovTopo', 'onosUtil', 'onosLayer'));
beforeEach(inject(function (_$log_, FnService, TopoSelectService) {
$log = _$log_;
fs = FnService;
tss = TopoSelectService;
}));
it('should define TopoSelectService', function () {
expect(tss).toBeDefined();
});
it('should define api functions', function () {
expect(fs.areFunctions(tss, [
'initSelect', 'destroySelect', 'showDetails',
'nodeMouseOver', 'nodeMouseOut', 'selectObject', 'deselectObject',
'deselectAll', 'hovered'
])).toBeTruthy();
});
// TODO: more tests...
});