Simon Hunt

GUI -- Added onos.ui.addFloatingPanel() function.

- re-instated detail pane in topo2.js; triggered of non-zero selection state.
- single-select now requests details and displays them in detail pane.
- multi-select WIP.

Change-Id: I300a3dfd4d35abc82f832a172854c6aff50d8cd6
/*
* Copyright 2014 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 -- Floating Panels -- CSS file
@author Simon Hunt
*/
.fpanel {
position: absolute;
z-index: 100;
display: block;
top: 10%;
width: 280px;
right: -300px;
opacity: 0;
background-color: rgba(255,255,255,0.8);
padding: 10px;
color: black;
font-size: 10pt;
box-shadow: 2px 2px 16px #777;
}
/* TODO: light/dark themes */
.light .fpanel {
}
.dark .fpanel {
}
......@@ -40,6 +40,7 @@
<link rel="stylesheet" href="base.css">
<link rel="stylesheet" href="onos2.css">
<link rel="stylesheet" href="mast2.css">
<link rel="stylesheet" href="floatPanel.css">
<!-- This is where contributed stylesheets get INJECTED -->
<!-- TODO: replace with template marker and inject refs server-side -->
......@@ -62,8 +63,9 @@
<div id="view">
<!-- NOTE: views injected here by onos.js -->
</div>
<div id="overlays">
<!-- NOTE: overlays injected here, as needed -->
<div id="floatPanels">
<!-- NOTE: floating panels injected here, as needed -->
<!-- see onos.ui.addFloatingPanel -->
</div>
<div id="alerts">
<!-- NOTE: alert content injected here, as needed -->
......
{
"event": "showDetails",
"sid": 9,
"payload": {
"id": "CA:4B:EE:A4:B0:33/-1",
"type": "host",
"propOrder": [
"MAC",
"IP",
"-",
"Latitude",
"Longitude"
],
"props": {
"MAC": "CA:4B:EE:A4:B0:33",
"IP": "[10.0.0.1]",
"-": "",
"Latitude": null,
"Longitude": null
}
}
}
{
"event": "showDetails",
"sid": 37,
"payload": {
"id": "of:000000000000000a",
"type": "switch",
"propOrder": [
"Name",
"Vendor",
"H/W Version",
"S/W Version",
"Serial Number",
"-",
"Latitude",
"Longitude",
"Ports",
"-",
"Master"
],
"props": {
"Name": null,
"Vendor": "Nicira, Inc.",
"H/W Version": "Open vSwitch",
"S/W Version": "2.0.1",
"Serial Number": "None",
"-": "",
"Latitude": null,
"Longitude": null,
"Ports": "5",
"Master":"local"
}
}
}
{
"event": "requestDetails",
"sid": 15,
"payload": {
"id": "of:0000000000000003",
"class": "device"
}
}
{
"event": "requestDetails",
"sid": 9,
"payload": {
"id": "CA:4B:EE:A4:B0:33/-1",
"class": "host"
}
}
......@@ -50,6 +50,7 @@
// internal state
var views = {},
fpanels = {},
current = {
view: null,
ctx: '',
......@@ -57,7 +58,7 @@
theme: settings.theme
},
built = false,
errorCount = 0,
buildErrors = [],
keyHandler = {
globalKeys: {},
maskedKeys: {},
......@@ -70,7 +71,11 @@
};
// DOM elements etc.
var $view,
// TODO: verify existence of following elements...
var $view = d3.select('#view'),
$floatPanels = d3.select('#floatPanels'),
$alerts = d3.select('#alerts'),
// note, following elements added programmatically...
$mastRadio;
......@@ -241,10 +246,22 @@
setView(view, hash, t);
}
function buildError(msg) {
buildErrors.push(msg);
}
function reportBuildErrors() {
traceFn('reportBuildErrors');
// TODO: validate registered views / nav-item linkage etc.
console.log('(no build errors)');
var nerr = buildErrors.length,
errmsg;
if (!nerr) {
console.log('(no build errors)');
} else {
errmsg = 'Build errors: ' + nerr + ' found...\n\n' +
buildErrors.join('\n');
doAlert(errmsg);
console.error(errmsg);
}
}
// returns the reference if it is a function, null otherwise
......@@ -449,22 +466,20 @@
}
function createAlerts() {
var al = d3.select('#alerts')
.style('display', 'block');
al.append('span')
$alerts.style('display', 'block');
$alerts.append('span')
.attr('class', 'close')
.text('X')
.on('click', closeAlerts);
al.append('pre');
al.append('p').attr('class', 'footnote')
$alerts.append('pre');
$alerts.append('p').attr('class', 'footnote')
.text('Press ESCAPE to close');
alerts.open = true;
alerts.count = 0;
}
function closeAlerts() {
d3.select('#alerts')
.style('display', 'none')
$alerts.style('display', 'none')
.html('');
alerts.open = false;
}
......@@ -474,7 +489,7 @@
oldContent;
if (alerts.count) {
oldContent = d3.select('#alerts pre').html();
oldContent = $alerts.select('pre').html();
}
lines = msg.split('\n');
......@@ -485,7 +500,7 @@
lines += '\n----\n' + oldContent;
}
d3.select('#alerts pre').html(lines);
$alerts.select('pre').html(lines);
alerts.count++;
}
......@@ -691,6 +706,53 @@
libApi[libName] = api;
},
// TODO: implement floating panel as a class
// TODO: parameterize position (currently hard-coded to TopRight)
/*
* Creates div in floating panels block, with the given id.
* Returns panel token used to interact with the panel
*/
addFloatingPanel: function (id, position) {
var pos = position || 'TR',
el,
fp;
if (fpanels[id]) {
buildError('Float panel with id "' + id + '" already exists.');
return null;
}
el = $floatPanels.append('div')
.attr('id', id)
.attr('class', 'fpanel');
fp = {
id: id,
el: el,
pos: pos,
show: function () {
console.log('show pane: ' + id);
el.transition().duration(750)
.style('right', '20px')
.style('opacity', 1);
},
hide: function () {
console.log('hide pane: ' + id);
el.transition().duration(750)
.style('right', '-320px')
.style('opacity', 0);
},
empty: function () {
return el.html('');
},
append: function (what) {
return el.append(what);
}
};
fpanels[id] = fp;
return fp;
},
// TODO: it remains to be seen whether we keep this style of docs
/** @api ui addView( vid, nid, cb )
* Adds a view to the UI.
......@@ -782,7 +844,6 @@
}
built = true;
$view = d3.select('#view');
$mastRadio = d3.select('#mastRadio');
$(window).on('hashchange', hash);
......
......@@ -96,3 +96,45 @@
fill: white;
stroke: red;
}
/* detail topo-detail pane */
#topo-detail {
/* gets base CSS from .fpanel in floatPanel.css */
}
#topo-detail h2 {
margin: 8px 4px;
color: black;
vertical-align: middle;
}
#topo-detail h2 img {
height: 32px;
padding-right: 8px;
vertical-align: middle;
}
#topo-detail p, table {
margin: 4px 4px;
}
#topo-detail td.label {
font-style: italic;
color: #777;
padding-right: 12px;
}
#topo-detail td.value {
}
#topo-detail hr {
height: 1px;
color: #ccc;
background-color: #ccc;
border: 0;
}
......
......@@ -152,7 +152,7 @@
webSock,
deviceLabelIndex = 0,
hostLabelIndex = 0,
detailPane,
selectOrder = [],
selections = {},
......@@ -192,6 +192,10 @@
function testMe(view) {
view.alert('test');
detailPane.show();
setTimeout(function () {
detailPane.hide();
}, 3000);
}
function abortIfLive() {
......@@ -285,14 +289,6 @@
view.alert('unpin() callback')
}
function requestPath(view) {
var payload = {
one: selections[selectOrder[0]].obj.id,
two: selections[selectOrder[1]].obj.id
}
sendMessage('requestPath', payload);
}
// ==============================
// Radio Button Callbacks
......@@ -353,6 +349,7 @@
removeDevice: stillToImplement,
removeLink: removeLink,
removeHost: removeHost,
showDetails: showDetails,
showPath: showPath
};
......@@ -463,6 +460,12 @@
}
}
function showDetails(data) {
fnTrace('showDetails', data.payload.id);
populateDetails(data.payload);
detailPane.show();
}
function showPath(data) {
fnTrace('showPath', data.payload.id);
var links = data.payload.links,
......@@ -500,6 +503,32 @@
}
// ==============================
// Out-going messages...
function getSel(idx) {
return selections[selectOrder[idx]];
}
// for now, just a host-to-host intent, (and implicit start-monitoring)
function requestPath() {
var payload = {
one: getSel(0).obj.id,
two: getSel(1).obj.id
};
sendMessage('requestPath', payload);
}
// request details for the selected element
function requestDetails() {
var data = getSel(0).obj,
payload = {
id: data.id,
class: data.class
};
sendMessage('requestDetails', payload);
}
// ==============================
// force layout modification functions
function translate(x, y) {
......@@ -1015,6 +1044,8 @@
var sid = 0;
// TODO: use cache of pending messages (key = sid) to reconcile responses
function sendMessage(evType, payload) {
var toSend = {
event: evType,
......@@ -1033,7 +1064,6 @@
wsTrace('rx', msg);
}
function wsTrace(rxtx, msg) {
console.log('[' + rxtx + '] ' + msg);
// TODO: integrate with trace view
//if (trace) {
......@@ -1062,7 +1092,7 @@
if (meta && n.classed('selected')) {
deselectObject(obj.id);
//flyinPane(null);
updateDetailPane();
return;
}
......@@ -1074,17 +1104,16 @@
selectOrder.push(obj.id);
n.classed('selected', true);
//flyinPane(obj);
updateDetailPane();
}
function deselectObject(id) {
var obj = selections[id];
if (obj) {
d3.select(obj.el).classed('selected', false);
selections[id] = null;
// TODO: use splice to remove element
delete selections[id];
}
//flyinPane(null);
updateDetailPane();
}
function deselectAll() {
......@@ -1092,10 +1121,10 @@
node.classed('selected', false);
selections = {};
selectOrder = [];
//flyinPane(null);
updateDetailPane();
}
// TODO: this click handler does not get unloaded when the view does
// FIXME: this click handler does not get unloaded when the view does
$('#view').on('click', function(e) {
if (!$(e.target).closest('.node').length) {
if (!e.metaKey) {
......@@ -1104,6 +1133,66 @@
}
});
// update the state of the detail pane, based on current selections
function updateDetailPane() {
var nSel = selectOrder.length;
if (!nSel) {
detailPane.hide();
} else if (nSel === 1) {
singleSelect();
} else {
multiSelect();
}
}
function singleSelect() {
requestDetails();
// NOTE: detail pane will be shown from showDetails event.
}
function multiSelect() {
// TODO: use detail pane for multi-select view.
//detailPane.show();
}
function populateDetails(data) {
detailPane.empty();
var title = detailPane.append("h2"),
table = detailPane.append("table"),
tbody = table.append("tbody");
$('<img src="img/' + data.type + '.png">').appendTo(title);
$('<span>').attr('class', 'icon').text(data.id).appendTo(title);
data.propOrder.forEach(function(p) {
if (p === '-') {
addSep(tbody);
} else {
addProp(tbody, p, data.props[p]);
}
});
function addSep(tbody) {
var tr = tbody.append('tr');
$('<hr>').appendTo(tr.append('td').attr('colspan', 2));
}
function addProp(tbody, label, value) {
var tr = tbody.append('tr');
tr.append('td')
.attr('class', 'label')
.text(label + ' :');
tr.append('td')
.attr('class', 'value')
.text(value);
}
}
// ==============================
// Test harness code
function prepareScenario(view, ctx, dbg) {
var sc = scenario,
......@@ -1272,4 +1361,6 @@
resize: resize
});
detailPane = onos.ui.addFloatingPanel('topo-detail');
}(ONOS));
......