Merge "Adding synchronous behaviour to the calendar REST API to wait until intent is in parked state. Needs testing."
diff --git a/core/net/src/main/java/org/onlab/onos/net/intent/impl/ConnectivityIntentCompiler.java b/core/net/src/main/java/org/onlab/onos/net/intent/impl/ConnectivityIntentCompiler.java
index d66e849..405366d 100644
--- a/core/net/src/main/java/org/onlab/onos/net/intent/impl/ConnectivityIntentCompiler.java
+++ b/core/net/src/main/java/org/onlab/onos/net/intent/impl/ConnectivityIntentCompiler.java
@@ -15,6 +15,8 @@
*/
package org.onlab.onos.net.intent.impl;
+import com.google.common.base.Predicate;
+import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Reference;
@@ -94,11 +96,19 @@
protected Path getPath(ConnectivityIntent intent,
ElementId one, ElementId two) {
Set<Path> paths = pathService.getPaths(one, two, weight(intent.constraints()));
- if (paths.isEmpty()) {
- throw new PathNotFoundException("No packet path from " + one + " to " + two);
+ final List<Constraint> constraints = intent.constraints();
+ ImmutableList<Path> filtered = FluentIterable.from(paths)
+ .filter(new Predicate<Path>() {
+ @Override
+ public boolean apply(Path path) {
+ return checkPath(path, constraints);
+ }
+ }).toList();
+ if (filtered.isEmpty()) {
+ throw new PathNotFoundException("No packet path form " + one + " to " + two);
}
// TODO: let's be more intelligent about this eventually
- return paths.iterator().next();
+ return filtered.iterator().next();
}
/**
diff --git a/web/gui/src/main/webapp/floatPanel.css b/web/gui/src/main/webapp/floatPanel.css
new file mode 100644
index 0000000..1c5a815
--- /dev/null
+++ b/web/gui/src/main/webapp/floatPanel.css
@@ -0,0 +1,45 @@
+/*
+ * 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 {
+
+}
diff --git a/web/gui/src/main/webapp/index2.html b/web/gui/src/main/webapp/index2.html
index 03272e9..ca5b6a8 100644
--- a/web/gui/src/main/webapp/index2.html
+++ b/web/gui/src/main/webapp/index2.html
@@ -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 -->
diff --git a/web/gui/src/main/webapp/json/ev/_capture/rx/showDetails_ex1_host.json b/web/gui/src/main/webapp/json/ev/_capture/rx/showDetails_ex1_host.json
new file mode 100644
index 0000000..19d9959
--- /dev/null
+++ b/web/gui/src/main/webapp/json/ev/_capture/rx/showDetails_ex1_host.json
@@ -0,0 +1,22 @@
+{
+ "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
+ }
+ }
+}
diff --git a/web/gui/src/main/webapp/json/ev/_capture/rx/showDetails_ex2_device.json b/web/gui/src/main/webapp/json/ev/_capture/rx/showDetails_ex2_device.json
new file mode 100644
index 0000000..8ac1f4f
--- /dev/null
+++ b/web/gui/src/main/webapp/json/ev/_capture/rx/showDetails_ex2_device.json
@@ -0,0 +1,33 @@
+{
+ "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"
+ }
+ }
+}
diff --git a/web/gui/src/main/webapp/json/ev/_capture/tx/requestDetails_ex1.json b/web/gui/src/main/webapp/json/ev/_capture/tx/requestDetails_ex1.json
new file mode 100644
index 0000000..6c88605
--- /dev/null
+++ b/web/gui/src/main/webapp/json/ev/_capture/tx/requestDetails_ex1.json
@@ -0,0 +1,9 @@
+{
+ "event": "requestDetails",
+ "sid": 15,
+ "payload": {
+ "id": "of:0000000000000003",
+ "class": "device"
+ }
+}
+
diff --git a/web/gui/src/main/webapp/json/ev/_capture/tx/requestDetails_ex2.json b/web/gui/src/main/webapp/json/ev/_capture/tx/requestDetails_ex2.json
new file mode 100644
index 0000000..2cc1bfa
--- /dev/null
+++ b/web/gui/src/main/webapp/json/ev/_capture/tx/requestDetails_ex2.json
@@ -0,0 +1,8 @@
+{
+ "event": "requestDetails",
+ "sid": 9,
+ "payload": {
+ "id": "CA:4B:EE:A4:B0:33/-1",
+ "class": "host"
+ }
+}
diff --git a/web/gui/src/main/webapp/onos2.js b/web/gui/src/main/webapp/onos2.js
index f38b35f..0644c32 100644
--- a/web/gui/src/main/webapp/onos2.js
+++ b/web/gui/src/main/webapp/onos2.js
@@ -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);
diff --git a/web/gui/src/main/webapp/topo2.css b/web/gui/src/main/webapp/topo2.css
index 6c0c313..aeaad2d 100644
--- a/web/gui/src/main/webapp/topo2.css
+++ b/web/gui/src/main/webapp/topo2.css
@@ -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;
+}
+
diff --git a/web/gui/src/main/webapp/topo2.js b/web/gui/src/main/webapp/topo2.js
index 24053d8..a23f48d 100644
--- a/web/gui/src/main/webapp/topo2.js
+++ b/web/gui/src/main/webapp/topo2.js
@@ -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));