aboutsummaryrefslogtreecommitdiffstats
path: root/framework/src/onos/web/gui/src/main/webapp/app
diff options
context:
space:
mode:
Diffstat (limited to 'framework/src/onos/web/gui/src/main/webapp/app')
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/README.txt7
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/common.css31
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/directives.js72
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/fw/README.txt44
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/fw/layer/flash.css49
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/fw/layer/flash.js165
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/fw/layer/layer.js25
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/fw/layer/panel.css50
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/fw/layer/panel.js214
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/fw/layer/quickhelp.css64
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/fw/layer/quickhelp.js387
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/fw/layer/veil.css50
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/fw/layer/veil.js99
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/fw/mast/mast.css102
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/fw/mast/mast.html6
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/fw/mast/mast.js56
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/fw/nav/nav.css90
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/fw/nav/nav.js103
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/fw/remote/remote.js25
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/fw/remote/rest.js72
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/fw/remote/urlfn.js63
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/fw/remote/websocket.js329
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/fw/remote/wsevent.js49
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/fw/remote/wsock.js42
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/fw/svg/geodata.js186
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/fw/svg/glyph.css34
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/fw/svg/glyph.js646
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/fw/svg/icon.css92
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/fw/svg/icon.js265
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/fw/svg/map.js129
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/fw/svg/svg.js25
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/fw/svg/svgUtil.js311
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/fw/svg/zoom.js132
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/fw/util/fn.js292
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/fw/util/keys.js215
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/fw/util/prefs.js128
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/fw/util/random.js51
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/fw/util/theme.js121
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/fw/util/util.js25
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/fw/widget/button.css120
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/fw/widget/button.js263
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/fw/widget/table.css215
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/fw/widget/table.js272
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/fw/widget/tableBuilder.js165
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/fw/widget/toolbar.css77
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/fw/widget/toolbar.js268
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/fw/widget/tooltip.css44
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/fw/widget/tooltip.js146
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/fw/widget/widget.js25
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/onos.css60
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/app/app.css32
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/app/app.html80
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/app/app.js148
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/cluster/cluster.css27
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/cluster/cluster.html68
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/cluster/cluster.js36
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/device/device.css144
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/device/device.html82
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/device/device.js319
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/flow/flow.css86
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/flow/flow.html84
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/flow/flow.js62
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/group/group.css77
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/group/group.html90
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/group/group.js70
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/host/host.css27
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/host/host.html54
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/host/host.js36
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/intent/intent.css71
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/intent/intent.html72
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/intent/intent.js36
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/link/link.css27
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/link/link.html70
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/link/link.js44
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/port/port.css49
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/port/port.html90
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/port/port.js62
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/settings/settings.css27
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/settings/settings.html52
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/settings/settings.js36
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/topo/README.txt3
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/topo/topo.css729
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/topo/topo.html7
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/topo/topo.js529
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoD3.js576
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoEvent.js131
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoFilter.js149
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoForce.js1134
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoInst.js373
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoLink.js338
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoModel.js439
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoOblique.js257
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoOverlay.js418
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoPanel.js539
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoSelect.js286
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoSprite.js262
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoToolbar.js284
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoTraffic.js220
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoTrafficNew.js159
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/tunnel/tunnel.css27
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/tunnel/tunnel.html72
-rw-r--r--framework/src/onos/web/gui/src/main/webapp/app/view/tunnel/tunnel.js44
102 files changed, 15635 insertions, 0 deletions
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/README.txt b/framework/src/onos/web/gui/src/main/webapp/app/README.txt
new file mode 100644
index 00000000..d5f94d87
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/README.txt
@@ -0,0 +1,7 @@
+# Main Application
+
+index.html is the main launch point for the application.
+
+fw/ contains framework related code
+
+view/ contains view related code \ No newline at end of file
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/common.css b/framework/src/onos/web/gui/src/main/webapp/app/common.css
new file mode 100644
index 00000000..3a181afc
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/common.css
@@ -0,0 +1,31 @@
+/*
+ * 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 -- common -- CSS file
+ */
+
+#view h2 {
+ margin: 0;
+ /* separated out so that other padding doesn't get clobbered */
+ padding-top: 20px;
+ padding-bottom: 20px;
+}
+
+.clickable {
+ cursor: pointer;
+}
+
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/directives.js b/framework/src/onos/web/gui/src/main/webapp/app/directives.js
new file mode 100644
index 00000000..0b4f4957
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/directives.js
@@ -0,0 +1,72 @@
+/*
+ * 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 -- General Purpose Angular directives defined here.
+ */
+
+(function () {
+ 'use strict';
+
+ angular.module('onosApp')
+
+ // Create a resize directive, that we can apply to elements
+ // so that they can respond to window resize events.
+ .directive('resize', ['$window', 'FnService', function ($window, fs) {
+ return {
+ scope: {
+ offsetHeight: '@',
+ offsetWidth: '@',
+ notifier: '&'
+ },
+ link: function (scope, element) {
+ var elem = d3.select(element[0]);
+ scope.$watchCollection(function () {
+ return {
+ h: $window.innerHeight,
+ w: $window.innerWidth
+ };
+ }, function () {
+ var offH = scope.offsetHeight || 0,
+ offW = scope.offsetWidth || 0,
+ wsz = fs.windowSize(offH, offW);
+
+ elem.style({
+ height: wsz.height + 'px',
+ width: wsz.width + 'px'
+ });
+
+ if (fs.isF(scope.notifier)) {
+ scope.notifier();
+ }
+ });
+
+ angular.element($window).bind('resize', function () {
+ scope.$apply();
+ });
+ }
+ };
+ }])
+
+ .directive('ngRepeatComplete', [function () {
+ return function (scope) {
+ if (scope.$last) {
+ scope.$emit('ngRepeatComplete');
+ scope.$broadcast('ngRepeatComplete');
+ }
+ };
+ }]);
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/fw/README.txt b/framework/src/onos/web/gui/src/main/webapp/app/fw/README.txt
new file mode 100644
index 00000000..626547b2
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/fw/README.txt
@@ -0,0 +1,44 @@
+# Framework related code
+
+- layer
+ - Flash Service (transient messages)
+ - Panel Service (floating panels)
+ - Quick Help Service (key bindings, mouse gestures)
+ - Veil Service (loss of server connection)
+
+- mast
+ - Masthead Service
+
+- nav
+ - Navigation Service (navigation menu)
+
+- remote
+ - REST Service
+ - URL functin Service
+ - Web Socket Service
+ - Web Socket Event Service
+ - Web Socket encapsulation
+
+ - (Login Service) << planned
+
+- svg
+ - GeoData Service (TopoJSON map functions)
+ - Glyph Service
+ - Icon Service
+ - Map Service
+ - SVG Utilities Service
+ - Zoom Service
+
+- util
+ - General Functions
+ - Key Handler
+ - User Preference Service
+ - Randomization Service
+ - Theme Service
+
+- widget
+ - Button Service
+ - Table Service (table styling directives)
+ - Table Builder Service
+ - Toolbar Service
+ - Tooltip Service
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/fw/layer/flash.css b/framework/src/onos/web/gui/src/main/webapp/app/fw/layer/flash.css
new file mode 100644
index 00000000..02064625
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/fw/layer/flash.css
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2014,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 -- Flash Service -- CSS file
+ */
+
+#flash {
+ z-index: 1400;
+}
+
+#flash svg {
+ position: absolute;
+ bottom: 0;
+ opacity: 0.8;
+}
+
+.light #flash svg g.flashItem rect {
+ fill: #ccc;
+}
+.dark #flash svg g.flashItem rect {
+ fill: #555;
+}
+
+#flash svg g.flashItem text {
+ stroke: none;
+ text-anchor: middle;
+ alignment-baseline: middle;
+ font-size: 16pt;
+}
+.light #flash svg g.flashItem text {
+ fill: #333;
+}
+.dark #flash svg g.flashItem text {
+ fill: #999;
+}
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/fw/layer/flash.js b/framework/src/onos/web/gui/src/main/webapp/app/fw/layer/flash.js
new file mode 100644
index 00000000..0d95b774
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/fw/layer/flash.js
@@ -0,0 +1,165 @@
+/*
+ * 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 -- Layer -- Flash Service
+
+ Provides a mechanism to flash short informational messages to the screen
+ to alert the user of something, e.g. "Hosts visible" or "Hosts hidden".
+ */
+(function () {
+ 'use strict';
+
+ // injected references
+ var $log, $timeout;
+
+ // configuration
+ var defaultSettings = {
+ fade: 200,
+ showFor: 1200
+ },
+ w = '100%',
+ h = 200,
+ xpad = 20,
+ ypad = 10,
+ rx = 10,
+ vbox = '-200 -' + (h/2) + ' 400 ' + h;
+
+ // internal state
+ var settings,
+ timer = null,
+ data = [],
+ enabled;
+
+ // DOM elements
+ var flashDiv, svg;
+
+
+ function computeBox(el) {
+ var text = el.select('text'),
+ box = text.node().getBBox();
+
+ // center
+ box.x = -box.width / 2;
+ box.y = -box.height / 2;
+
+ // add some padding
+ box.x -= xpad;
+ box.width += xpad * 2;
+ box.y -= ypad;
+ box.height += ypad * 2;
+
+ return box;
+ }
+
+ function updateFlash() {
+ if (!svg) {
+ svg = flashDiv.append('svg').attr({
+ width: w,
+ height: h,
+ viewBox: vbox
+ });
+ }
+
+ var items = svg.selectAll('.flashItem')
+ .data(data);
+
+ // this is when there is an existing item
+ items.each(function (msg) {
+ var el = d3.select(this),
+ box;
+
+ el.select('text').text(msg);
+ box = computeBox(el);
+ el.select('rect').attr(box);
+ });
+
+
+ // this is when there is no existing item
+ var entering = items.enter()
+ .append('g')
+ .attr({
+ class: 'flashItem',
+ opacity: 0
+ })
+ .transition()
+ .duration(settings.fade)
+ .attr('opacity', 1);
+
+ entering.each(function (msg) {
+ var el = d3.select(this),
+ box;
+
+ el.append('rect').attr('rx', rx);
+ el.append('text').text(msg);
+ box = computeBox(el);
+ el.select('rect').attr(box);
+ });
+
+ items.exit()
+ .transition()
+ .duration(settings.fade)
+ .attr('opacity', 0)
+ .remove();
+
+ if (svg && data.length === 0) {
+ svg.transition()
+ .delay(settings.fade + 10)
+ .remove();
+ svg = null;
+ }
+ }
+
+ function flash(msg) {
+ if (!enabled) return;
+
+ if (timer) {
+ $timeout.cancel(timer);
+ }
+
+ timer = $timeout(function () {
+ data = [];
+ updateFlash();
+ }, settings.showFor);
+
+ data = [msg];
+ updateFlash();
+ }
+
+ function enable(b) {
+ enabled = !!b;
+ }
+
+ angular.module('onosLayer')
+ .factory('FlashService', ['$log', '$timeout',
+ function (_$log_, _$timeout_) {
+ $log = _$log_;
+ $timeout = _$timeout_;
+
+ function initFlash(opts) {
+ settings = angular.extend({}, defaultSettings, opts);
+ flashDiv = d3.select('#flash');
+ enabled = true;
+ }
+
+ return {
+ initFlash: initFlash,
+ flash: flash,
+ enable: enable
+ };
+ }]);
+
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/fw/layer/layer.js b/framework/src/onos/web/gui/src/main/webapp/app/fw/layer/layer.js
new file mode 100644
index 00000000..a47f53e0
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/fw/layer/layer.js
@@ -0,0 +1,25 @@
+/*
+ * 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 -- Layers Module
+ */
+(function () {
+ 'use strict';
+
+ angular.module('onosLayer', ['onosUtil']);
+
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/fw/layer/panel.css b/framework/src/onos/web/gui/src/main/webapp/app/fw/layer/panel.css
new file mode 100644
index 00000000..46dbdb52
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/fw/layer/panel.css
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2014,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 -- Panel Service -- CSS file
+ */
+
+.floatpanel {
+ position: absolute;
+ z-index: 100;
+ display: block;
+ top: 64px;
+ width: 200px;
+ right: -220px;
+ opacity: 0;
+
+ padding: 10px;
+ font-size: 10pt;
+
+ -moz-border-radius: 6px;
+ border-radius: 6px;
+}
+
+html[data-platform='iPad'] .floatpanel {
+ top: 80px;
+}
+
+.light .floatpanel {
+ background-color: rgba(255,255,255,0.8);
+ color: black;
+ box-shadow: 0 2px 12px #777;
+}
+.dark .floatpanel {
+ background-color: rgba(50,50,50,0.8);
+ color: #ccc;
+ box-shadow: 0 2px 12px #000;
+}
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/fw/layer/panel.js b/framework/src/onos/web/gui/src/main/webapp/app/fw/layer/panel.js
new file mode 100644
index 00000000..aef71eee
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/fw/layer/panel.js
@@ -0,0 +1,214 @@
+/*
+ * 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 -- Layer -- Panel Service
+ */
+(function () {
+ 'use strict';
+
+ var $log, fs;
+
+ var defaultSettings = {
+ edge: 'right',
+ width: 200,
+ margin: 20,
+ hideMargin: 20,
+ xtnTime: 750,
+ fade: true
+ };
+
+ var panels,
+ panelLayer;
+
+
+ function init() {
+ panelLayer = d3.select('#floatpanels');
+ panelLayer.html('');
+ panels = {};
+ }
+
+ // helpers for panel
+ function noop() {}
+
+ function margin(p) {
+ return p.settings.margin;
+ }
+ function hideMargin(p) {
+ return p.settings.hideMargin;
+ }
+ function noPx(p, what) {
+ return Number(p.el.style(what).replace(/px$/, ''));
+ }
+ function widthVal(p) {
+ return noPx(p, 'width');
+ }
+ function heightVal(p) {
+ return noPx(p, 'height');
+ }
+ function pxShow(p) {
+ return margin(p) + 'px';
+ }
+ function pxHide(p) {
+ return (-hideMargin(p) - widthVal(p) - (noPx(p, 'padding') * 2)) + 'px';
+ }
+
+ function makePanel(id, settings) {
+ var p = {
+ id: id,
+ settings: settings,
+ on: false,
+ el: null
+ },
+ api = {
+ show: showPanel,
+ hide: hidePanel,
+ toggle: togglePanel,
+ empty: emptyPanel,
+ append: appendPanel,
+ width: panelWidth,
+ height: panelHeight,
+ isVisible: panelIsVisible,
+ classed: classed,
+ el: panelEl
+ };
+
+ p.el = panelLayer.append('div')
+ .attr('id', id)
+ .attr('class', 'floatpanel')
+ .style('opacity', 0);
+
+ // has to be called after el is set
+ p.el.style(p.settings.edge, pxHide(p));
+ panelWidth(p.settings.width);
+ if (p.settings.height) {
+ panelHeight(p.settings.height);
+ }
+
+ panels[id] = p;
+
+ function showPanel(cb) {
+ var endCb = fs.isF(cb) || noop;
+ p.on = true;
+ p.el.transition().duration(p.settings.xtnTime)
+ .each('end', endCb)
+ .style(p.settings.edge, pxShow(p))
+ .style('opacity', 1);
+ }
+
+ function hidePanel(cb) {
+ var endCb = fs.isF(cb) || noop,
+ endOpacity = p.settings.fade ? 0 : 1;
+ p.on = false;
+ p.el.transition().duration(p.settings.xtnTime)
+ .each('end', endCb)
+ .style(p.settings.edge, pxHide(p))
+ .style('opacity', endOpacity);
+ }
+
+ function togglePanel(cb) {
+ if (p.on) {
+ hidePanel(cb);
+ } else {
+ showPanel(cb);
+ }
+ return p.on;
+ }
+
+ function emptyPanel() {
+ return p.el.html('');
+ }
+
+ function appendPanel(what) {
+ return p.el.append(what);
+ }
+
+ function panelWidth(w) {
+ if (w === undefined) {
+ return widthVal(p);
+ }
+ p.el.style('width', w + 'px');
+ }
+
+ function panelHeight(h) {
+ if (h === undefined) {
+ return heightVal(p);
+ }
+ p.el.style('height', h + 'px');
+ }
+
+ function panelIsVisible() {
+ return p.on;
+ }
+
+ function classed(cls, bool) {
+ return p.el.classed(cls, bool);
+ }
+
+ function panelEl() {
+ return p.el;
+ }
+
+ return api;
+ }
+
+ function removePanel(id) {
+ panelLayer.select('#' + id).remove();
+ delete panels[id];
+ }
+
+ angular.module('onosLayer')
+ .factory('PanelService', ['$log', 'FnService', function (_$log_, _fs_) {
+ $log = _$log_;
+ fs = _fs_;
+
+ function createPanel(id, opts) {
+ var settings = angular.extend({}, defaultSettings, opts);
+ if (!id) {
+ $log.warn('createPanel: no ID given');
+ return null;
+ }
+ if (panels[id]) {
+ $log.warn('Panel with ID "' + id + '" already exists');
+ return null;
+ }
+ if (fs.debugOn('widget')) {
+ $log.debug('creating panel:', id, settings);
+ }
+ return makePanel(id, settings);
+ }
+
+ function destroyPanel(id) {
+ if (panels[id]) {
+ if (fs.debugOn('widget')) {
+ $log.debug('destroying panel:', id);
+ }
+ removePanel(id);
+ } else {
+ if (fs.debugOn('widget')) {
+ $log.debug('no panel to destroy:', id);
+ }
+ }
+ }
+
+ return {
+ init: init,
+ createPanel: createPanel,
+ destroyPanel: destroyPanel
+ };
+ }]);
+
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/fw/layer/quickhelp.css b/framework/src/onos/web/gui/src/main/webapp/app/fw/layer/quickhelp.css
new file mode 100644
index 00000000..bb806d8a
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/fw/layer/quickhelp.css
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2014,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 -- Quick Help Service -- CSS file
+ */
+
+#quickhelp {
+ z-index: 1300;
+}
+
+#quickhelp svg {
+ position: absolute;
+ top: 180px;
+ opacity: 1;
+}
+
+#quickhelp svg g.help rect {
+ fill: black;
+ opacity: 0.7;
+}
+
+#quickhelp svg text.title {
+ font-size: 10pt;
+ font-style: italic;
+ text-anchor: middle;
+ fill: #999;
+}
+
+#quickhelp svg g.keyItem {
+ fill: white;
+}
+
+#quickhelp svg g line.qhrowsep {
+ stroke: #888;
+ stroke-dasharray: 2 2;
+}
+
+#quickhelp svg text {
+ font-size: 7pt;
+ alignment-baseline: middle;
+}
+
+#quickhelp svg text.key {
+ fill: #add;
+}
+
+#quickhelp svg text.desc {
+ fill: #ddd;
+}
+
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/fw/layer/quickhelp.js b/framework/src/onos/web/gui/src/main/webapp/app/fw/layer/quickhelp.js
new file mode 100644
index 00000000..a25cf68d
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/fw/layer/quickhelp.js
@@ -0,0 +1,387 @@
+/*
+ * 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 -- Layer -- Quick Help Service
+
+ Provides a mechanism to display key bindings and mouse gesture notes.
+ */
+(function () {
+ 'use strict';
+
+ // injected references
+ var $log, fs, sus;
+
+ // configuration
+ var defaultSettings = {
+ fade: 500
+ },
+ w = '100%',
+ h = '80%',
+ vbox = '-200 0 400 400',
+ pad = 10,
+ offy = 45,
+ sepYDelta = 20,
+ colXDelta = 16,
+ yTextSpc = 12,
+ offDesc = 8;
+
+ // internal state
+ var settings,
+ data = [],
+ yCount;
+
+ // DOM elements
+ var qhdiv, svg, pane, rect, items;
+
+ // key-logical-name to key-display lookup..
+ var keyDisp = {
+ equals: '=',
+ slash: '/',
+ backSlash: '\\',
+ backQuote: '`',
+ leftArrow: 'L-arrow',
+ upArrow: 'U-arrow',
+ rightArrow: 'R-arrow',
+ downArrow: 'D-arrow'
+ };
+
+ // list of needed bindings to use in aggregateData
+ var neededBindings = [
+ 'globalKeys', 'globalFormat', 'viewKeys', 'viewGestures'
+ ];
+
+ // ===========================================
+ // === Function Definitions ===
+
+ function mkKeyDisp(id) {
+ var v = keyDisp[id] || id;
+ return fs.cap(v);
+ }
+
+ function addSeparator(el, i) {
+ var y = sepYDelta/2 - 5;
+ el.append('line')
+ .attr({ 'class': 'qhrowsep', x1: 0, y1: y, x2: 0, y2: y });
+ }
+
+ function addContent(el, data, ri) {
+ var xCount = 0,
+ clsPfx = 'qh-r' + ri + '-c';
+
+ function addColumn(el, c, i) {
+ var cls = clsPfx + i,
+ oy = 0,
+ aggKey = el.append('g').attr('visibility', 'hidden'),
+ gcol = el.append('g').attr({
+ 'class': cls,
+ transform: sus.translate(xCount, 0)
+ });
+
+ c.forEach(function (j) {
+ var k = j[0],
+ v = j[1];
+
+ if (k !== '-') {
+ aggKey.append('text').text(k);
+
+ gcol.append('text').text(k)
+ .attr({
+ 'class': 'key',
+ y: oy
+ });
+ gcol.append('text').text(v)
+ .attr({
+ 'class': 'desc',
+ y: oy
+ });
+ }
+
+ oy += yTextSpc;
+ });
+
+ // adjust position of descriptions, based on widest key
+ var kbox = aggKey.node().getBBox(),
+ ox = kbox.width + offDesc;
+ gcol.selectAll('.desc').attr('x', ox);
+ aggKey.remove();
+
+ // now update x-offset for next column
+ var bbox = gcol.node().getBBox();
+ xCount += bbox.width + colXDelta;
+ }
+
+ data.forEach(function (d, i) {
+ addColumn(el, d, i);
+ });
+
+ // finally, return the height of the row..
+ return el.node().getBBox().height;
+ }
+
+ function updateKeyItems() {
+ var rows = items.selectAll('.qhRow').data(data);
+
+ yCount = offy;
+
+ var entering = rows.enter()
+ .append('g')
+ .attr({
+ 'class': 'qhrow'
+ });
+
+ entering.each(function (r, i) {
+ var el = d3.select(this),
+ sep = r.type === 'sep',
+ dy;
+
+ el.attr('transform', sus.translate(0, yCount));
+
+ if (sep) {
+ addSeparator(el, i);
+ yCount += sepYDelta;
+ } else {
+ dy = addContent(el, r.data, i);
+ yCount += dy;
+ }
+ });
+
+ // size the backing rectangle
+ var ibox = items.node().getBBox(),
+ paneW = ibox.width + pad * 2,
+ paneH = ibox.height + offy;
+
+ items.selectAll('.qhrowsep').attr('x2', ibox.width);
+ items.attr('transform', sus.translate(-paneW/2, -pad));
+ rect.attr({
+ width: paneW,
+ height: paneH,
+ transform: sus.translate(-paneW/2-pad, 0)
+ });
+
+ }
+
+ function checkFmt(fmt) {
+ // should be a single array of keys,
+ // or array of arrays of keys (one per column).
+ // return null if there is a problem.
+ var a = fs.isA(fmt),
+ n = a && a.length,
+ ns = 0,
+ na = 0;
+
+ if (n) {
+ // it is an array which has some content
+ a.forEach(function (d) {
+ fs.isA(d) && na++;
+ fs.isS(d) && ns++;
+ });
+ if (na === n || ns === n) {
+ // all arrays or all strings...
+ return a;
+ }
+ }
+ return null;
+ }
+
+ function buildBlock(map, fmt) {
+ var b = [];
+ fmt.forEach(function (k) {
+ var v = map.get(k),
+ a = fs.isA(v),
+ d = (a && a[1]);
+
+ // '-' marks a separator; d is the description
+ if (k === '-' || d) {
+ b.push([mkKeyDisp(k), d]);
+ }
+ });
+ return b;
+ }
+
+ function emptyRow() {
+ return { type: 'row', data: [] };
+ }
+
+ function mkArrRow(fmt) {
+ var d = emptyRow();
+ d.data.push(fmt);
+ return d;
+ }
+
+ function mkColumnarRow(map, fmt) {
+ var d = emptyRow();
+ fmt.forEach(function (a) {
+ d.data.push(buildBlock(map, a));
+ });
+ return d;
+ }
+
+ function mkMapRow(map, fmt) {
+ var d = emptyRow();
+ d.data.push(buildBlock(map, fmt));
+ return d;
+ }
+
+ function addRow(row) {
+ var d = row || { type: 'sep' };
+ data.push(d);
+ }
+
+ function aggregateData(bindings) {
+ var hf = '_helpFormat',
+ gmap = d3.map(bindings.globalKeys),
+ gfmt = bindings.globalFormat,
+ vmap = d3.map(bindings.viewKeys),
+ vgest = bindings.viewGestures,
+ vfmt, vkeys;
+
+ // filter out help format entry
+ vfmt = checkFmt(vmap.get(hf));
+ vmap.remove(hf);
+
+ // if bad (or no) format, fallback to sorted keys
+ if (!vfmt) {
+ vkeys = vmap.keys();
+ vfmt = vkeys.sort();
+ }
+
+ data = [];
+
+ addRow(mkMapRow(gmap, gfmt));
+ addRow();
+ addRow(fs.isA(vfmt[0]) ? mkColumnarRow(vmap, vfmt) : mkMapRow(vmap, vfmt));
+ addRow();
+ addRow(mkArrRow(vgest));
+ }
+
+
+ function popBind(bindings) {
+ pane = svg.append('g')
+ .attr({
+ class: 'help',
+ opacity: 0
+ });
+
+ rect = pane.append('rect')
+ .attr('rx', 8);
+
+ pane.append('text')
+ .text('Quick Help')
+ .attr({
+ class: 'title',
+ dy: '1.2em',
+ transform: sus.translate(-pad,0)
+ });
+
+ items = pane.append('g');
+ aggregateData(bindings);
+ updateKeyItems();
+
+ _fade(1);
+ }
+
+ function fadeBindings() {
+ _fade(0);
+ }
+
+ function _fade(o) {
+ svg.selectAll('g.help')
+ .transition()
+ .duration(settings.fade)
+ .attr('opacity', o);
+ }
+
+ function addSvg() {
+ svg = qhdiv.append('svg')
+ .attr({
+ width: w,
+ height: h,
+ viewBox: vbox
+ });
+ }
+
+ function removeSvg() {
+ svg.transition()
+ .delay(settings.fade + 20)
+ .remove();
+ }
+
+ function goodBindings(bindings) {
+ var warnPrefix = 'Quickhelp Service: showQuickHelp(), ';
+ if (!bindings || !fs.isO(bindings) || fs.isEmptyObject(bindings)) {
+ $log.warn(warnPrefix + 'invalid bindings object');
+ return false;
+ }
+ if (!(neededBindings.every(function (key) { return key in bindings; }))) {
+ $log.warn(
+ warnPrefix +
+ 'needed bindings for help panel not provided:',
+ neededBindings
+ );
+ return false
+ }
+ return true;
+ }
+
+ // ===========================================
+ // === Module Definition ===
+
+ angular.module('onosLayer')
+ .factory('QuickHelpService',
+ ['$log', 'FnService', 'SvgUtilService',
+
+ function (_$log_, _fs_, _sus_) {
+ $log = _$log_;
+ fs = _fs_;
+ sus = _sus_;
+
+ function initQuickHelp(opts) {
+ settings = angular.extend({}, defaultSettings, fs.isO(opts));
+ qhdiv = d3.select('#quickhelp');
+ }
+
+ function showQuickHelp(bindings) {
+ svg = qhdiv.select('svg');
+ if (!goodBindings(bindings)) {
+ return null;
+ }
+ if (svg.empty()) {
+ addSvg();
+ popBind(bindings);
+ } else {
+ hideQuickHelp();
+ }
+ }
+
+ function hideQuickHelp() {
+ svg = qhdiv.select('svg');
+ if (!svg.empty()) {
+ fadeBindings();
+ removeSvg();
+ return true;
+ }
+ return false;
+ }
+
+ return {
+ initQuickHelp: initQuickHelp,
+ showQuickHelp: showQuickHelp,
+ hideQuickHelp: hideQuickHelp
+ };
+ }]);
+
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/fw/layer/veil.css b/framework/src/onos/web/gui/src/main/webapp/app/fw/layer/veil.css
new file mode 100644
index 00000000..afef94ac
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/fw/layer/veil.css
@@ -0,0 +1,50 @@
+/*
+ * 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 -- Veil Service -- CSS file
+ */
+
+#veil {
+ z-index: 5000;
+ display: none;
+ position: absolute;
+ top: 0;
+ left: 0;
+}
+
+.light #veil {
+ background-color: rgba(0,0,0,0.75);
+}
+.dark #veil {
+ background-color: rgba(64,64,64,0.75);
+}
+
+#veil .msg {
+ position: absolute;
+ padding: 60px;
+}
+
+#veil .msg p {
+ display: block;
+ font-size: 14pt;
+ font-style: italic;
+ color: #ddd;
+}
+
+#veil svg .glyph {
+ fill: #222;
+}
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/fw/layer/veil.js b/framework/src/onos/web/gui/src/main/webapp/app/fw/layer/veil.js
new file mode 100644
index 00000000..fc0530af
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/fw/layer/veil.js
@@ -0,0 +1,99 @@
+/*
+ * 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 -- Layer -- Veil Service
+
+ Provides a mechanism to display an overlaying div with information.
+ Used mainly for web socket connection interruption.
+ */
+(function () {
+ 'use strict';
+
+ // injected references
+ var $log, $route, fs, ks, gs;
+
+ var veil;
+
+ function init() {
+ var wSize = fs.windowSize(),
+ ww = wSize.width,
+ wh = wSize.height,
+ shrink = wh * 0.3,
+ birdDim = wh - shrink,
+ birdCenter = (ww - birdDim) / 2,
+ svg;
+
+ veil = d3.select('#veil');
+
+ svg = veil.select('svg').attr({
+ width: ww,
+ height: wh
+ }).style('opacity', 0.2);
+
+ gs.addGlyph(svg, 'bird', birdDim, false, [birdCenter, shrink/2]);
+ }
+
+ // msg should be an array of strings
+ function show(msg) {
+ var msgs = fs.isA(msg) || [msg],
+ pdiv = veil.select('.msg');
+ pdiv.selectAll('p').remove();
+
+ msgs.forEach(function (line) {
+ pdiv.append('p').text(line);
+ });
+
+ veil.style('display', 'block');
+ ks.enableKeys(false);
+ }
+
+ function hide() {
+ veil.style('display', 'none');
+ ks.enableKeys(true);
+ }
+
+ // function that only invokes the veil if the caller is the current view
+ // TODO: review - is this deprecated ?
+ function lostServer(ctrlName, msg) {
+ if ($route.current.$$route.controller === ctrlName) {
+ $log.debug('VEIL-service: ', ctrlName);
+ show(msg)
+ } else {
+ $log.debug('VEIL-service: IGNORING ', ctrlName);
+ }
+ }
+
+ angular.module('onosLayer')
+ .factory('VeilService',
+ ['$log', '$route', 'FnService', 'KeyService', 'GlyphService',
+
+ function (_$log_, _$route_, _fs_, _ks_, _gs_) {
+ $log = _$log_;
+ $route = _$route_;
+ fs = _fs_;
+ ks = _ks_;
+ gs = _gs_;
+
+ return {
+ init: init,
+ show: show,
+ hide: hide,
+ lostServer: lostServer
+ };
+ }]);
+
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/fw/mast/mast.css b/framework/src/onos/web/gui/src/main/webapp/app/fw/mast/mast.css
new file mode 100644
index 00000000..2e86e86d
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/fw/mast/mast.css
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2014,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 -- Masthead -- CSS file
+ */
+
+#mast {
+ height: 36px;
+ padding: 4px;
+ vertical-align: baseline;
+}
+
+html[data-platform='iPad'] #mast {
+ padding-top: 16px;
+}
+
+.light #mast {
+ background-color: #bbb;
+ box-shadow: 0 2px 8px #777;
+}
+.dark #mast {
+ background-color: #444;
+ box-shadow: 0 2px 8px #222;
+}
+
+#mast .nav-menu-button {
+ width: 30px;
+ height: 30px;
+ margin-left: 8px;
+ margin-bottom: 4px;
+ cursor: pointer;
+}
+.light #mast .nav-menu-button:hover {
+ background-color: #ddd;
+}
+.dark #mast .nav-menu-button:hover {
+ background-color: #777;
+}
+
+#mast img.logo {
+ height: 38px;
+ padding-left: 8px;
+ padding-right: 8px;
+}
+
+#mast img.logo:hover {
+ /* need something better */
+ /*background-color: #888;*/
+}
+
+#mast .title {
+ font-size: 14pt;
+ font-style: italic;
+ vertical-align: 12px;
+}
+
+.light #mast .title {
+ color: #369;
+}
+.dark #mast .title {
+ color: #eee;
+}
+
+#mast-right {
+ display: inline-block;
+ padding-top: 8px;
+ padding-right: 16px;
+ float: right;
+ /*border: 1px solid red;*/
+}
+
+#mast-right a {
+ font-size: 12pt;
+ font-style: normal;
+ font-weight: bold;
+ text-decoration: none;
+}
+
+.light #mast-right a {
+ color: #369;
+}
+.dark #mast-right a {
+ color: #eee;
+}
+
+#mast-right a:hover {
+ color: #CE5650;
+}
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/fw/mast/mast.html b/framework/src/onos/web/gui/src/main/webapp/app/fw/mast/mast.html
new file mode 100644
index 00000000..5bb488a8
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/fw/mast/mast.html
@@ -0,0 +1,6 @@
+<!-- Masthead partial HTML -->
+<img class="nav-menu-button" src="data/img/nav-menu.png"
+ ng-click="mastCtrl.toggleNav()"></div>
+<img class="logo" src="data/img/onos-logo.png">
+<span class="title">Open Network Operating System</span>
+<div id="mast-right"><a href="rs/logout">logout</a></div>
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/fw/mast/mast.js b/framework/src/onos/web/gui/src/main/webapp/app/fw/mast/mast.js
new file mode 100644
index 00000000..1cde58c7
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/fw/mast/mast.js
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2014,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 -- Masthead Module
+ */
+(function () {
+ 'use strict';
+
+ // injected services
+ var $log;
+
+ // configuration
+ var mastHeight = 36,
+ padMobile = 16;
+
+ angular.module('onosMast', ['onosNav'])
+ .controller('MastCtrl', ['$log', 'NavService', function (_$log_, ns) {
+ var self = this;
+
+ $log = _$log_;
+
+ // initialize mast controller here...
+ self.radio = null;
+
+ // delegate to NavService
+ self.toggleNav = function () {
+ ns.toggleNav();
+ };
+
+ $log.log('MastCtrl has been created');
+ }])
+
+ // also define a service to allow lookup of mast height.
+ .factory('MastService', ['FnService', function (fs) {
+ return {
+ mastHeight: function () {
+ return fs.isMobile() ? mastHeight + padMobile : mastHeight;
+ }
+ }
+ }]);
+
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/fw/nav/nav.css b/framework/src/onos/web/gui/src/main/webapp/app/fw/nav/nav.css
new file mode 100644
index 00000000..4d2c4e91
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/fw/nav/nav.css
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2014,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 -- Navigation -- CSS file
+ */
+
+#nav {
+ position: absolute;
+ top: 45px;
+ left: 1px;
+ padding: 0;
+ z-index: 3000;
+ visibility: hidden;
+}
+
+html[data-platform='iPad'] #nav {
+ top: 57px;
+}
+
+.light #nav {
+ background-color: #bbb;
+ box-shadow: 0 2px 8px #777;
+}
+.dark #nav {
+ background-color: #444;
+ box-shadow: 0 2px 8px #111;
+}
+
+#nav .nav-hdr {
+ font-style: italic;
+ padding: 6px 8px 6px 8px;
+}
+
+.light #nav .nav-hdr {
+ color: #ddd;
+ border-bottom: solid 1px #999;
+ background-color: #aaa;
+}
+.dark #nav .nav-hdr {
+ color: #888;
+ border-bottom: solid 1px #444;
+ background-color: #555;
+}
+
+#nav a {
+ text-decoration: none;
+ font-size: 14pt;
+ display: block;
+ padding: 8px 16px 6px 12px;
+}
+
+.light #nav a {
+ color: #369;
+ border-bottom: solid #ccc 1px;
+}
+.dark #nav a {
+ color: #eee;
+ border-bottom: solid #333 1px;
+}
+
+.light #nav a:hover {
+ background-color: #ddd;
+}
+.dark #nav a:hover {
+ background-color: #777;
+}
+
+#nav a div {
+ display: inline-block;
+ vertical-align: middle;
+ padding-right: 4px;
+}
+
+#nav a div svg.embeddedIcon g.icon .glyph {
+ fill: #c66;
+}
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/fw/nav/nav.js b/framework/src/onos/web/gui/src/main/webapp/app/fw/nav/nav.js
new file mode 100644
index 00000000..36ef599e
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/fw/nav/nav.js
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2014,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 -- Navigation Module
+ */
+(function () {
+ 'use strict';
+
+ // injected dependencies
+ var $log, $location, $window, fs;
+
+ // internal state
+ var navShown = false;
+
+ function updatePane() {
+ var vis = navShown ? 'visible' : 'hidden';
+ d3.select('#nav').style('visibility', vis);
+ }
+
+
+ function showNav() {
+ navShown = true;
+ updatePane();
+ }
+ function hideNav() {
+ navShown = false;
+ updatePane();
+ }
+ function toggleNav() {
+ navShown = !navShown;
+ updatePane();
+ }
+ function hideIfShown() {
+ if (navShown) {
+ hideNav();
+ return true;
+ }
+ return false;
+ }
+
+ function navTo(path, params) {
+ var url;
+ if (!path) {
+ $log.warn('Not a valid navigation path');
+ return null;
+ }
+ $location.url('/' + path);
+
+ if (fs.isO(params)) {
+ $location.search(params);
+ } else if (params !== undefined) {
+ $log.warn('Query params not an object', params);
+ }
+
+ url = $location.absUrl();
+ $log.log('Navigating to ', url);
+ $window.location.href = url;
+ }
+
+ angular.module('onosNav', [])
+ .controller('NavCtrl', ['$log',
+
+ function (_$log_) {
+ var self = this;
+ $log = _$log_;
+
+ self.hideNav = hideNav;
+ $log.log('NavCtrl has been created');
+ }
+ ])
+ .factory('NavService',
+ ['$log', '$location', '$window', 'FnService',
+
+ function (_$log_, _$location_, _$window_, _fs_) {
+ $log = _$log_;
+ $location = _$location_;
+ $window = _$window_;
+ fs = _fs_;
+
+ return {
+ showNav: showNav,
+ hideNav: hideNav,
+ toggleNav: toggleNav,
+ hideIfShown: hideIfShown,
+ navTo: navTo
+ };
+ }]);
+
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/fw/remote/remote.js b/framework/src/onos/web/gui/src/main/webapp/app/fw/remote/remote.js
new file mode 100644
index 00000000..453d08f9
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/fw/remote/remote.js
@@ -0,0 +1,25 @@
+/*
+ * 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 -- Remote Communications Module
+ */
+(function () {
+ 'use strict';
+
+ angular.module('onosRemote', ['onosUtil']);
+
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/fw/remote/rest.js b/framework/src/onos/web/gui/src/main/webapp/app/fw/remote/rest.js
new file mode 100644
index 00000000..04b9fe02
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/fw/remote/rest.js
@@ -0,0 +1,72 @@
+/*
+ * 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 -- Remote Communications Module -- REST Service
+ */
+(function () {
+ 'use strict';
+
+ var $log;
+
+ angular.module('onosRemote')
+ .factory('RestService',
+ ['$log', '$http', 'FnService', 'UrlFnService',
+
+ function (_$log_, $http, fs, ufs) {
+ $log = _$log_;
+
+ function get(url, callback, errorCb) {
+ var fullUrl = ufs.rsUrl(url);
+
+ $http.get(fullUrl).then(function (response) {
+ // success
+ callback(response.data);
+ }, function (response) {
+ // error
+ var emsg = 'Failed to retrieve JSON data: ' + fullUrl;
+ $log.warn(emsg, response.status, response.data);
+ if (errorCb) {
+ errorCb(emsg);
+ }
+ });
+ }
+
+ // TODO: test this
+ function post(url, data, callbacks) {
+ var fullUrl = ufs.rsUrl(url);
+ $http.post(fullUrl, data).then(function (response) {
+ // success
+ if (callbacks && fs.isF(callbacks.success)) {
+ callbacks.success(response.data);
+ }
+ }, function (response) {
+ // error
+ var msg = 'Problem with $http post request: ' + fullUrl;
+ $log.warn(msg, response.status, response.data);
+
+ if (callbacks && fs.isF(callbacks.error)) {
+ callbacks.error(msg);
+ }
+ });
+ }
+
+ return {
+ get: get,
+ post: post
+ };
+ }]);
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/fw/remote/urlfn.js b/framework/src/onos/web/gui/src/main/webapp/app/fw/remote/urlfn.js
new file mode 100644
index 00000000..6ec01b41
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/fw/remote/urlfn.js
@@ -0,0 +1,63 @@
+/*
+ * 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 -- Remote -- General Purpose Functions
+ */
+(function () {
+ 'use strict';
+
+ var uiContext = '/onos/ui/',
+ rsSuffix = uiContext + 'rs/',
+ wsSuffix = uiContext + 'websock/';
+
+ angular.module('onosRemote')
+ .factory('UrlFnService', ['$location', function ($loc) {
+
+ function matchSecure(protocol) {
+ var p = $loc.protocol(),
+ secure = (p === 'https' || p === 'wss');
+ return secure ? protocol + 's' : protocol;
+ }
+
+ function urlBase(protocol, port, host) {
+ return matchSecure(protocol) + '://' +
+ (host || $loc.host()) + ':' + (port || $loc.port());
+ }
+
+ function httpPrefix(suffix) {
+ return urlBase('http') + suffix;
+ }
+
+ function wsPrefix(suffix, wsport, host) {
+ return urlBase('ws', wsport, host) + suffix;
+ }
+
+ function rsUrl(path) {
+ return httpPrefix(rsSuffix) + path;
+ }
+
+ function wsUrl(path, wsport, host) {
+ return wsPrefix(wsSuffix, wsport, host) + path;
+ }
+
+ return {
+ rsUrl: rsUrl,
+ wsUrl: wsUrl
+ };
+ }]);
+
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/fw/remote/websocket.js b/framework/src/onos/web/gui/src/main/webapp/app/fw/remote/websocket.js
new file mode 100644
index 00000000..1c03d6fd
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/fw/remote/websocket.js
@@ -0,0 +1,329 @@
+/*
+ * 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 -- Remote -- Web Socket Service
+ */
+(function () {
+ 'use strict';
+
+ // injected refs
+ var $log, $loc, fs, ufs, wsock, vs;
+
+ // internal state
+ var webSockOpts, // web socket options
+ ws = null, // web socket reference
+ wsUp = false, // web socket is good to go
+ sid = 0, // event sequence identifier
+ handlers = {}, // event handler bindings
+ pendingEvents = [], // events TX'd while socket not up
+ host, // web socket host
+ url, // web socket URL
+ clusterNodes = [], // ONOS instances data for failover
+ clusterIndex = -1, // the instance to which we are connected
+ connectRetries = 0, // limit our attempts at reconnecting
+ openListeners = {}, // registered listeners for websocket open()
+ nextListenerId = 1; // internal ID for open listeners
+
+ // =======================
+ // === Bootstrap Handler
+
+ var builtinHandlers = {
+ bootstrap: function (data) {
+ clusterNodes = data.clusterNodes;
+ clusterNodes.forEach(function (d, i) {
+ if (d.uiAttached) {
+ clusterIndex = i;
+ $log.info('Connected to cluster node ' + d.ip);
+ // TODO: add connect info to masthead somewhere
+ }
+ });
+ }
+ };
+
+ // ==========================
+ // === Web socket callbacks
+
+ function handleOpen() {
+ $log.info('Web socket open - ', url);
+ vs.hide();
+
+ if (fs.debugOn('txrx')) {
+ $log.debug('Sending ' + pendingEvents.length + ' pending event(s)...');
+ }
+ pendingEvents.forEach(function (ev) {
+ _send(ev);
+ });
+ pendingEvents = [];
+
+ connectRetries = 0;
+ wsUp = true;
+ informListeners(host, url);
+ }
+
+ // Handles the specified (incoming) message using handler bindings.
+ function handleMessage(msgEvent) {
+ var ev, h;
+
+ try {
+ ev = JSON.parse(msgEvent.data);
+ } catch (e) {
+ $log.error('Message.data is not valid JSON', msgEvent.data, e);
+ return null;
+ }
+ if (fs.debugOn('txrx')) {
+ $log.debug(' << *Rx* ', ev.event, ev.payload);
+ }
+
+ if (h = handlers[ev.event]) {
+ try {
+ h(ev.payload);
+ } catch (e) {
+ $log.error('Problem handling event:', ev, e);
+ return null;
+ }
+ } else {
+ $log.warn('Unhandled event:', ev);
+ }
+
+ }
+
+ function handleClose() {
+ var gsucc;
+
+ $log.info('Web socket closed');
+ wsUp = false;
+
+ if (gsucc = findGuiSuccessor()) {
+ createWebSocket(webSockOpts, gsucc);
+ } else {
+ // If no controllers left to contact, show the Veil...
+ vs.show([
+ 'Oops!',
+ 'Web-socket connection to server closed...',
+ 'Try refreshing the page.'
+ ]);
+ }
+ }
+
+
+ // ==============================
+ // === Private Helper Functions
+
+ function findGuiSuccessor() {
+ var ncn = clusterNodes.length,
+ ip, node;
+
+ while (connectRetries < ncn && !ip) {
+ connectRetries++;
+ clusterIndex = (clusterIndex + 1) % ncn;
+ node = clusterNodes[clusterIndex];
+ ip = node && node.ip;
+ }
+
+ return ip;
+ }
+
+ function informListeners(host, url) {
+ angular.forEach(openListeners, function (lsnr) {
+ lsnr.cb(host, url);
+ });
+ }
+
+ function _send(ev) {
+ if (fs.debugOn('txrx')) {
+ $log.debug(' *Tx* >> ', ev.event, ev.payload);
+ }
+ ws.send(JSON.stringify(ev));
+ }
+
+ function noHandlersWarn(handlers, caller) {
+ if (!handlers || fs.isEmptyObject(handlers)) {
+ $log.warn('WSS.' + caller + '(): no event handlers');
+ return true;
+ }
+ return false;
+ }
+
+ // ===================
+ // === API Functions
+
+ // Required for unit tests to set to known state
+ function resetSid() {
+ sid = 0;
+ }
+ function resetState() {
+ webSockOpts = undefined;
+ ws = null;
+ wsUp = false;
+ host = undefined;
+ url = undefined;
+ pendingEvents = [];
+ handlers = {};
+ sid = 0;
+ clusterNodes = [];
+ clusterIndex = -1;
+ connectRetries = 0;
+ openListeners = {};
+ nextListenerId = 1;
+ }
+
+ // Currently supported opts:
+ // wsport: web socket port (other than default 8181)
+ // host: if defined, is the host address to use
+ function createWebSocket(opts, _host_) {
+ var wsport = (opts && opts.wsport) || null;
+
+ webSockOpts = opts; // preserved for future calls
+
+ host = _host_ || $loc.host();
+ url = ufs.wsUrl('core', wsport, _host_);
+
+ $log.debug('Attempting to open websocket to: ' + url);
+ ws = wsock.newWebSocket(url);
+ if (ws) {
+ ws.onopen = handleOpen;
+ ws.onmessage = handleMessage;
+ ws.onclose = handleClose;
+ }
+ // Note: Wsock logs an error if the new WebSocket call fails
+ return url;
+ }
+
+ // Binds the message handlers to their message type (event type) as
+ // specified in the given map. Note that keys are the event IDs; values
+ // are either:
+ // * the event handler function, or
+ // * an API object which has an event handler for the key
+ //
+ function bindHandlers(handlerMap) {
+ var m,
+ dups = [];
+
+ if (noHandlersWarn(handlerMap, 'bindHandlers')) {
+ return null;
+ }
+ m = d3.map(handlerMap);
+
+ m.forEach(function (eventId, api) {
+ var fn = fs.isF(api) || fs.isF(api[eventId]);
+ if (!fn) {
+ $log.warn(eventId + ' handler not a function');
+ return;
+ }
+
+ if (handlers[eventId]) {
+ dups.push(eventId);
+ } else {
+ handlers[eventId] = fn;
+ }
+ });
+ if (dups.length) {
+ $log.warn('duplicate bindings ignored:', dups);
+ }
+ }
+
+ // Unbinds the specified message handlers.
+ // Expected that the same map will be used, but we only care about keys
+ function unbindHandlers(handlerMap) {
+ var m;
+
+ if (noHandlersWarn(handlerMap, 'unbindHandlers')) {
+ return null;
+ }
+ m = d3.map(handlerMap);
+
+ m.forEach(function (eventId) {
+ delete handlers[eventId];
+ });
+ }
+
+ function addOpenListener(callback) {
+ var id = nextListenerId++,
+ cb = fs.isF(callback),
+ o = { id: id, cb: cb };
+
+ if (cb) {
+ openListeners[id] = o;
+ } else {
+ $log.error('WSS.addOpenListener(): callback not a function');
+ o.error = 'No callback defined';
+ }
+ return o;
+ }
+
+ function removeOpenListener(lsnr) {
+ var id = fs.isO(lsnr) && lsnr.id,
+ o;
+ if (!id) {
+ $log.warn('WSS.removeOpenListener(): invalid listener', lsnr);
+ return null;
+ }
+ o = openListeners[id];
+
+ if (o) {
+ delete openListeners[id];
+ }
+ }
+
+ // Formulates an event message and sends it via the web-socket.
+ // If the websocket is not up yet, we store it in a pending list.
+ function sendEvent(evType, payload) {
+ var ev = {
+ event: evType,
+ sid: ++sid,
+ payload: payload || {}
+ };
+
+ if (wsUp) {
+ _send(ev);
+ } else {
+ pendingEvents.push(ev);
+ }
+ }
+
+
+ // ============================
+ // ===== Definition of module
+ angular.module('onosRemote')
+ .factory('WebSocketService',
+ ['$log', '$location', 'FnService', 'UrlFnService', 'WSock',
+ 'VeilService',
+
+ function (_$log_, _$loc_, _fs_, _ufs_, _wsock_, _vs_) {
+ $log = _$log_;
+ $loc = _$loc_;
+ fs = _fs_;
+ ufs = _ufs_;
+ wsock = _wsock_;
+ vs = _vs_;
+
+ bindHandlers(builtinHandlers);
+
+ return {
+ resetSid: resetSid,
+ resetState: resetState,
+ createWebSocket: createWebSocket,
+ bindHandlers: bindHandlers,
+ unbindHandlers: unbindHandlers,
+ addOpenListener: addOpenListener,
+ removeOpenListener: removeOpenListener,
+ sendEvent: sendEvent
+ };
+ }
+ ]);
+
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/fw/remote/wsevent.js b/framework/src/onos/web/gui/src/main/webapp/app/fw/remote/wsevent.js
new file mode 100644
index 00000000..02d71b52
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/fw/remote/wsevent.js
@@ -0,0 +1,49 @@
+/*
+ * 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.
+ */
+
+/*
+ DEPRECATED: to be deleted
+ ONOS GUI -- Remote -- Web Socket Event Service
+ */
+(function () {
+ 'use strict';
+
+ var sid = 0;
+
+ angular.module('onosRemote')
+ .factory('WsEventService', [function () {
+
+ function sendEvent(ws, evType, payload) {
+ var p = payload || {};
+
+ ws.send({
+ event: evType,
+ sid: ++sid,
+ payload: p
+ });
+ }
+
+ function resetSid() {
+ sid = 0;
+ }
+
+ return {
+ sendEvent: sendEvent,
+ resetSid: resetSid
+ };
+ }]);
+
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/fw/remote/wsock.js b/framework/src/onos/web/gui/src/main/webapp/app/fw/remote/wsock.js
new file mode 100644
index 00000000..0c58a798
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/fw/remote/wsock.js
@@ -0,0 +1,42 @@
+/*
+ * 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 -- Remote -- Web Socket Wrapper Service
+
+ This service provided specifically so that it can be mocked in unit tests.
+ */
+(function () {
+ 'use strict';
+
+ angular.module('onosRemote')
+ .factory('WSock', ['$log', function ($log) {
+
+ function newWebSocket(url) {
+ var ws = null;
+ try {
+ ws = new WebSocket(url);
+ } catch (e) {
+ $log.error('Unable to create web socket:', e);
+ }
+ return ws;
+ }
+
+ return {
+ newWebSocket: newWebSocket
+ };
+ }]);
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/fw/svg/geodata.js b/framework/src/onos/web/gui/src/main/webapp/app/fw/svg/geodata.js
new file mode 100644
index 00000000..6c54bb45
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/fw/svg/geodata.js
@@ -0,0 +1,186 @@
+/*
+ * 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 -- SVG -- GeoData Service
+ */
+
+/*
+ The GeoData Service facilitates the fetching and caching of TopoJSON data
+ from the server, as well as providing a way of creating a path generator
+ for that data, to be used to render the map in an SVG layer.
+
+ A TopoData object can be fetched by ID. IDs that start with an asterisk
+ identify maps bundled with the GUI. IDs that do not start with an
+ asterisk are assumed to be URLs to externally provided data.
+
+ var topodata = GeoDataService.fetchTopoData('*continental-us');
+
+ The path generator can then be created for that data-set:
+
+ var gen = GeoDataService.createPathGenerator(topodata, opts);
+
+ opts is an optional argument that allows the override of default settings:
+ {
+ objectTag: 'states',
+ projection: d3.geo.mercator(),
+ logicalSize: 1000,
+ mapFillScale: .95
+ };
+
+ The returned object (gen) comprises transformed data (TopoJSON -> GeoJSON),
+ the D3 path generator function, and the settings used ...
+
+ {
+ geodata: { ... },
+ pathgen: function (...) { ... },
+ settings: { ... }
+ }
+ */
+
+(function () {
+ 'use strict';
+
+ // injected references
+ var $log, $http, fs;
+
+ // internal state
+ var cache = d3.map(),
+ bundledUrlPrefix = 'data/map/';
+
+ function getUrl(id) {
+ if (id[0] === '*') {
+ return bundledUrlPrefix + id.slice(1) + '.topojson';
+ }
+ return id + '.topojson';
+ }
+
+
+ // start afresh...
+ function clearCache() {
+ cache = d3.map();
+ }
+
+ // returns a promise decorated with:
+ // .meta -- id, url, and whether the data was cached
+ // .topodata -- TopoJSON data (on response from server)
+
+ function fetchTopoData(id) {
+ if (!fs.isS(id)) {
+ return null;
+ }
+ var url = getUrl(id),
+ promise = cache.get(id);
+
+ if (!promise) {
+ // need to fetch the data, build the object,
+ // cache it, and return it.
+ promise = $http.get(url);
+
+ promise.meta = {
+ id: id,
+ url: url,
+ wasCached: false
+ };
+
+ promise.then(function (response) {
+ // success
+ promise.topodata = response.data;
+ }, function (response) {
+ // error
+ $log.warn('Failed to retrieve map TopoJSON data: ' + url,
+ response.status, response.data);
+ });
+
+ cache.set(id, promise);
+
+ } else {
+ promise.meta.wasCached = true;
+ }
+
+ return promise;
+ }
+
+ var defaultGenSettings = {
+ objectTag: 'states',
+ projection: d3.geo.mercator(),
+ logicalSize: 1000,
+ mapFillScale: .95
+ };
+
+ // converts given TopoJSON-format data into corresponding GeoJSON
+ // data, and creates a path generator for that data.
+ function createPathGenerator(topoData, opts) {
+ var settings = angular.extend({}, defaultGenSettings, opts),
+ topoObject = topoData.objects[settings.objectTag],
+ geoData = topojson.feature(topoData, topoObject),
+ proj = settings.projection,
+ dim = settings.logicalSize,
+ mfs = settings.mapFillScale,
+ path = d3.geo.path().projection(proj);
+
+ rescaleProjection(proj, mfs, dim, path, geoData);
+
+ // return the results
+ return {
+ geodata: geoData,
+ pathgen: path,
+ settings: settings
+ };
+ }
+
+ function rescaleProjection(proj, mfs, dim, path, geoData) {
+ // adjust projection scale and translation to fill the view
+ // with the map
+
+ // start with unit scale, no translation..
+ proj.scale(1).translate([0, 0]);
+
+ // figure out dimensions of map data..
+ var b = path.bounds(geoData),
+ x1 = b[0][0],
+ y1 = b[0][1],
+ x2 = b[1][0],
+ y2 = b[1][1],
+ dx = x2 - x1,
+ dy = y2 - y1,
+ x = (x1 + x2) / 2,
+ y = (y1 + y2) / 2;
+
+ // size map to 95% of minimum dimension to fill space..
+ var s = mfs / Math.min(dx / dim, dy / dim),
+ t = [dim / 2 - s * x, dim / 2 - s * y];
+
+ // set new scale, translation on the projection..
+ proj.scale(s).translate(t);
+ }
+
+ angular.module('onosSvg')
+ .factory('GeoDataService', ['$log', '$http', 'FnService',
+ function (_$log_, _$http_, _fs_) {
+ $log = _$log_;
+ $http = _$http_;
+ fs = _fs_;
+
+
+ return {
+ clearCache: clearCache,
+ fetchTopoData: fetchTopoData,
+ createPathGenerator: createPathGenerator,
+ rescaleProjection: rescaleProjection
+ };
+ }]);
+}()); \ No newline at end of file
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/fw/svg/glyph.css b/framework/src/onos/web/gui/src/main/webapp/app/fw/svg/glyph.css
new file mode 100644
index 00000000..82910654
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/fw/svg/glyph.css
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2014,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 -- Glyph Service -- CSS file
+ */
+
+svg .glyph {
+ stroke: none;
+ fill-rule: evenodd;
+}
+
+.light svg .glyph,
+.dark svg .glyph.overlay {
+ fill: black;
+}
+
+.dark svg .glyph,
+.light svg .glyph.overlay {
+ fill: white;
+}
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/fw/svg/glyph.js b/framework/src/onos/web/gui/src/main/webapp/app/fw/svg/glyph.js
new file mode 100644
index 00000000..838a2ac0
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/fw/svg/glyph.js
@@ -0,0 +1,646 @@
+/*
+ * 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 -- SVG -- Glyph Service
+ */
+(function () {
+ 'use strict';
+
+ // injected references
+ var $log, fs, sus;
+
+ // internal state
+ var glyphs = d3.map();
+
+ // ----------------------------------------------------------------------
+ // Base set of Glyphs...
+
+ var birdData = {
+ _bird: "352 224 113 112",
+ bird: "M427.7,300.4 c-6.9,0.6-13.1,5-19.2,7.1c-18.1,6.2-33.9," +
+ "9.1-56.5,4.7c24.6,17.2,36.6,13,63.7,0.1c-0.5,0.6-0.7,1.3-1.3," +
+ "1.9c1.4-0.4,2.4-1.7,3.4-2.2c-0.4,0.7-0.9,1.5-1.4,1.9c2.2-0.6," +
+ "3.7-2.3,5.9-3.9c-2.4,2.1-4.2,5-6,8c-1.5,2.5-3.1,4.8-5.1,6.9c-1," +
+ "1-1.9,1.9-2.9,2.9c-1.4,1.3-2.9,2.5-5.1,2.9c1.7,0.1,3.6-0.3,6.5" +
+ "-1.9c-1.6,2.4-7.1,6.2-9.9,7.2c10.5-2.6,19.2-15.9,25.7-18c18.3" +
+ "-5.9,13.8-3.4,27-14.2c1.6-1.3,3-1,5.1-0.8c1.1,0.1,2.1,0.3,3.2," +
+ "0.5c0.8,0.2,1.4,0.4,2.2,0.8l1.8,0.9c-1.9-4.5-2.3-4.1-5.9-6c-2.3" +
+ "-1.3-3.3-3.8-6.2-4.9c-7.1-2.6-11.9,11.7-11.7-5c0.1-8,4.2-14.4," +
+ "6.4-22c1.1-3.8,2.3-7.6,2.4-11.5c0.1-2.3,0-4.7-0.4-7c-2-11.2-8.4" +
+ "-21.5-19.7-24.8c-1-0.3-1.1-0.3-0.9,0c9.6,17.1,7.2,38.3,3.1,54.2" +
+ "C429.9,285.5,426.7,293.2,427.7,300.4z"
+ },
+
+ // TODO: ONOS-2566 glyphs for device types:
+ // otn, roadm_otn, firewall, balancer, ips, ids,
+ // controller, virtual, fiber_switch, other
+
+ glyphDataSet = {
+ _viewbox: "0 0 110 110",
+
+ unknown: "M35,40a5,5,0,0,1,5-5h30a5,5,0,0,1,5,5v30a5,5,0,0,1-5,5" +
+ "h-30a5,5,0,0,1-5-5z",
+
+ node: "M15,100a5,5,0,0,1-5-5v-65a5,5,0,0,1,5-5h80a5,5,0,0,1,5,5" +
+ "v65a5,5,0,0,1-5,5zM14,22.5l11-11a10,3,0,0,1,10-2h40a10,3,0,0,1," +
+ "10,2l11,11zM16,35a5,5,0,0,1,10,0a5,5,0,0,1-10,0z",
+
+ switch: "M10,20a10,10,0,0,1,10-10h70a10,10,0,0,1,10,10v70a10,10," +
+ "0,0,1-10,10h-70a10,10,0,0,1-10-10zM60,26l12,0,0-8,18,13-18,13,0" +
+ "-8-12,0zM60,60l12,0,0-8,18,13-18,13,0-8-12,0zM50,40l-12,0,0-8" +
+ "-18,13,18,13,0-8,12,0zM50,74l-12,0,0-8-18,13,18,13,0-8,12,0z",
+
+ roadm: "M10,35l25-25h40l25,25v40l-25,25h-40l-25-25zM58,26l12,0,0" +
+ "-8,18,13-18,13,0-8-12,0zM58,60l12,0,0-8,18,13-18,13,0-8-12,0z" +
+ "M52,40l-12,0,0-8-18,13,18,13,0-8,12,0zM52,74l-12,0,0-8-18,13," +
+ "18,13,0-8,12,0z",
+
+ endstation: "M10,15a5,5,0,0,1,5-5h65a5,5,0,0,1,5,5v80a5,5,0,0,1" +
+ "-5,5h-65a5,5,0,0,1-5-5zM87.5,14l11,11a3,10,0,0,1,2,10v40a3,10," +
+ "0,0,1,-2,10l-11,11zM17,19a2,2,0,0,1,2-2h56a2,2,0,0,1,2,2v26a2," +
+ "2,0,0,1-2,2h-56a2,2,0,0,1-2-2zM20,20h54v10h-54zM20,33h54v10h" +
+ "-54zM42,70a5,5,0,0,1,10,0a5,5,0,0,1-10,0z",
+
+ router: "M10,55A45,45,0,0,1,100,55A45,45,0,0,1,10,55M20,50l12,0," +
+ "0-8,18,13-18,13,0-8-12,0zM90,50l-12,0,0-8-18,13,18,13,0-8,12,0z" +
+ "M50,47l0-12-8,0,13-18,13,18-8,0,0,12zM50,63l0,12-8,0,13,18,13" +
+ "-18-8,0,0-12z",
+
+ bgpSpeaker: "M10,40a45,35,0,0,1,90,0Q100,77,55,100Q10,77,10,40z" +
+ "M50,29l12,0,0-8,18,13-18,13,0-8-12,0zM60,57l-12,0,0-8-18,13," +
+ "18,13,0-8,12,0z",
+
+ chain: "M60.4,77.6c-4.9,5.2-9.6,11.3-15.3,16.3c-8.6,7.5-20.4,6.8" +
+ "-28-0.8c-7.7-7.7-8.4-19.6-0.8-28.4c6.5-7.4,13.5-14.4,20.9-20.9" +
+ "c7.5-6.7,19.2-6.7,26.5-0.8c3.5,2.8,4.4,6.1,2.2,8.7c-2.7,3.1" +
+ "-5.5,2.5-8.5,0.3c-4.7-3.4-9.7-3.2-14,0.9C37.1,58.7,31,64.8," +
+ "25.2,71c-4.2,4.4-4.2,10.6-0.6,14.3c3.7,3.7,9.7,3.7,14.3-0.4" +
+ "c2.9-2.5,5.3-5.5,8.3-8c1-0.9,3-1.1,4.4-0.9C54.8,76.3,57.9,77.1," +
+ "60.4,77.6zM49.2,32.2c5-5.2,9.7-10.9,15.2-15.7c12.8-11,31.2" +
+ "-4.9,34.3,11.2C100,34.2,98.3,40.2,94,45c-6.7,7.4-13.7,14.6" +
+ "-21.2,21.2C65.1,73,53.2,72.7,46,66.5c-3.2-2.8-3.9-5.8-1.6-8.4" +
+ "c2.6-2.9,5.3-2.4,8.2-0.3c5.2,3.7,10,3.3,14.7-1.1c5.8-5.6,11.6" +
+ "-11.3,17.2-17.2c4.6-4.8,4.9-11.1,0.9-15c-3.9-3.9-10.1-3.4-15," +
+ "1.2c-3.1,2.9-5.7,7.4-9.3,8.5C57.6,35.3,53,33,49.2,32.2z",
+
+ crown: "M99.5,21.6c0,3-2.3,5.4-5.1,5.4c-0.3,0-0.7,0-1-0.1c-1.8," +
+ "4-4.8,10-7.2,17.3c-3.4,10.6-0.9,26.2,2.7,27.3C90.4,72,91.3," +
+ "75,88,75H22.7c-3.3,0-2.4-3-0.9-3.5c3.6-1.1,6.1-16.7,2.7-27.3" +
+ "c-2.4-7.4-5.4-13.5-7.2-17.5c-0.5,0.2-1,0.3-1.6,0.3c-2.8,0" +
+ "-5.1-2.4-5.1-5.4c0-3,2.3-5.4,5.1-5.4c2.8,0,5.1,2.4,5.1,5.4c0," +
+ "1-0.2,1.9-0.7,2.7c0.7,0.8,1.4,1.6,2.4,2.6c8.8,8.9,11.9,12.7," +
+ "18.1,11.7c6.5-1,11-8.2,13.3-14.1c-2-0.8-3.3-2.7-3.3-5.1c0-3," +
+ "2.3-5.4,5.1-5.4c2.8,0,5.1,2.4,5.1,5.4c0,2.5-1.6,4.5-3.7,5.2" +
+ "c2.3,5.9,6.8,13,13.2,14c6.2,1,9.3-2.7,18.1-11.7c0.7-0.7,1.4" +
+ "-1.5,2-2.1c-0.6-0.9-1-2-1-3.1c0-3,2.3-5.4,5.1-5.4C97.2,16.2," +
+ "99.5,18.6,99.5,21.6zM92,87.9c0,2.2-1.7,4.1-3.8,4.1H22.4c" +
+ "-2.1,0-4.4-1.9-4.4-4.1v-3.3c0-2.2,2.3-4.5,4.4-4.5h65.8c2.1," +
+ "0,3.8,2.3,3.8,4.5V87.9z",
+
+ lock: "M79.4,48.6h-2.7c0.2-5.7-0.2-20.4-7.9-28.8c-3.6-3.9-8.3" +
+ "-5.9-13.7-5.9c-5.4,0-10.2,2-13.8,5.9c-7.8,8.4-8.3,23.2-8.1,28.8" +
+ "h-2.7c-4.4,0-8,2.6-8,5.9v35.7c0,3.3,3.6,5.9,8,5.9h48.9c4.4,0," +
+ "8-2.6,8-5.9V54.5C87.5,51.3,83.9,48.6,79.4,48.6z M48.1,26.1c1.9" +
+ "-2,4.1-2.9,7-2.9c2.9,0,5.1,0.9,6.9,2.9c5,5.4,5.6,17.1,5.4,22.6" +
+ "h-25C42.3,43.1,43.1,31.5,48.1,26.1z",
+
+ topo: 'M97.2,76.3H86.6l-7.7-21.9H82c1,0,1.9-0.8,1.9-1.9V35.7c' +
+ '0-1-0.8-1.9-1.9-1.9H65.2c-1,0-1.9,0.8-1.9,1.9v2.6L33.4,26.1v-11' +
+ 'c0-1-0.8-1.9-1.9-1.9H14.7c-1,0-1.9,0.8-1.9,1.9v16.8c0,1,0.8,' +
+ '1.9,1.9,1.9h16.8c1,0,1.9-0.8,1.9-1.9v-2.6l29.9,12.2v9L30.5,76.9' +
+ 'c-0.3-0.3-0.8-0.5-1.3-0.5H12.4c-1,0-1.9,0.8-1.9,1.9V95c0,1,0.8,' +
+ '1.9,1.9,1.9h16.8c1,0,1.9-0.8,1.9-1.9v-6.9h47.4V95c0,1,0.8,1.9,' +
+ '1.9,1.9h16.8c1,0,1.9-0.8,1.9-1.9V78.2C99.1,77.2,98.2,76.3,97.2,' +
+ '76.3z M31.1,85.1v-4.9l32.8-26.4c0.3,0.3,0.8,0.5,1.3,0.5h10.5l' +
+ '7.7,21.9h-3c-1,0-1.9,0.8-1.9,1.9v6.9H31.1z',
+
+ refresh: 'M102.6,40.8L88.4,70.5L69.8,43.4L80,42.6c-0.7-2.3-1.7-' +
+ '5.1-3.4-7.8C71.8,27,64,23.1,53.5,23.1c-19.5,0-24.8,11.2-24.8,' +
+ '11.3l-10.1-4.3c0.3-0.7,7.9-18,35-18c24.8,0,35,17.3,37.7,29.6L' +
+ '102.6,40.8z M81.5,74.5c-0.2,0.5-5.5,11.4-24.9,11.4c-10.5,0-18.3' +
+ '-3.9-23.1-11.7c-1.7-2.8-2.8-5.6-3.4-7.8l10.2-0.8L21.7,38.5L7.5,' +
+ '68.2l11.4-0.9c2.7,12.3,12.9,29.6,37.7,29.6c26.9,0,34.6-17.2,34.9' +
+ '-18L81.5,74.5z',
+
+ garbage: 'M94.6,20.2c0,2.7-2.1,4.8-4.8,4.8H19.2c-2.7,0-4.8-2.1-' +
+ '4.8-4.8s2.1-4.8,4.8-4.8h27.6c-0.8-0.7-1.4-1.7-1.4-2.9c0-2.1,1.7-' +
+ '3.9,3.9-3.9h10.4c2.1,0,3.9,1.7,3.9,3.9c0,1.2-0.5,2.2-1.4,2.9h' +
+ '27.6C92.5,15.4,94.6,17.6,94.6,20.2z M91,33.4v64.8c0,2-1.7,3.6-' +
+ '3.8,3.6h-65c-2.1,0-3.8-1.6-3.8-3.6V33.4c0-2,1.7-3.6,3.8-3.6h65C' +
+ '89.3,29.8,91,31.4,91,33.4z M31.5,37.7c0-2.1-1.2-3.8-2.7-3.8h-0.7' +
+ 'c-1.5,0-2.7,1.7-2.7,3.8v55.9c0,2.1,1.2,3.8,2.7,3.8h0.7c1.5,0,2.7' +
+ '-1.7,2.7-3.8V37.7z M58.5,37.7c0-2.1-1.8-3.8-4-3.8h-1c-2.2,0-4,' +
+ '1.7-4,3.8v55.9c0,2.1,1.8,3.8,4,3.8h1c2.2,0,4-1.7,4-3.8V37.7z M' +
+ '83.5,37.7c0-2.1-1.2-3.8-2.7-3.8h-0.7c-1.5,0-2.7,1.7-2.7,3.8v55.9' +
+ 'c0,2.1,1.2,3.8,2.7,3.8h0.7c1.5,0,2.7-1.7,2.7-3.8V37.7z',
+
+ loading: 'M103.1,53.1c0,0,0,0.2,0,0.5c0,0.2,0,0.4,0,0.7c0,0.3,0,0' +
+ '.6,0,0.9c-0.1,1.3-0.2,3-0.5,4.8c-0.5,3.4-1.6,6.8-1.6,6.8l-9.2-2.' +
+ '7c0,0,0.8-2.7,1.1-5.5c0.2-1.4,0.3-2.8,0.3-3.8c0-0.3,0-0.5,0-0.7c' +
+ '0-0.2,0-0.4,0-0.6c0-0.3,0-0.5,0-0.5L103.1,53.1z M87.3,74.3c0,0-0' +
+ '.1,0.2-0.3,0.5c-0.2,0.3-0.4,0.6-0.7,1.1c-0.6,0.9-1.4,2-2.3,3.1c-' +
+ '1.8,2.2-3.9,4.1-3.9,4.1l5.7,6.5c0,0,0.7-0.5,1.6-1.4c1-0.9,2.2-2.' +
+ '1,3.3-3.4c1.1-1.3,2.2-2.6,3-3.7c0.4-0.5,0.7-1,0.9-1.3c0.2-0.3,0.' +
+ '3-0.4,0.3-0.4L87.3,74.3z M70.8,89.2c0,0-0.2,0.1-0.5,0.2c-0.3,0.2' +
+ '-0.6,0.3-1.2,0.5c-1,0.4-2.3,0.9-3.7,1.4c-2.7,0.9-5.5,1.3-5.5,1.3' +
+ 'l1.1,7.6c0,0,0.8-0.1,2.1-0.3c1.3-0.2,2.9-0.6,4.6-1c1.6-0.5,3.3-1' +
+ ',4.5-1.5c0.6-0.2,1.2-0.5,1.5-0.6c0.3-0.1,0.5-0.2,0.5-0.2L70.8,89' +
+ '.2z M48.6,92.9c0,0-0.2,0-0.5-0.1c-0.4,0-0.7-0.1-1.3-0.2c-1.1-0.2' +
+ '-2.5-0.5-3.9-0.8c-2.8-0.7-5.4-1.9-5.4-1.9L34.6,96c0,0,3,1.5,6.3,' +
+ '2.5c1.6,0.5,3.3,0.9,4.5,1.2c0.6,0.1,1.2,0.2,1.5,0.3c0.3,0.1,0.5,' +
+ '0.1,0.5,0.1L48.6,92.9z M27.6,83.8c0,0-0.1-0.1-0.4-0.3c-0.3-0.2-0' +
+ '.6-0.5-1-0.9c-0.8-0.7-1.8-1.8-2.8-2.8c-2-2.2-3.6-4.6-3.6-4.6l-5,' +
+ '3.2c0,0,0.4,0.7,1.1,1.7c0.7,1,1.7,2.4,2.8,3.7c1.1,1.3,2.2,2.5,3.' +
+ '1,3.4c0.4,0.4,0.9,0.8,1.1,1.1c0.3,0.2,0.4,0.4,0.4,0.4L27.6,83.8z' +
+ 'M14.8,64.7c0,0-0.1-0.2-0.2-0.5c-0.1-0.3-0.2-0.7-0.4-1.3c-0.3-1.1' +
+ '-0.6-2.5-0.8-4c-0.5-2.9-0.5-5.9-0.5-5.9l-5,0c0,0,0,0.8,0,2.1c0,1' +
+ '.2,0.1,2.9,0.3,4.5c0.2,1.6,0.5,3.3,0.8,4.5c0.1,0.6,0.3,1.2,0.4,1' +
+ '.5c0.1,0.3,0.1,0.5,0.1,0.5L14.8,64.7z M14.3,41.4c0,0,0.1-0.2,0.1' +
+ '-0.5c0.1-0.3,0.2-0.7,0.4-1.3c0.3-1.1,0.8-2.5,1.4-3.8c1.2-2.7,2.8' +
+ '-5.3,2.8-5.3l-3.4-2.2c0,0-1.8,2.7-3.2,5.7c-0.7,1.5-1.3,3-1.7,4.2' +
+ 'c-0.2,0.6-0.4,1.1-0.5,1.4C10,39.9,10,40.1,10,40.1L14.3,41.4z M26' +
+ '.7,21.3c0,0,0.1-0.1,0.4-0.4c0.3-0.2,0.6-0.5,1-0.9c0.9-0.7,2.1-1.' +
+ '6,3.3-2.5c1.2-0.8,2.5-1.6,3.5-2.1c1-0.5,1.7-0.9,1.7-0.9l-1.3-2.9' +
+ 'c0,0-0.7,0.3-1.8,0.9c-1.1,0.5-2.5,1.3-3.9,2.2c-1.4,0.9-2.7,1.8-3' +
+ '.7,2.5c-0.5,0.4-0.9,0.7-1.2,0.9c-0.3,0.2-0.4,0.3-0.4,0.3L26.7,21' +
+ '.3z M48.2,11c0,0,0.2,0,0.5-0.1c0.3,0,0.8-0.1,1.4-0.2c1.1-0.1,2.6' +
+ '-0.3,4.2-0.3c3-0.1,6.1,0.3,6.1,0.3l0.3-2.3c0,0-0.8-0.1-2-0.3C57.' +
+ '4,8.1,55.8,8,54.2,8c-1.6,0-3.2,0-4.4,0.1c-0.6,0-1.1,0.1-1.5,0.1c' +
+ '-0.3,0-0.5,0.1-0.5,0.1L48.2,11z M72,14c0,0,0.7,0.3,1.7,0.8c1,0.5' +
+ ',2.4,1.2,3.7,2c2.6,1.6,5,3.6,5,3.6l0.9-1c0,0-2.4-2.1-5-3.9c-1.3-' +
+ '0.9-2.7-1.7-3.8-2.3c-1.1-0.6-1.8-0.9-1.8-0.9L72,14zM90.7,29.6c0,' +
+ '0,0.4,0.6,1,1.6c0.6,1,1.4,2.3,2,3.7c0.7,1.4,1.3,2.8,1.7,3.9c0.4,' +
+ '1.1,0.6,1.8,0.6,1.8l0.4-0.1c0,0-0.2-0.8-0.6-1.9c-0.4-1.1-0.9-2.6' +
+ '-1.5-4c-0.6-1.4-1.3-2.9-1.9-3.9c-0.6-1-1-1.7-1-1.7L90.7,29.6z',
+
+ // --- Navigation glyphs ------------------------------------
+
+ flowTable: 'M15.9,19.1h-8v-13h8V19.1z M90.5,6.1H75.6v13h14.9V6.1z' +
+ ' M71.9,6.1H56.9v13h14.9V6.1z M53.2,6.1H38.3v13h14.9V6.1z M34.5,' +
+ '6.1H19.6v13h14.9V6.1z M102.2,6.1h-8v13h8V6.1z M102.2,23.6H7.9v' +
+ '78.5h94.4V23.6z M86,63.2c0,3.3-2.7,6-6,6c-2.8,0-5.1-1.9-5.8-' +
+ '4.5H63.3v5.1c0,0.9-0.7,1.5-1.5,1.5h-5.2v10.6c2.6,0.7,4.5,3,4.5,' +
+ '5.8c0,3.3-2.7,6-6,6c-3.3,0-6-2.7-6-6c0-2.8,1.9-5.1,4.4-5.8V71.3' +
+ 'H48c-0.9,0-1.5-0.7-1.5-1.5v-5.1H36c-0.7,2.6-3,4.4-5.8,4.4c-3.3,' +
+ '0-6-2.7-6-6s2.7-6,6-6c2.8,0,5.2,1.9,5.8,4.5h10.5V56c0-0.9,0.7-' +
+ '1.5,1.5-1.5h5.5V43.8c-2.6-0.7-4.5-3-4.5-5.8c0-3.3,2.7-6,6-6s6,' +
+ '2.7,6,6c0,2.8-1.9,5.1-4.5,5.8v10.6h5.2c0.9,0,1.5,0.7,1.5,1.5v' +
+ '5.6h10.8c0.7-2.6,3-4.5,5.8-4.5C83.3,57.1,86,59.8,86,63.2z M55.1,' +
+ '42.3c2.3,0,4.3-1.9,4.3-4.3c0-2.3-1.9-4.3-4.3-4.3s-4.3,1.9-4.3,' +
+ '4.3C50.8,40.4,52.7,42.3,55.1,42.3z M34.4,63.1c0-2.3-1.9-4.3-4.3' +
+ '-4.3s-4.3,1.9-4.3,4.3s1.9,4.3,4.3,4.3S34.4,65.5,34.4,63.1z ' +
+ 'M55.1,83.5c-2.3,0-4.3,1.9-4.3,4.3s1.9,4.3,4.3,4.3s4.3-1.9,4.3-' +
+ '4.3S57.5,83.5,55.1,83.5zM84.2,63.2c0-2.3-1.9-4.3-4.3-4.3s-4.3,' +
+ '1.9-4.3,4.3s1.9,4.3,4.3,4.3S84.2,65.5,84.2,63.2z',
+
+ portTable: 'M15.9,19.1h-8v-13h8V19.1z M90.5,6.1H75.6v13h14.9V6.1' +
+ 'z M71.9,6.1H56.9v13h14.9V6.1z M53.2,6.1H38.3v13h14.9V6.1z M34.5,' +
+ '6.1H19.6v13h14.9V6.1z M102.2,6.1h-8v13h8V6.1z M102.6,23.6v78.5H' +
+ '8.2V23.6H102.6z M85.5,37.7c0-0.7-0.4-1.3-0.9-1.3H26.2c-0.5,0-' +
+ '0.9,0.6-0.9,1.3v34.6c0,0.7,0.4,1.3,0.9,1.3h11v9.6c0,1.1,0.5,2,' +
+ '1.2,2h9.1c0,0.2,0,0.3,0,0.5v3c0,1.1,0.5,2,1.2,2h13.5c0.6,0,1.2-' +
+ '0.9,1.2-2v-3c0-0.2,0-0.3,0-0.5h9.1c0.6,0,1.2-0.9,1.2-2v-9.6h11' +
+ 'c0.5,0,0.9-0.6,0.9-1.3V37.7z M30.2,40h-1v8h1V40zM75.2,40h-2.1v8' +
+ 'h2.1V40z M67.7,40h-2.1v8h2.1V40z M60.2,40h-2.1v8h2.1V40z M52.7,' +
+ '40h-2.1v8h2.1V40z M45.2,40h-2.1v8h2.1V40zM37.7,40h-2.1v8h2.1V40' +
+ 'z M81.6,40h-1v8h1V40z',
+
+ groupTable: 'M16,19.1H8v-13h8V19.1z M90.6,6.1H75.7v13h14.9V6.1z ' +
+ 'M71.9,6.1H57v13h14.9V6.1z M53.3,6.1H38.4v13h14.9V6.1z M34.6,6.1' +
+ 'H19.7v13h14.9V6.1z M102.3,6.1h-8v13h8V6.1z M45.7,52.7c0.2-5.6,' +
+ '2.6-10.7,6.2-14.4c-2.6-1.5-5.7-2.5-8.9-2.5c-9.8,0-17.7,7.9-17.7,' +
+ '17.7c0,6.3,3.3,11.9,8.3,15C34.8,61.5,39.4,55.6,45.7,52.7z M51.9,' +
+ '68.8c-3.1-3.1-5.2-7.2-6-11.7c-4.7,2.8-7.9,7.6-8.6,13.2c1.8,0.6,' +
+ '3.6,0.9,5.6,0.9C46.2,71.2,49.3,70.3,51.9,68.8z M55.2,71.5c-3.5,' +
+ '2.4-7.7,3.7-12.2,3.7c-1.9,0-3.8-0.3-5.6-0.7C38.5,83.2,45.9,90,' +
+ '54.9,90c9,0,16.4-6.7,17.5-15.4c-1.6,0.4-3.4,0.6-5.1,0.6C62.8,' +
+ '75.2,58.6,73.8,55.2,71.5z M54.9,50.6c1.9,0,3.8,0.3,5.6,0.7c-0.5' +
+ '-4.1-2.5-7.9-5.4-10.6c-2.9,2.7-4.8,6.4-5.3,10.5C51.5,50.8,53.2,' +
+ '50.6,54.9,50.6z M49.7,55.4c0.5,4.3,2.4,8.1,5.4,10.9c2.9-2.8,4.9' +
+ '-6.6,5.4-10.8c-1.8-0.6-3.6-0.9-5.6-0.9C53.1,54.6,51.4,54.9,49.7,' +
+ '55.4z M102.3,23.6v78.5H8V23.6H102.3z M89,53.5c0-12-9.7-21.7-' +
+ '21.7-21.7c-4.5,0-8.7,1.4-12.2,3.7c-3.5-2.4-7.7-3.7-12.2-3.7c-12,' +
+ '0-21.7,9.7-21.7,21.7c0,8.5,4.9,15.9,12,19.4C33.6,84.6,43.2,94,' +
+ '54.9,94c11.7,0,21.2-9.3,21.7-20.9C84,69.7,89,62.2,89,53.5z M' +
+ '64.3,57.3c-0.8,4.4-2.9,8.4-5.9,11.5c2.6,1.5,5.7,2.5,8.9,2.5c1.8,' +
+ '0,3.6-0.3,5.2-0.8C72,64.9,68.8,60.1,64.3,57.3z M67.3,35.8c-3.3,0' +
+ '-6.3,0.9-8.9,2.5c3.7,3.8,6.1,8.9,6.2,14.6c6.1,3.1,10.6,8.9,11.7,' +
+ '15.8C81.5,65.6,85,60,85,53.5C85,43.8,77.1,35.8,67.3,35.8z',
+
+ // --- Topology toolbar specific glyphs ----------------------
+
+ summary: "M95.8,9.2H14.2c-2.8,0-5,2.2-5,5v81.5c0,2.8,2.2,5,5," +
+ "5h81.5c2.8,0,5-2.2,5-5V14.2C100.8,11.5,98.5,9.2,95.8,9.2z " +
+ "M16.7,22.2c0-1.1,0.9-2,2-2h20.1c1.1,0,2,0.9,2,2v20.1c0,1.1-0.9," +
+ "2-2,2H18.7c-1.1,0-2-0.9-2-2V22.2z M93,87c0,1.1-0.9,2-2,2H18.9" +
+ "c-1.1,0-2-0.9-2-2v-7c0-1.1,0.9-2,2-2H91c1.1,0,2,0.9,2,2V87z " +
+ "M93,65c0,1.1-0.9,2-2,2H18.9c-1.1,0-2-0.9-2-2v-7c0-1.1,0.9-2," +
+ "2-2H91c1.1,0,2,0.9,2,2V65z",
+
+ details: "M95.8,9.2H14.2c-2.8,0-5,2.2-5,5v81.5c0,2.8,2.2,5,5," +
+ "5h81.5c2.8,0,5-2.2,5-5V14.2C100.8,11.5,98.5,9.2,95.8,9.2z M16.9," +
+ "22.2c0-1.1,0.9-2,2-2H91c1.1,0,2,0.9,2,2v7c0,1.1-0.9,2-2,2H18.9c" +
+ "-1.1,0-2-0.9-2-2V22.2z M93,87.8c0,1.1-0.9,2-2,2H18.9c-1.1," +
+ "0-2-0.9-2-2v-7c0-1.1,0.9-2,2-2H91c1.1,0,2,0.9,2,2V87.8z M93,68.2" +
+ "c0,1.1-0.9,2-2,2H18.9c-1.1,0-2-0.9-2-2v-7c0-1.1,0.9-2,2-2H91" +
+ "c1.1,0,2,0.9,2,2V68.2z M93,48.8c0,1.1-0.9,2-2,2H19c-1.1,0-2-" +
+ "0.9-2-2v-7c0-1.1,0.9-2,2-2H91c1.1,0,2,0.9,2,2V48.8z",
+
+ ports: "M98,9.2H79.6c-1.1,0-2.1,0.9-2.1,2.1v17.6l-5.4,5.4c-1.7" +
+ "-1.1-3.8-1.8-6-1.8c-6,0-10.9,4.9-10.9,10.9c0,2.2,0.7,4.3,1.8,6" +
+ "l-7.5,7.5c-1.8-1.2-3.9-1.9-6.2-1.9c-6,0-10.9,4.9-10.9,10.9c0," +
+ "2.3,0.7,4.4,1.9,6.2l-6.2,6.2H11.3c-1.1,0-2.1,0.9-2.1,2.1v18.4" +
+ "c0,1.1,0.9,2.1,2.1,2.1h18.4c1.1,0,2.1-0.9,2.1-2.1v-16l7-6.9" +
+ "c1.4,0.7,3,1.1,4.7,1.1c6,0,10.9-4.9,10.9-10.9c0-1.7-0.4-3.3-" +
+ "1.1-4.7l8-8c1.5,0.7,3.1,1.1,4.8,1.1c6,0,10.9-4.9,10.9-10.9c0" +
+ "-1.7-0.4-3.4-1.1-4.8l6.9-6.9H98c1.1,0,2.1-0.9,2.1-2.1V11.3" +
+ "C100.1,10.2,99.2,9.2,98,9.2z M43.4,72c-3.3,0-6-2.7-6-6s2.7-6," +
+ "6-6s6,2.7,6,6S46.7,72,43.4,72z M66.1,49.5c-3.3,0-6-2.7-6-6" +
+ "c0-3.3,2.7-6,6-6s6,2.7,6,6C72.2,46.8,69.5,49.5,66.1,49.5z",
+
+ map: "M95.8,9.2H14.2c-2.8,0-5,2.2-5,5v66c0.3-1.4,0.7-2.8," +
+ "1.1-4.1l1.6,0.5c-0.9,2.4-1.6,4.8-2.2,7.3l-0.5-0.1v12c0,2.8,2.2," +
+ "5,5,5h81.5c2.8,0,5-2.2,5-5V14.2C100.8,11.5,98.5,9.2,95.8,9.2z " +
+ "M16.5,67.5c-0.4,0.5-0.7,1-1,1.5c-0.3,0.5-0.6,1-0.9,1.6l-1.9-0.9" +
+ "c0.3-0.6,0.6-1.2,0.9-1.8c0.3-0.6,0.6-1.2,1-1.7c0.7-1.1,1.5-2.2," +
+ "2.5-3.2l1.8,1.8C18,65.6,17.2,66.5,16.5,67.5z M29.7,64.1" +
+ "c-0.4-0.4-0.8-0.8-1.2-1.1c-0.1-0.1-0.2-0.1-0.2-0.1c0,0-0.1," +
+ "0-0.1-0.1l-0.1,0l0,0l-0.1,0c-0.3-0.1-0.5-0.2-0.8-0.2c-0.5-0.1" +
+ "-1.1-0.2-1.6-0.3c-0.6,0-1.1,0-1.6,0l-0.4-2.8c0.7-0.1,1.5-0.2,2.2" +
+ "-0.1c0.7,0,1.4,0.1,2.2,0.3c0.4,0.1,0.7,0.2,1,0.3l0.1,0l0,0l0.1," +
+ "0l0.1,0c0.1,0,0.1,0,0.3,0.1c0.3,0.1,0.5,0.2,0.7,0.4c0.7,0.5," +
+ "1.2,0.9,1.7,1.4L29.7,64.1z M39.4,74.7c-1.8-1.8-3.6-3.8-5.3-5.7" +
+ "l2.6-2.4c0.9,0.9,1.8,1.8,2.7,2.7c0.9,0.9,1.8,1.7,2.7,2.6L39.4," +
+ "74.7z M50.8,84.2c-1.1-0.7-2.2-1.5-3.3-2.3c-0.5-0.4-1.1-0.8-1.6" +
+ "-1.2c-0.5-0.4-1-0.8-1.5-1.2l2.7-3.4c0.5,0.4,1,0.8,1.5,1.1c0.5," +
+ "0.3,1,0.7,1.5,1c1,0.7,2.1,1.3,3.1,1.9L50.8,84.2z M61.3," +
+ "88.7c-0.7-0.1-1.4-0.3-2.1-0.5c-0.7-0.2-1.4-0.5-2-0.7l1.8" +
+ "-4.8c0.6,0.2,1.1,0.4,1.6,0.5c0.5,0.2,1.1,0.3,1.6,0.4c1,0.2,2.1," +
+ "0.2,3,0.1l0.7,5.1C64.3,89.1,62.7,88.9,61.3,88.7z M75.1,80.4c" +
+ "-0.2,0.7-0.5,1.4-0.9,2c-0.2,0.3-0.3,0.7-0.5,1l-0.3,0.5l-0.3," +
+ "0.4l-3.9-2.8l0.3-0.4l0.2-0.3c0.1-0.2,0.3-0.4,0.4-0.7c0.3-0.5," +
+ "0.5-0.9,0.7-1.5c0.4-1,0.8-2.1,1.1-3.3l4.2,0.9C75.9,77.7,75.6," +
+ "79,75.1,80.4z M73,69.2l0.2-1.9l0.1-1.9c0.1-1.2,0.1-2.5,0.1-" +
+ "3.8l2.5-0.2c0.2,1.3,0.4,2.6,0.5,3.9l0.1,2l0.1,2L73,69.2z " +
+ "M73,51l0.5-0.1c0.4,1.3,0.8,2.6,1.1,3.9L73.2,55C73.1,53.7,73.1," +
+ "52.3,73,51z M91.9,20.4c-0.7,1.4-3.6,3.6-4.2,3.9c-1.5,0.8-5," +
+ "2.8-10.1,7.7c3,2.9,5.8,5.4,7.3,6.4c2.6,1.8,3.4,4.3,3.6,6.1c0.1," +
+ "1.1-0.1,2.5-0.4,3c-0.5,0.9-1.6,2-3,1.4c-2-0.8-11.5-9.6-13-11c" +
+ "-3.5,3.9-7.4,8.9-11.7,15.1c0,0-3.1,3.4-5.2,0.9C52.9,51.5,61," +
+ "39.3,61,39.3s2.2-3.1,5.6-7c-2.9-3-5.9-6.3-6.6-7.3c0,0-3.7-5-1.3" +
+ "-6.6c3.2-2.1,6.3,0.8,6.3,0.8s3.1,3.3,7,7.2c4.7-4.7,10.1-9.2," +
+ "14.7-10c0,0,3.3-1,5.2,1.7C92.5,18.8,92.4,19.6,91.9,20.4z",
+
+ cycleLabels: "M72.5,33.9c0,0.6-0.2,1-0.5,1H40c-0.3,0-0.5-0.4" +
+ "-0.5-1V20.7c0-0.6,0.2-1,0.5-1h32c0.3,0,0.5,0.4,0.5,1V33.9z " +
+ "M41.2,61.8c0-0.6-0.2-1-0.5-1h-32c-0.3,0-0.5,0.4-0.5,1V75c0,0.6," +
+ "0.2,1,0.5,1h32c0.3,0,0.5-0.4,0.5-1V61.8z M101.8,61.8c0-0.6-0.2" +
+ "-1-0.5-1h-32c-0.3,0-0.5,0.4-0.5,1V75c0,0.6,0.2,1,0.5,1h32c0.3," +
+ "0,0.5-0.4,0.5-1V61.8z M17.2,52.9c0-0.1-0.3-7.1,4.6-13.6l-2.4-1.8" +
+ "c-5.4,7.3-5.2,15.2-5.1,15.5L17.2,52.9z M12.7,36.8l7.4,2.5l1.5," +
+ "7.6L29.5,31L12.7,36.8z M94.2,42.3c-2.1-8.9-8.3-13.7-8.6-13.9l" +
+ "-1.8,2.4c0.1,0,5.6,4.3,7.5,12.2L94.2,42.3z M99,37.8l-6.6,4.1l" +
+ "-6.8-3.7l7.1,16.2L99,37.8z M69,90.2l-1.2-2.8c-0.1,0-6.6,2.8" +
+ "-14.3,0.6l-0.8,2.9c2.5,0.7,4.9,1,7,1C65,91.8,68.7,90.2,69,90.2z " +
+ "M54.3,97.3L54,89.5l6.6-4.1l-17.6-1.7L54.3,97.3z",
+
+ oblique: "M80.9,30.2h4.3l15-16.9H24.8l-15,16.9h19v48.5h-4l-15," +
+ "16.9h75.3l15-16.9H80.9V30.2z M78.6,78.7H56.1V30.2h22.5V78.7z" +
+ "M79.7,17.4c2.4,0,4.3,1.9,4.3,4.3c0,2.4-1.9,4.3-4.3,4.3s-4.3" +
+ "-1.9-4.3-4.3C75.4,19.3,77.4,17.4,79.7,17.4z M55,17.4c2.4,0," +
+ "4.3,1.9,4.3,4.3c0,2.4-1.9,4.3-4.3,4.3s-4.3-1.9-4.3-4.3C50.7," +
+ "19.3,52.6,17.4,55,17.4z M26.1,21.7c0-2.4,1.9-4.3,4.3-4.3c2.4," +
+ "0,4.3,1.9,4.3,4.3c0,2.4-1.9,4.3-4.3,4.3C28,26,26.1,24.1,26.1," +
+ "21.7z M31.1,30.2h22.6v48.5H31.1V30.2z M30.3,91.4c-2.4,0-4.3" +
+ "-1.9-4.3-4.3c0-2.4,1.9-4.3,4.3-4.3c2.4,0,4.3,1.9,4.3,4.3C34.6," +
+ "89.5,32.7,91.4,30.3,91.4z M54.9,91.4c-2.4,0-4.3-1.9-4.3-4.3c0" +
+ "-2.4,1.9-4.3,4.3-4.3c2.4,0,4.3,1.9,4.3,4.3C59.2,89.5,57.3," +
+ "91.4,54.9,91.4z M84,87.1c0,2.4-1.9,4.3-4.3,4.3c-2.4,0-4.3-1.9" +
+ "-4.3-4.3c0-2.4,1.9-4.3,4.3-4.3C82.1,82.8,84,84.7,84,87.1z",
+
+ filters: "M24.8,13.3L9.8,40.5h75.3l15.0-27.2H24.8z M72.8,32.1l-" +
+ "9.7-8.9l-19.3,8.9l-6.0-7.4L24.1,30.9l-1.2-2.7l15.7-7.1l6.0,7.4" +
+ "l19.0-8.8l9.7,8.8l11.5-5.6l1.3,2.7L72.8,32.1zM24.3,68.3L9.3," +
+ "95.5h75.3l15.0-27.2H24.3z M84.3,85.9L70.7,79.8l-6.0,7.4l-19.3" +
+ "-8.9l-9.7,8.9l-13.3-6.5l1.3-2.7l11.5,5.6l9.7-8.8l19.0,8.8l6.0" +
+ "-7.4l15.7,7.1L84.3,85.9z M15.3,57h-6v-4h6V57zM88.1,57H76.0v-4h" +
+ "12.1V57z M69.9,57H57.8v-4h12.1V57z M51.7,57H39.6v-4H51.7V57z " +
+ "M33.5,57H21.4v-4h12.1V57zM100.2,57h-6v-4h6V57z",
+
+ resetZoom: "M86,79.8L61.7,54.3c1.8-2.9,2.8-6.3,2.9-10c0.3-11.2" +
+ "-8.6-20.5-19.8-20.8C33.7,23.2,24.3,32,24.1,43.2c-0.3,11.2,8.6," +
+ "20.5,19.8,20.8c4,0.1,8.9-0.8,11.9-3.6l23.7,25c1.5,1.6,4,2.3," +
+ "5.3,1l1.6-1.6C87.7,83.7,87.5,81.4,86,79.8z M31.4,43.4c0.2-7.1," +
+ "6.1-12.8,13.2-12.6C51.8,31,57.5,37,57.3,44.1c-0.2,7.1-6.1,12.8" +
+ "-13.2,12.6C36.9,56.5,31.3,50.6,31.4,43.4zM22.6,104H6V86.4c0" +
+ "-1.7,1.4-3.1,3.1-3.1s3.1,1.4,3.1,3.1v11.4h10.4c1.7,0,3.1,1.4," +
+ "3.1,3.1S24.3,104,22.6,104z M25.7,9.1c0,1.7-1.4,3.1-3.1,3.1" +
+ "H12.2v11.4c0,1.7-1.4,3.1-3.1,3.1S6,25.3,6,23.6V6h16.6C24.3,6," +
+ "25.7,7.4,25.7,9.1z M84.3,100.9c0-1.7,1.4-3.1,3.1-3.1h10.4V86.4" +
+ "c0-1.7,1.4-3.1,3.1-3.1s3.1,1.4,3.1,3.1V104H87.4C85.7,104,84.3," +
+ "102.6,84.3,100.9z M87.4,6H104v17.6c0,1.7-1.4,3.1-3.1,3.1s-3.1" +
+ "-1.4-3.1-3.1V12.2H87.4c-1.7,0-3.1-1.4-3.1-3.1S85.7,6,87.4,6z",
+
+ relatedIntents: "M99.9,43.7v22.6c0,1.9-1.5,3.4-3.4,3.4H73.9c" +
+ "-1.9,0-3.4-1.5-3.4-3.4V43.7c0-1.9,1.5-3.4,3.4-3.4h22.6C98.4," +
+ "40.3,99.9,41.8,99.9,43.7z M48.4,46.3l6.2,6.7h-4.6L38.5,38v9.7" +
+ "l4.7,5.3H10.1V57h33.2l-4.8,5.3v9.5L49.8,57h5.1v0l-6.5,7v11.5" +
+ "L64.1,55L48.4,34.4V46.3z",
+
+ nextIntent: "M88.1,55.7L34.6,13.1c0,0-1.6-0.5-2.1-0.2c-1.9,1.2" +
+ "-6.5,13.8-3.1,17.2c7,6.9,30.6,24.5,32.4,25.9c-1.8,1.4-25.4,19" +
+ "-32.4,25.9c-3.4,3.4,1.2,16,3.1,17.2c0.6,0.4,2.1-0.2,2.1-0.2" +
+ "s53.1-42.4,53.5-42.7C88.5,56,88.1,55.7,88.1,55.7z",
+
+ prevIntent: "M22.5,55.6L76,12.9c0,0,1.6-0.5,2.2-0.2c1.9,1.2," +
+ "6.5,13.8,3.1,17.2c-7,6.9-30.6,24.5-32.4,25.9c1.8,1.4,25.4,19," +
+ "32.4,25.9c3.4,3.4-1.2,16-3.1,17.2c-0.6,0.4-2.2-0.2-2.2-0.2" +
+ "S22.9,56.3,22.5,56C22.2,55.8,22.5,55.6,22.5,55.6z",
+
+ intentTraffic: "M14.7,71.5h-6v-33h6V71.5z M88.5,38.5H76.9v33" +
+ "h11.7V38.5z M70.1,38.5H58.4v33h11.7V38.5z M51.6,38.5H39.9v33" +
+ "h11.7V38.5z M33.1,38.5H21.5v33h11.7V38.5z M101.3,38.5h-6v33h6" +
+ "V38.5z",
+
+ allTraffic: "M15.7,64.5h-7v-19h7V64.5z M78.6,45.5H62.9v19h15.7" +
+ "V45.5z M47.1,45.5H31.4v19h15.7V45.5z M101.3,45.5h-7v19h7V45.5z" +
+ "M14.7,14.1h-6v19h6V14.1z M88.5,14.1H76.9v19h11.7V14.1z M70.1," +
+ "14.1H58.4v19h11.7V14.1z M51.6,14.1H39.9v19h11.7V14.1z M33.1,14.1" +
+ "H21.5v19h11.7V14.1z M101.3,14.1h-6v19h6V14.1z M14.7,76.9h-6v19" +
+ "h6V76.9z M88.5,76.9H76.9v19h11.7V76.9z M70.1,76.9H58.4v19h11.7" +
+ "V76.9z M51.6,76.9H39.9v19h11.7V76.9z M33.1,76.9H21.5v19h11.7" +
+ "V76.9z M101.3,76.9h-6v19h6V76.9z",
+
+ flows: "M93.8,46.1c-4.3,0-8,3-9,7H67.9v-8.8c0-1.3-1.1-2.4-2.4" +
+ "-2.4h-8.1V25.3c4-1,7-4.7,7-9.1c0-5.2-4.2-9.4-9.4-9.4c-5.2,0" +
+ "-9.4,4.2-9.4,9.4c0,4.3,3,8,7,9v16.5H44c-1.3,0-2.4,1.1-2.4,2.4" +
+ "v8.8H25.3c-1-4.1-4.7-7.1-9.1-7.1c-5.2,0-9.4,4.2-9.4,9.4s4.2," +
+ "9.4,9.4,9.4c4.3,0,8-2.9,9-6.9h16.4v7.9c0,1.3,1.1,2.4,2.4,2.4" +
+ "h8.6v16.6c-4,1.1-6.9,4.7-6.9,9c0,5.2,4.2,9.4,9.4,9.4c5.2,0," +
+ "9.4-4.2,9.4-9.4c0-4.4-3-8-7.1-9.1V68.2h8.1c1.3,0,2.4-1.1,2.4" +
+ "-2.4v-7.9h16.8c1.1,4,4.7,7,9,7c5.2,0,9.4-4.2,9.4-9.4S99,46.1," +
+ "93.8,46.1z M48.7,16.3c0-3.5,2.9-6.4,6.4-6.4c3.5,0,6.4,2.9,6.4," +
+ "6.4s-2.9,6.4-6.4,6.4C51.5,22.6,48.7,19.8,48.7,16.3zM16.2,61.7c" +
+ "-3.5,0-6.4-2.9-6.4-6.4c0-3.5,2.9-6.4,6.4-6.4s6.4,2.9,6.4,6.4" +
+ "C22.6,58.9,19.7,61.7,16.2,61.7z M61.4,93.7c0,3.5-2.9,6.4-6.4," +
+ "6.4c-3.5,0-6.4-2.9-6.4-6.4c0-3.5,2.9-6.4,6.4-6.4C58.6,87.4," +
+ "61.4,90.2,61.4,93.7z M93.8,61.8c-3.5,0-6.4-2.9-6.4-6.4c0-3.5," +
+ "2.9-6.4,6.4-6.4s6.4,2.9,6.4,6.4C100.1,58.9,97.3,61.8,93.8,61.8z",
+
+ eqMaster: "M100.1,46.9l-10.8-25h0.2c0.5,0,0.8-0.5,0.8-1.1v-3.2" +
+ "c0-0.6-0.4-1.1-0.8-1.1H59.2v-5.1c0-0.5-0.8-1-1.7-1h-5.1c-0.9,0" +
+ "-1.7,0.4-1.7,1v5.1l-30.2,0c-0.5,0-0.8,0.5-0.8,1.1v3.2c0,0.6," +
+ "0.4,1.1,0.8,1.1h0.1l-10.8,25C9,47.3,8.4,48,8.4,48.8v1.6l0,0h0" +
+ "v6.4c0,1.3,1.4,2.3,3.2,2.3h21.7c1.8,0,3.2-1,3.2-2.3v-8c0-0.9" +
+ "-0.7-1.6-1.7-2L22.9,21.9h27.9v59.6l-29,15.9c0,1.2,1.8,2.2,4.1," +
+ "2.2h58.3c2.3,0,4.1-1,4.1-2.2l-29-15.9V21.9h27.8L75.2,46.8c-1," +
+ "0.4-1.7,1.1-1.7,2v8c0,1.3,1.4,2.3,3.2,2.3h21.7c1.8,0,3.2-1,3.2" +
+ "-2.3v-8C101.6,48,101,47.3,100.1,46.9z M22,23.7l10.8,22.8H12.1" +
+ "L22,23.7z M97.9,46.5H77.2L88,23.7L97.9,46.5z"
+ },
+
+ badgeDataSet = {
+ _viewbox: "0 0 10 10",
+
+ uiAttached: "M2,2.5a.5,.5,0,0,1,.5-.5h5a.5,.5,0,0,1,.5,.5v3" +
+ "a.5,.5,0,0,1-.5,.5h-5a.5,.5,0,0,1-.5-.5zM2.5,2.8a.3,.3,0,0,1," +
+ ".3-.3h4.4a.3,.3,0,0,1,.3,.3v2.4a.3,.3,0,0,1-.3,.3h-4.4" +
+ "a.3,.3,0,0,1-.3-.3zM2,6.55h6l1,1.45h-8z",
+
+ checkMark: "M2.6,4.5c0,0,0.7-0.4,1.2,0.3l1.0," +
+ "1.8c0,0,2.7-5.4,2.8-5.7c0,0,0.5-0.9,1.4-0.1c0," +
+ "0,0.5,0.5,0,1.3S6.8,7.3,5.6,9.2c0,0-0.4," +
+ "0.5-1.2,0.1S2.2,5.4,2.2,5.4S2.2,4.7,2.6,4.5z",
+
+ xMark: "M9.0,7.2C8.2,6.9,7.4,6.1,6.7,5.2c0.4-0.5," +
+ "0.7-0.8,0.8-1.0C7.8,3.5,9.4,1.6,8.1,1.1" +
+ "C6.8,0.6,6.6,1.7,6.6,1.7C6.4,2.1,6.0,2.7,5.4," +
+ "3.4C4.9,2.5,4.5,1.9,4.5,1.9" +
+ "S3.8,0.2,2.9,0.7C1.9,1.1,2.3,2.3,2.3,2.3c0.3,1.1,0.8,2.1,1.4,2.9" +
+ "C2.5,6.4,1.3,7.4,1.3,7.4S0.8,7.8,0.8,8.1C0.9,8.3,0.9,9.6,2.4,9.1" +
+ "C3.1,8.8,4.1,7.9,5.1,7.0c1.3,1.3,2.5,1.9,2.5,1.9s0.5,0.5,1.4-0.2" +
+ "C9.8,7.9,9.0,7.2,9.0,7.2z",
+
+ triangleUp: "M0.5,6.2c0,0,3.8-3.8,4.2-4.2C5,1.7,5.3,2,5.3,2l4.3," +
+ "4.3c0,0,0.4,0.4-0.1,0.4c-1.7,0-8.2,0-8.8,0C0,6.6,0.5,6.2,0.5,6.2z",
+
+ triangleDown: "M9.5,4.2c0,0-3.8,3.8-4.2,4.2c-0.3,0.3-0.5,0-0.5," +
+ "0L0.5,4.2c0,0-0.4-0.4,0.1-0.4c1.7,0,8.2,0,8.8,0C10,3.8,9.5,4.2," +
+ "9.5,4.2z",
+
+ plus: "M4,2h2v2h2v2h-2v2h-2v-2h-2v-2h2z",
+
+ minus: "M2,4h6v2h-6z",
+
+ play: "M2.5,2l5.5,3l-5.5,3z",
+
+ stop: "M2.5,2.5h5v5h-5z"
+ },
+
+ spriteData = {
+ _cloud: '0 0 110 110',
+ cloud: "M37.6,79.5c-6.9,8.7-20.4,8.6-22.2-2.7" +
+ "M16.3,41.2c-0.8-13.9,19.4-19.2,23.5-7.8" +
+ "M38.9,30.9c5.1-9.4,15.1-8.5,16.9-1.3" +
+ "M54.4,32.9c4-12.9,14.8-9.6,18.6-3.8" +
+ "M95.8,58.5c10-4.1,11.7-17.8-0.9-19.8" +
+ "M18.1,76.4C5.6,80.3,3.8,66,13.8,61.5" +
+ "M16.2,62.4C2.1,58.4,3.5,36,16.8,36.6" +
+ "M93.6,74.7c10.2-2,10.7-14,5.8-18.3" +
+ "M71.1,79.3c11.2,7.6,24.6,6.4,22.1-11.7" +
+ "M36.4,76.8c3.4,13.3,35.4,11.6,36.1-1.4" +
+ "M70.4,31c11.8-10.4,26.2-5.2,24.7,10.1"
+ };
+
+ // ----------------------------------------------------------------------
+ // === Constants
+
+ var msgGS = 'GlyphService.',
+ rg = "registerGlyphs(): ",
+ rgs = "registerGlyphSet(): ";
+
+ // ----------------------------------------------------------------------
+
+ function warn(msg) {
+ $log.warn(msgGS + msg);
+ }
+
+ function addToMap(key, value, vbox, overwrite, dups) {
+ if (!overwrite && glyphs.get(key)) {
+ dups.push(key);
+ } else {
+ glyphs.set(key, {id: key, vb: vbox, d: value});
+ }
+ }
+
+ function reportDups(dups, which) {
+ var ok = (dups.length == 0),
+ msg = 'ID collision: ';
+
+ if (!ok) {
+ dups.forEach(function (id) {
+ warn(which + msg + '"' + id + '"');
+ });
+ }
+ return ok;
+ }
+
+ function reportMissVb(missing, which) {
+ var ok = (missing.length == 0),
+ msg = 'Missing viewbox property: ';
+
+ if (!ok) {
+ missing.forEach(function (vbk) {
+ warn(which + msg + '"' + vbk + '"');
+ });
+ }
+ return ok;
+ }
+
+ // ----------------------------------------------------------------------
+ // === API functions ===
+
+ function clear() {
+ // start with a fresh map
+ glyphs = d3.map();
+ }
+
+ function init() {
+ registerGlyphs(birdData);
+ registerGlyphSet(glyphDataSet);
+ registerGlyphSet(badgeDataSet);
+ registerGlyphs(spriteData);
+ }
+
+ function registerGlyphs(data, overwrite) {
+ var dups = [],
+ missvb = [];
+
+ angular.forEach(data, function (value, key) {
+ var vbk = '_' + key,
+ vb = data[vbk];
+
+ if (key[0] !== '_') {
+ if (!vb) {
+ missvb.push(vbk);
+ return;
+ }
+ addToMap(key, value, vb, overwrite, dups);
+ }
+ });
+ return reportDups(dups, rg) && reportMissVb(missvb, rg);
+ }
+
+ function registerGlyphSet(data, overwrite) {
+ var dups = [],
+ vb = data._viewbox;
+
+ if (!vb) {
+ warn(rgs + 'no "_viewbox" property found');
+ return false;
+ }
+
+ angular.forEach(data, function (value, key) {
+ if (key[0] !== '_') {
+ addToMap(key, value, vb, overwrite, dups);
+ }
+ });
+ return reportDups(dups, rgs);
+ }
+
+ function ids() {
+ return glyphs.keys();
+ }
+
+ function glyph(id) {
+ return glyphs.get(id);
+ }
+
+ // Note: defs should be a D3 selection of a single <defs> element
+ function loadDefs(defs, glyphIds, noClear) {
+ var list = fs.isA(glyphIds) || ids(),
+ clearCache = !noClear;
+
+ if (clearCache) {
+ // remove all existing content
+ defs.html(null);
+ }
+
+ // load up the requested glyphs
+ list.forEach(function (id) {
+ var g = glyph(id);
+ if (g) {
+ if (noClear) {
+ // quick exit if symbol is already present
+ if (defs.select('symbol#' + g.id).size() > 0) {
+ return;
+ }
+ }
+ defs.append('symbol')
+ .attr({ id: g.id, viewBox: g.vb })
+ .append('path').attr('d', g.d);
+ }
+ });
+ }
+
+ // trans can specify translation [x,y]
+ function addGlyph(elem, glyphId, size, overlay, trans) {
+ var sz = size || 40,
+ ovr = !!overlay,
+ xns = fs.isA(trans),
+ atr = {
+ width: sz,
+ height: sz,
+ 'class': 'glyph',
+ 'xlink:href': '#' + glyphId
+ };
+
+ if (xns) {
+ atr.transform = sus.translate(trans);
+ }
+ return elem.append('use').attr(atr).classed('overlay', ovr);
+ }
+
+ // ----------------------------------------------------------------------
+
+ angular.module('onosSvg')
+ .factory('GlyphService',
+ ['$log', 'FnService', 'SvgUtilService',
+
+ function (_$log_, _fs_, _sus_) {
+ $log = _$log_;
+ fs = _fs_;
+ sus = _sus_;
+
+ return {
+ clear: clear,
+ init: init,
+ registerGlyphs: registerGlyphs,
+ registerGlyphSet: registerGlyphSet,
+ ids: ids,
+ glyph: glyph,
+ loadDefs: loadDefs,
+ addGlyph: addGlyph
+ };
+ }]
+ )
+ .run(['$log', function ($log) {
+ $log.debug('Clearing glyph cache');
+ clear();
+ }]);
+
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/fw/svg/icon.css b/framework/src/onos/web/gui/src/main/webapp/app/fw/svg/icon.css
new file mode 100644
index 00000000..6ade7c7e
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/fw/svg/icon.css
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2014,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 -- Icon Service -- CSS file
+ */
+
+svg#IconLibDefs {
+ display: none;
+}
+
+svg .svgIcon {
+ fill-rule: evenodd;
+}
+
+svg.embeddedIcon g.icon {
+ fill: none;
+}
+
+svg.embeddedIcon g.icon rect {
+ stroke: none;
+ fill: none;
+}
+
+svg.embeddedIcon g.icon .glyph {
+ stroke: none;
+ fill: white;
+ fill-rule: evenodd;
+}
+
+
+/* Sortable table headers */
+.light div.tableColSort svg.embeddedIcon .icon .glyph {
+ fill: black;
+}
+.dark div.tableColSort svg.embeddedIcon .icon .glyph {
+ fill: #ccc;
+}
+
+
+/* color schemes for specific icon classes */
+
+svg.embeddedIcon .icon.appInactive .glyph {
+ fill: none;
+}
+
+.light svg.embeddedIcon .icon.active .glyph {
+ fill: green;
+}
+.dark svg.embeddedIcon .icon.active .glyph {
+ fill: #308C10;
+}
+
+
+.light table svg.embeddedIcon {
+ fill: #ccc;
+}
+.dark table svg.embeddedIcon {
+ fill: #222;
+}
+.light table svg.embeddedIcon .glyph {
+ fill: #333;
+}
+.dark table svg.embeddedIcon .glyph {
+ fill: #ccc;
+}
+
+.light svg.embeddedIcon .icon.active .glyph {
+ fill: green;
+}
+.light svg.embeddedIcon .icon.inactive .glyph {
+ fill: darkred;
+}
+.dark svg.embeddedIcon .icon.active .glyph {
+ fill: #308C10;
+}
+.dark svg.embeddedIcon .icon.inactive .glyph {
+ fill: #AD2D2D;
+}
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/fw/svg/icon.js b/framework/src/onos/web/gui/src/main/webapp/app/fw/svg/icon.js
new file mode 100644
index 00000000..ba794313
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/fw/svg/icon.js
@@ -0,0 +1,265 @@
+/*
+ * 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 -- SVG -- Icon Service
+ */
+(function () {
+ 'use strict';
+
+ var $log, fs, gs, sus;
+
+ var vboxSize = 50,
+ cornerSize = vboxSize / 10,
+ viewBox = '0 0 ' + vboxSize + ' ' + vboxSize;
+
+ // Maps icon ID to the glyph ID it uses.
+ // NOTE: icon ID maps to a CSS class for styling that icon
+ var glyphMapping = {
+ active: 'checkMark',
+ inactive: 'xMark',
+
+ plus: 'plus',
+ minus: 'minus',
+ play: 'play',
+ stop: 'stop',
+
+ refresh: 'refresh',
+ garbage: 'garbage',
+
+ upArrow: 'triangleUp',
+ downArrow: 'triangleDown',
+
+ loading: 'loading',
+
+ appInactive: 'unknown',
+
+ devIcon_SWITCH: 'switch',
+ devIcon_ROADM: 'roadm',
+ flowTable: 'flowTable',
+ portTable: 'portTable',
+ groupTable: 'groupTable',
+
+ hostIcon_endstation: 'endstation',
+ hostIcon_router: 'router',
+ hostIcon_bgpSpeaker: 'bgpSpeaker',
+
+ nav_apps: 'bird',
+ nav_settings: 'chain',
+ nav_cluster: 'node',
+ nav_topo: 'topo',
+ nav_devs: 'switch',
+ nav_links: 'ports',
+ nav_hosts: 'endstation',
+ nav_intents: 'relatedIntents'
+ };
+
+ function ensureIconLibDefs() {
+ var body = d3.select('body'),
+ svg = body.select('svg#IconLibDefs'),
+ defs;
+
+ if (svg.empty()) {
+ svg = body.append('svg').attr('id', 'IconLibDefs');
+ defs = svg.append('defs');
+ }
+ return svg.select('defs');
+ }
+
+ // div is a D3 selection of the <DIV> element into which icon should load
+ // glyphId identifies the glyph to use
+ // size is dimension of icon in pixels. Defaults to 20.
+ // installGlyph, if truthy, will cause the glyph to be added to
+ // well-known defs element. Defaults to false.
+ // svgClass is the CSS class used to identify the SVG layer.
+ // Defaults to 'embeddedIcon'.
+ function loadIcon(div, glyphId, size, installGlyph, svgClass) {
+ var dim = size || 20,
+ svgCls = svgClass || 'embeddedIcon',
+ gid = glyphId || 'unknown',
+ svg, g;
+
+ if (installGlyph) {
+ gs.loadDefs(ensureIconLibDefs(), [gid], true);
+ }
+
+ svg = div.append('svg').attr({
+ 'class': svgCls,
+ width: dim,
+ height: dim,
+ viewBox: viewBox
+ });
+
+ g = svg.append('g').attr({
+ 'class': 'icon'
+ });
+
+ g.append('rect').attr({
+ width: vboxSize,
+ height: vboxSize,
+ rx: cornerSize
+ });
+
+ g.append('use').attr({
+ width: vboxSize,
+ height: vboxSize,
+ 'class': 'glyph',
+ 'xlink:href': '#' + gid
+ });
+ }
+
+ // div is a D3 selection of the <DIV> element into which icon should load
+ // iconCls is the CSS class used to identify the icon
+ // size is dimension of icon in pixels. Defaults to 20.
+ // installGlyph, if truthy, will cause the glyph to be added to
+ // well-known defs element. Defaults to false.
+ // svgClass is the CSS class used to identify the SVG layer.
+ // Defaults to 'embeddedIcon'.
+ function loadIconByClass(div, iconCls, size, installGlyph, svgClass) {
+ loadIcon(div, glyphMapping[iconCls], size, installGlyph, svgClass);
+ div.select('svg g').classed(iconCls, true);
+ }
+
+ function loadEmbeddedIcon(div, iconCls, size) {
+ loadIconByClass(div, iconCls, size, true);
+ }
+
+
+ // configuration for device and host icons in the topology view
+ var config = {
+ device: {
+ dim: 36,
+ rx: 4
+ },
+ host: {
+ radius: {
+ noGlyph: 9,
+ withGlyph: 14
+ },
+ glyphed: {
+ endstation: 1,
+ bgpSpeaker: 1,
+ router: 1
+ }
+ }
+ };
+
+
+ // Adds a device icon to the specified element, using the given glyph.
+ // Returns the D3 selection of the icon.
+ function addDeviceIcon(elem, glyphId) {
+ var cfg = config.device,
+ g = elem.append('g')
+ .attr('class', 'svgIcon deviceIcon');
+
+ g.append('rect').attr({
+ x: 0,
+ y: 0,
+ rx: cfg.rx,
+ width: cfg.dim,
+ height: cfg.dim
+ });
+
+ g.append('use').attr({
+ 'xlink:href': '#' + glyphId,
+ width: cfg.dim,
+ height: cfg.dim
+ });
+
+ g.dim = cfg.dim;
+ return g;
+ }
+
+ function addHostIcon(elem, radius, glyphId) {
+ var dim = radius * 1.5,
+ xlate = -dim / 2,
+ g = elem.append('g')
+ .attr('class', 'svgIcon hostIcon');
+
+ g.append('circle').attr('r', radius);
+
+ g.append('use').attr({
+ 'xlink:href': '#' + glyphId,
+ width: dim,
+ height: dim,
+ transform: sus.translate(xlate,xlate)
+ });
+ return g;
+ }
+
+ function sortIcons() {
+ function sortAsc(div) {
+ div.style('display', 'inline-block');
+ loadEmbeddedIcon(div, 'upArrow', 10);
+ div.classed('tableColSort', true);
+ }
+
+ function sortDesc(div) {
+ div.style('display', 'inline-block');
+ loadEmbeddedIcon(div, 'downArrow', 10);
+ div.classed('tableColSort', true);
+ }
+
+ function sortNone(div) {
+ div.remove();
+ }
+
+ return {
+ sortAsc: sortAsc,
+ sortDesc: sortDesc,
+ sortNone: sortNone
+ };
+ }
+
+
+ // =========================
+ // === DEFINE THE MODULE
+
+ angular.module('onosSvg')
+ .directive('icon', ['IconService', function (is) {
+ return {
+ restrict: 'A',
+ link: function (scope, element, attrs) {
+ attrs.$observe('iconId', function () {
+ var div = d3.select(element[0]);
+ div.selectAll('*').remove();
+ is.loadEmbeddedIcon(div, attrs.iconId, attrs.iconSize);
+ });
+ }
+ };
+ }])
+
+ .factory('IconService', ['$log', 'FnService', 'GlyphService',
+ 'SvgUtilService',
+
+ function (_$log_, _fs_, _gs_, _sus_) {
+ $log = _$log_;
+ fs = _fs_;
+ gs = _gs_;
+ sus = _sus_;
+
+ return {
+ loadIcon: loadIcon,
+ loadIconByClass: loadIconByClass,
+ loadEmbeddedIcon: loadEmbeddedIcon,
+ addDeviceIcon: addDeviceIcon,
+ addHostIcon: addHostIcon,
+ iconConfig: function () { return config; },
+ sortIcons: sortIcons
+ };
+ }]);
+
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/fw/svg/map.js b/framework/src/onos/web/gui/src/main/webapp/app/fw/svg/map.js
new file mode 100644
index 00000000..f8d40f9c
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/fw/svg/map.js
@@ -0,0 +1,129 @@
+/*
+ * 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 -- SVG -- Map Service
+ */
+
+/*
+ The Map Service provides a simple API for loading geographical maps into
+ an SVG layer. For example, as a background to the Topology View.
+
+ e.g. var promise = MapService.loadMapInto(svgLayer, '*continental-us');
+
+ The Map Service makes use of the GeoDataService to load the required data
+ from the server and to create the appropriate geographical projection.
+
+ A promise is returned to the caller, which is resolved with the
+ map projection once created.
+*/
+
+(function () {
+ 'use strict';
+
+ // injected references
+ var $log, $q, fs, gds;
+
+ // NOTE: This method assumes the datafile has exactly the map data
+ // that you want to load; for example id="*continental_us"
+ // mapping to ~/data/map/continental_us.topojson contains
+ // exactly the paths for the continental US.
+
+ function loadMapInto(mapLayer, id, opts) {
+ var promise = gds.fetchTopoData(id),
+ deferredProjection = $q.defer();
+
+ if (!promise) {
+ $log.warn('Failed to load map: ' + id);
+ return false;
+ }
+
+ promise.then(function () {
+ var gen = gds.createPathGenerator(promise.topodata, opts);
+
+ deferredProjection.resolve(gen.settings.projection);
+
+ mapLayer.selectAll('path')
+ .data(gen.geodata.features)
+ .enter()
+ .append('path')
+ .attr('d', gen.pathgen);
+ });
+ return deferredProjection.promise;
+ }
+
+ // ---
+
+ // NOTE: This method uses the countries.topojson data file, and then
+ // filters the results based on the supplied options.
+ // Usage:
+ // promise = loadMapRegionInto(svgGroup, {
+ // countryFilter: function (country) {
+ // return country.properties.continent === 'South America';
+ // }
+ // });
+
+ function loadMapRegionInto(mapLayer, filterOpts) {
+ var promise = gds.fetchTopoData("*countries"),
+ deferredProjection = $q.defer();
+
+ if (!promise) {
+ $log.warn('Failed to load countries TopoJSON data');
+ return false;
+ }
+
+ promise.then(function () {
+ var width = 1000,
+ height = 1000,
+ proj = d3.geo.mercator().translate([width/2, height/2]),
+ pathGen = d3.geo.path().projection(proj),
+ data = promise.topodata,
+ features = topojson.feature(data, data.objects.countries).features,
+ country = features.filter(filterOpts.countryFilter),
+ countryFeature = {
+ type: 'FeatureCollection',
+ features: country
+ },
+ path = d3.geo.path().projection(proj);
+
+ gds.rescaleProjection(proj, 0.95, 1000, path, countryFeature);
+
+ deferredProjection.resolve(proj);
+
+ mapLayer.selectAll('path.country')
+ .data([countryFeature])
+ .enter()
+ .append('path').classed('country', true)
+ .attr('d', pathGen);
+ });
+ return deferredProjection.promise;
+ }
+
+ angular.module('onosSvg')
+ .factory('MapService', ['$log', '$q', 'FnService', 'GeoDataService',
+ function (_$log_, _$q_, _fs_, _gds_) {
+ $log = _$log_;
+ $q = _$q_;
+ fs = _fs_;
+ gds = _gds_;
+
+ return {
+ loadMapRegionInto: loadMapRegionInto,
+ loadMapInto: loadMapInto
+ };
+ }]);
+
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/fw/svg/svg.js b/framework/src/onos/web/gui/src/main/webapp/app/fw/svg/svg.js
new file mode 100644
index 00000000..8fd441fe
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/fw/svg/svg.js
@@ -0,0 +1,25 @@
+/*
+ * 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 -- Scalable Vector Graphics Module
+ */
+(function () {
+ 'use strict';
+
+ angular.module('onosSvg', ['onosUtil']);
+
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/fw/svg/svgUtil.js b/framework/src/onos/web/gui/src/main/webapp/app/fw/svg/svgUtil.js
new file mode 100644
index 00000000..cb67ae83
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/fw/svg/svgUtil.js
@@ -0,0 +1,311 @@
+/*
+ * 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 -- SVG -- Util Service
+ */
+
+/*
+ The SVG Util Service provides a miscellany of utility functions.
+ */
+
+(function () {
+ 'use strict';
+
+ // injected references
+ var $log, fs;
+
+ angular.module('onosSvg')
+ .factory('SvgUtilService', ['$log', 'FnService',
+ function (_$log_, _fs_) {
+ $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()
+ .domain([0, 0.1])
+ .range([5, 20])
+ .clamp(true),
+ drag,
+ fSel = fs.isF(selectCb),
+ fEnd = fs.isF(atDragEnd),
+ fDEn = fs.isF(dragEnabled),
+ fCEn = fs.isF(clickEnabled),
+ bad = [];
+
+ function naf(what) {
+ return 'SvgUtilService: createDragBehavior(): ' + what +
+ ' is not a function';
+ }
+
+ if (!force) {
+ bad.push('SvgUtilService: createDragBehavior(): ' +
+ 'Bad force reference');
+ }
+ if (!fSel) {
+ bad.push(naf('selectCb'));
+ }
+ if (!fEnd) {
+ bad.push(naf('atDragEnd'));
+ }
+ if (!fDEn) {
+ bad.push(naf('dragEnabled'));
+ }
+ if (!fCEn) {
+ bad.push(naf('clickEnabled'));
+ }
+
+ if (bad.length) {
+ $log.error(bad.join('\n'));
+ return null;
+ }
+
+ function dragged(d) {
+ var threshold = draggedThreshold(force.alpha()),
+ dx = d.oldX - d.px,
+ dy = d.oldY - d.py;
+ if (Math.abs(dx) >= threshold || Math.abs(dy) >= threshold) {
+ d.dragged = true;
+ }
+ return d.dragged;
+ }
+
+ drag = d3.behavior.drag()
+ .origin(function(d) { return d; })
+ .on('dragstart', function(d) {
+ if (clickEnabled() || dragEnabled()) {
+ d3.event.sourceEvent.stopPropagation();
+
+ d.oldX = d.x;
+ d.oldY = d.y;
+ d.dragged = false;
+ d.fixed |= 2;
+ d.dragStarted = true;
+ }
+ })
+ .on('drag', function(d) {
+ if (dragEnabled()) {
+ d.px = d3.event.x;
+ d.py = d3.event.y;
+ if (dragged(d)) {
+ if (!force.alpha()) {
+ force.alpha(.025);
+ }
+ }
+ }
+ })
+ .on('dragend', function(d) {
+ if (d.dragStarted) {
+ d.dragStarted = false;
+ if (!dragged(d)) {
+ // consider this the same as a 'click'
+ // (selection of a node)
+ if (clickEnabled()) {
+ selectCb.call(this, d);
+ }
+ }
+ d.fixed &= ~6;
+
+ // hook at the end of a drag gesture
+ if (dragEnabled()) {
+ atDragEnd.call(this, d);
+ }
+ }
+ });
+
+ return drag;
+ }
+
+
+ function loadGlow(defs, r, g, b, id) {
+ var glow = defs.append('filter')
+ .attr('x', '-50%')
+ .attr('y', '-50%')
+ .attr('width', '200%')
+ .attr('height', '200%')
+ .attr('id', id);
+
+ glow.append('feColorMatrix')
+ .attr('type', 'matrix')
+ .attr('values',
+ '0 0 0 0 ' + r + ' ' +
+ '0 0 0 0 ' + g + ' ' +
+ '0 0 0 0 ' + b + ' ' +
+ '0 0 0 1 0 ');
+
+ glow.append('feGaussianBlur')
+ .attr('stdDeviation', 3)
+ .attr('result', 'coloredBlur');
+
+ glow.append('feMerge').selectAll('feMergeNode')
+ .data(['coloredBlur', 'SourceGraphic'])
+ .enter().append('feMergeNode')
+ .attr('in', String);
+ }
+
+ function loadGlowDefs(defs) {
+ loadGlow(defs, 0.0, 0.0, 0.7, 'blue-glow');
+ loadGlow(defs, 1.0, 1.0, 0.3, 'yellow-glow');
+ }
+
+ // --- Ordinal scales for 7 values.
+
+ // blue brown brick red sea green purple dark teal lime
+ var lightNorm = ['#3E5780', '#78533B', '#CB4D28', '#018D61', '#8A2979', '#006D73', '#56AF00'],
+ lightMute = ['#A8B8CC', '#CCB3A8', '#FFC2BD', '#96D6BF', '#D19FCE', '#8FCCCA', '#CAEAA4'],
+
+ darkNorm = ['#304860', '#664631', '#A8391B', '#00754B', '#77206D', '#005959', '#428700'],
+ darkMute = ['#304860', '#664631', '#A8391B', '#00754B', '#77206D', '#005959', '#428700'];
+
+ var colors= {
+ light: {
+ norm: d3.scale.ordinal().range(lightNorm),
+ mute: d3.scale.ordinal().range(lightMute)
+ },
+ dark: {
+ norm: d3.scale.ordinal().range(darkNorm),
+ mute: d3.scale.ordinal().range(darkMute)
+ }
+ };
+
+ function cat7() {
+ var tcid = 'd3utilTestCard';
+
+ function getColor(id, muted, theme) {
+ // NOTE: since we are lazily assigning domain ids, we need to
+ // get the color from all 4 scales, to keep the domains
+ // in sync.
+ var ln = colors.light.norm(id),
+ lm = colors.light.mute(id),
+ dn = colors.dark.norm(id),
+ dm = colors.dark.mute(id);
+ if (theme === 'dark') {
+ return muted ? dm : dn;
+ } else {
+ return muted ? lm : ln;
+ }
+ }
+
+ function testCard(svg) {
+ var g = svg.select('g#' + tcid),
+ dom = d3.range(7),
+ k, muted, theme, what;
+
+ if (!g.empty()) {
+ g.remove();
+
+ } else {
+ g = svg.append('g')
+ .attr('id', tcid)
+ .attr('transform', 'scale(4)translate(20,20)');
+
+ for (k=0; k<4; k++) {
+ muted = k%2;
+ what = muted ? ' muted' : ' normal';
+ theme = k < 2 ? 'light' : 'dark';
+ dom.forEach(function (id, i) {
+ var x = i * 20,
+ y = k * 20,
+ f = get(id, muted, theme);
+ g.append('circle').attr({
+ cx: x,
+ cy: y,
+ r: 5,
+ fill: f
+ });
+ });
+ g.append('rect').attr({
+ x: 140,
+ y: k * 20 - 5,
+ width: 32,
+ height: 10,
+ rx: 2,
+ fill: '#888'
+ });
+ g.append('text').text(theme + what)
+ .attr({
+ x: 142,
+ y: k * 20 + 2,
+ fill: 'white'
+ })
+ .style('font-size', '4pt');
+ }
+ }
+ }
+
+ return {
+ testCard: testCard,
+ getColor: getColor
+ };
+ }
+
+ function translate(x, y) {
+ if (fs.isA(x) && x.length === 2 && !y) {
+ return 'translate(' + x[0] + ',' + x[1] + ')';
+ }
+ return 'translate(' + x + ',' + y + ')';
+ }
+
+ function scale(x, y) {
+ return 'scale(' + x + ',' + y + ')';
+ }
+
+ function skewX(x) {
+ return 'skewX(' + x + ')';
+ }
+
+ function rotate(deg) {
+ return 'rotate(' + deg + ')';
+ }
+
+ function stripPx(s) {
+ return s.replace(/px$/,'');
+ }
+
+ function safeId(s) {
+ return s.replace(/[^a-z0-9]/gi, '-');
+ }
+
+ function makeVisible(el, b) {
+ el.style('visibility', (b ? 'visible' : 'hidden'));
+ }
+
+ function isVisible(el) {
+ return el.style('visibility') === 'visible';
+ }
+
+ return {
+ createDragBehavior: createDragBehavior,
+ loadGlowDefs: loadGlowDefs,
+ cat7: cat7,
+ translate: translate,
+ scale: scale,
+ skewX: skewX,
+ rotate: rotate,
+ stripPx: stripPx,
+ safeId: safeId,
+ visible: function (el, x) {
+ if (x === undefined) {
+ return isVisible(el);
+ } else {
+ makeVisible(el, x);
+ }
+ }
+ };
+ }]);
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/fw/svg/zoom.js b/framework/src/onos/web/gui/src/main/webapp/app/fw/svg/zoom.js
new file mode 100644
index 00000000..6acab794
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/fw/svg/zoom.js
@@ -0,0 +1,132 @@
+/*
+ * 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 -- SVG -- Zoom Service
+ */
+(function () {
+ 'use strict';
+
+ // configuration
+ var defaultSettings = {
+ zoomMin: 0.05,
+ zoomMax: 10,
+ zoomEnabled: function (ev) { return true; },
+ zoomCallback: function () {}
+ };
+
+ // injected references to services
+ var $log;
+
+ angular.module('onosSvg')
+ .factory('ZoomService', ['$log',
+
+ function (_$log_) {
+ $log = _$log_;
+
+/*
+ NOTE: opts is an object:
+ {
+ svg: svg, D3 selection of <svg> element
+ zoomLayer: zoomLayer, D3 selection of <g> element
+ zoomEnabled: function (ev) { ... },
+ zoomCallback: function () { ... }
+ }
+
+ where:
+ * svg and zoomLayer should be D3 selections of DOM elements.
+ * zoomLayer <g> is a child of <svg> element.
+ * zoomEnabled is an optional predicate based on D3 event.
+ * default is always enabled.
+ * zoomCallback is an optional callback invoked each time we pan/zoom.
+ * default is do nothing.
+
+ Optionally, zoomMin and zoomMax also can be defined.
+ These default to 0.25 and 10 respectively.
+*/
+ function createZoomer(opts) {
+ var cz = 'ZoomService.createZoomer(): ',
+ d3s = ' (D3 selection) property defined',
+ settings = angular.extend({}, defaultSettings, opts),
+ zoom = d3.behavior.zoom()
+ .translate([0, 0])
+ .scale(1)
+ .scaleExtent([settings.zoomMin, settings.zoomMax])
+ .on('zoom', zoomed),
+ fail = false,
+ zoomer;
+
+ if (!settings.svg) {
+ $log.error(cz + 'No "svg" (svg tag)' + d3s);
+ fail = true;
+ }
+ if (!settings.zoomLayer) {
+ $log.error(cz + 'No "zoomLayer" (g tag)' + d3s);
+ fail = true;
+ }
+
+ if (fail) {
+ return null;
+ }
+
+ // zoom events from mouse gestures...
+ function zoomed() {
+ var ev = d3.event.sourceEvent;
+ if (settings.zoomEnabled(ev)) {
+ adjustZoomLayer(d3.event.translate, d3.event.scale);
+ }
+ }
+
+ function adjustZoomLayer(translate, scale) {
+ settings.zoomLayer.attr('transform',
+ 'translate(' + translate + ')scale(' + scale + ')');
+ settings.zoomCallback();
+ }
+
+ zoomer = {
+ panZoom: function (translate, scale) {
+ zoom.translate(translate).scale(scale);
+ adjustZoomLayer(translate, scale);
+ },
+
+ reset: function () {
+ zoomer.panZoom([0,0], 1);
+ },
+
+ translate: function () {
+ return zoom.translate();
+ },
+
+ scale: function () {
+ return zoom.scale();
+ },
+
+ scaleExtent: function () {
+ return zoom.scaleExtent();
+ }
+ };
+
+ // apply the zoom behavior to the SVG element
+ settings.svg && settings.svg.call(zoom);
+ return zoomer;
+ }
+
+ return {
+ createZoomer: createZoomer
+ };
+ }]);
+
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/fw/util/fn.js b/framework/src/onos/web/gui/src/main/webapp/app/fw/util/fn.js
new file mode 100644
index 00000000..defb8458
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/fw/util/fn.js
@@ -0,0 +1,292 @@
+/*
+ * Copyright 2014,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 -- Util -- General Purpose Functions
+ */
+(function () {
+ 'use strict';
+
+ // injected services
+ var $window, $log;
+
+ // internal state
+ var debugFlags = {};
+
+
+ function _parseDebugFlags(dbgstr) {
+ var bits = dbgstr ? dbgstr.split(",") : [];
+ bits.forEach(function (key) {
+ debugFlags[key] = true;
+ });
+ $log.debug('Debug flags:', dbgstr);
+ }
+
+ function isF(f) {
+ return typeof f === 'function' ? f : null;
+ }
+
+ function isA(a) {
+ // NOTE: Array.isArray() is part of EMCAScript 5.1
+ return Array.isArray(a) ? a : null;
+ }
+
+ function isS(s) {
+ return typeof s === 'string' ? s : null;
+ }
+
+ function isO(o) {
+ return (o && typeof o === 'object' && o.constructor === Object) ? o : null;
+ }
+
+ function contains(a, x) {
+ return isA(a) && a.indexOf(x) > -1;
+ }
+
+ // Returns true if all names in the array are defined as functions
+ // on the given api object; false otherwise.
+ // Also returns false if there are properties on the api that are NOT
+ // listed in the array of names.
+ function areFunctions(api, fnNames) {
+ var fnLookup = {},
+ extraFound = false;
+
+ if (!isA(fnNames)) {
+ return false;
+ }
+ var n = fnNames.length,
+ i, name;
+ for (i=0; i<n; i++) {
+ name = fnNames[i];
+ if (!isF(api[name])) {
+ return false;
+ }
+ fnLookup[name] = true;
+ }
+
+ // check for properties on the API that are not listed in the array,
+ angular.forEach(api, function (value, key) {
+ if (!fnLookup[key]) {
+ extraFound = true;
+ }
+ });
+ return !extraFound;
+ }
+
+ // Returns true if all names in the array are defined as functions
+ // on the given api object; false otherwise. This is a non-strict version
+ // that does not care about other properties on the api.
+ function areFunctionsNonStrict(api, fnNames) {
+ if (!isA(fnNames)) {
+ return false;
+ }
+ var n = fnNames.length,
+ i, name;
+ for (i=0; i<n; i++) {
+ name = fnNames[i];
+ if (!isF(api[name])) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ // Returns width and height of window inner dimensions.
+ // offH, offW : offset width/height are subtracted, if present
+ function windowSize(offH, offW) {
+ var oh = offH || 0,
+ ow = offW || 0;
+ return {
+ height: $window.innerHeight - oh,
+ width: $window.innerWidth - ow
+ };
+ }
+
+ // Returns true if current browser determined to be a mobile device
+ function isMobile() {
+ var ua = $window.navigator.userAgent,
+ patt = /iPhone|iPod|iPad|Silk|Android|BlackBerry|Opera Mini|IEMobile/;
+ return patt.test(ua);
+ }
+
+ // Returns true if the current browser determined to be Chrome
+ function isChrome() {
+ var isChromium = $window.chrome,
+ vendorName = $window.navigator.vendor,
+ isOpera = $window.navigator.userAgent.indexOf("OPR") > -1;
+ return (isChromium !== null &&
+ isChromium !== undefined &&
+ vendorName === "Google Inc." &&
+ isOpera == false);
+ }
+
+ // Returns true if the current browser determined to be Safari
+ function isSafari() {
+ return ($window.navigator.userAgent.indexOf('Safari') !== -1 &&
+ $window.navigator.userAgent.indexOf('Chrome') === -1);
+ }
+
+ // Returns true if the current browser determined to be Firefox
+ function isFirefox() {
+ return typeof InstallTrigger !== 'undefined';
+ }
+
+ // search through an array of objects, looking for the one with the
+ // tagged property matching the given key. tag defaults to 'id'.
+ // returns the index of the matching object, or -1 for no match.
+ function find(key, array, tag) {
+ var _tag = tag || 'id',
+ idx, n, d;
+ for (idx = 0, n = array.length; idx < n; idx++) {
+ d = array[idx];
+ if (d[_tag] === key) {
+ return idx;
+ }
+ }
+ return -1;
+ }
+
+ // search through array to find (the first occurrence of) item,
+ // returning its index if found; otherwise returning -1.
+ function inArray(item, array) {
+ var i;
+ if (isA(array)) {
+ for (i=0; i<array.length; i++) {
+ if (array[i] === item) {
+ return i;
+ }
+ }
+ }
+ return -1;
+ }
+
+ // remove (the first occurrence of) the specified item from the given
+ // array, if any. Return true if the removal was made; false otherwise.
+ function removeFromArray(item, array) {
+ var found = false,
+ i = inArray(item, array);
+ if (i >= 0) {
+ array.splice(i, 1);
+ found = true;
+ }
+ return found;
+ }
+
+ // return true if the object is empty, return false otherwise
+ function isEmptyObject(obj) {
+ var key;
+ for (key in obj) {
+ return false;
+ }
+ return true;
+ }
+
+ // returns true if the two objects have all the same properties
+ function sameObjProps(obj1, obj2) {
+ var key;
+ for (key in obj1) {
+ if (obj1.hasOwnProperty(key)) {
+ if (!(obj1[key] === obj2[key])) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ // returns true if the array contains the object
+ // does NOT use strict object reference equality,
+ // instead checks each property individually for equality
+ function containsObj(arr, obj) {
+ var i,
+ len = arr.length;
+ for (i = 0; i < len; i++) {
+ if (sameObjProps(arr[i], obj)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ // return the given string with the first character capitalized.
+ function cap(s) {
+ return s.toLowerCase().replace(/^[a-z]/, function (m) {
+ return m.toUpperCase();
+ });
+ }
+
+ // return the parameter without a px suffix
+ function noPx(num) {
+ return Number(num.replace(/px$/, ''));
+ }
+
+ // return an element's given style property without px suffix
+ function noPxStyle(elem, prop) {
+ return Number(elem.style(prop).replace(/px$/, ''));
+ }
+
+ function endsWith(str, suffix) {
+ return str.indexOf(suffix, str.length - suffix.length) !== -1;
+ }
+
+ function parseBitRate(str) {
+ return Number(str.replace(/,/, '')
+ .replace(/\s+.bps/i, '')
+ .replace(/\.\d*/, ''));
+ }
+
+ // return true if the given debug flag was specified in the query params
+ function debugOn(tag) {
+ return debugFlags[tag];
+ }
+
+ angular.module('onosUtil')
+ .factory('FnService',
+ ['$window', '$location', '$log', function (_$window_, $loc, _$log_) {
+ $window = _$window_;
+ $log = _$log_;
+
+ _parseDebugFlags($loc.search().debug);
+
+ return {
+ isF: isF,
+ isA: isA,
+ isS: isS,
+ isO: isO,
+ contains: contains,
+ areFunctions: areFunctions,
+ areFunctionsNonStrict: areFunctionsNonStrict,
+ windowSize: windowSize,
+ isMobile: isMobile,
+ isChrome: isChrome,
+ isSafari: isSafari,
+ isFirefox: isFirefox,
+ debugOn: debugOn,
+ find: find,
+ inArray: inArray,
+ removeFromArray: removeFromArray,
+ isEmptyObject: isEmptyObject,
+ sameObjProps: sameObjProps,
+ containsObj: containsObj,
+ cap: cap,
+ noPx: noPx,
+ noPxStyle: noPxStyle,
+ endsWith: endsWith,
+ parseBitRate: parseBitRate
+ };
+ }]);
+
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/fw/util/keys.js b/framework/src/onos/web/gui/src/main/webapp/app/fw/util/keys.js
new file mode 100644
index 00000000..2985565c
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/fw/util/keys.js
@@ -0,0 +1,215 @@
+/*
+ * Copyright 2014,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 -- Util -- Key Handler Service
+ */
+(function () {
+ 'use strict';
+
+ // references to injected services
+ var $log, fs, ts, ns, qhs;
+
+ // internal state
+ var enabled = true,
+ keyHandler = {
+ globalKeys: {},
+ maskedKeys: {},
+ viewKeys: {},
+ viewFn: null,
+ viewGestures: []
+ };
+
+ function whatKey(code) {
+ switch (code) {
+ case 13: return 'enter';
+ case 16: return 'shift';
+ case 17: return 'ctrl';
+ case 18: return 'alt';
+ case 27: return 'esc';
+ case 32: return 'space';
+ case 37: return 'leftArrow';
+ case 38: return 'upArrow';
+ case 39: return 'rightArrow';
+ case 40: return 'downArrow';
+ case 91: return 'cmdLeft';
+ case 93: return 'cmdRight';
+ case 187: return 'equals';
+ case 188: return 'comma';
+ case 189: return 'dash';
+ case 190: return 'dot';
+ case 191: return 'slash';
+ case 192: return 'backQuote';
+ case 220: return 'backSlash';
+ default:
+ if ((code >= 48 && code <= 57) ||
+ (code >= 65 && code <= 90)) {
+ return String.fromCharCode(code);
+ } else if (code >= 112 && code <= 123) {
+ return 'F' + (code - 111);
+ }
+ return '.';
+ }
+ }
+
+ function keyIn() {
+ var event = d3.event,
+ keyCode = event.keyCode,
+ key = whatKey(keyCode),
+ kh = keyHandler,
+ gk = kh.globalKeys[key],
+ gcb = fs.isF(gk) || (fs.isA(gk) && fs.isF(gk[0])),
+ vk = kh.viewKeys[key],
+ kl = fs.isF(kh.viewKeys._keyListener),
+ vcb = fs.isF(vk) || (fs.isA(vk) && fs.isF(vk[0])) || fs.isF(kh.viewFn),
+ token = 'keyev'; // indicate this was a key-pressed event
+
+ d3.event.stopPropagation();
+
+ if (enabled) {
+ // global callback?
+ if (gcb && gcb(token, key, keyCode, event)) {
+ // if the event was 'handled', we are done
+ return;
+ }
+ // otherwise, let the view callback have a shot
+ if (vcb) {
+ vcb(token, key, keyCode, event);
+ }
+ if (kl) {
+ kl(key);
+ }
+ }
+ }
+
+ function setupGlobalKeys() {
+ angular.extend(keyHandler, {
+ globalKeys: {
+ backSlash: [quickHelp, 'Show / hide Quick Help'],
+ slash: [quickHelp, 'Show / hide Quick Help'],
+ esc: [escapeKey, 'Dismiss dialog or cancel selections'],
+ T: [toggleTheme, "Toggle theme"]
+ },
+ globalFormat: ['backSlash', 'slash', 'esc', 'T'],
+
+ // Masked keys are global key handlers that always return true.
+ // That is, the view will never see the event for that key.
+ maskedKeys: {
+ slash: true,
+ backSlash: true,
+ T: true
+ }
+ });
+ }
+
+ function quickHelp(view, key, code, ev) {
+ qhs.showQuickHelp(keyHandler);
+ return true;
+ }
+
+ // returns true if we 'consumed' the ESC keypress, false otherwise
+ function escapeKey(view, key, code, ev) {
+ return ns.hideIfShown() || qhs.hideQuickHelp();
+ }
+
+ function toggleTheme(view, key, code, ev) {
+ ts.toggleTheme();
+ return true;
+ }
+
+ function setKeyBindings(keyArg) {
+ var viewKeys,
+ masked = [];
+
+ if (fs.isF(keyArg)) {
+ // set general key handler callback
+ keyHandler.viewFn = keyArg;
+ } else {
+ // set specific key filter map
+ viewKeys = d3.map(keyArg).keys();
+ viewKeys.forEach(function (key) {
+ if (keyHandler.maskedKeys[key]) {
+ masked.push('setKeyBindings(): Key "' + key + '" is reserved');
+ }
+ });
+
+ if (masked.length) {
+ $log.warn(masked.join('\n'));
+ }
+ keyHandler.viewKeys = keyArg;
+ }
+ }
+
+ function getKeyBindings() {
+ var gkeys = d3.map(keyHandler.globalKeys).keys(),
+ masked = d3.map(keyHandler.maskedKeys).keys(),
+ vkeys = d3.map(keyHandler.viewKeys).keys(),
+ vfn = !!fs.isF(keyHandler.viewFn);
+
+ return {
+ globalKeys: gkeys,
+ maskedKeys: masked,
+ viewKeys: vkeys,
+ viewFunction: vfn
+ };
+ }
+
+ function unbindKeys() {
+ keyHandler.viewKeys = {};
+ keyHandler.viewFn = null;
+ keyHandler.viewGestures = [];
+ }
+
+ angular.module('onosUtil')
+ .factory('KeyService',
+ ['$log', 'FnService', 'ThemeService', 'NavService',
+
+ function (_$log_, _fs_, _ts_, _ns_) {
+ $log = _$log_;
+ fs = _fs_;
+ ts = _ts_;
+ ns = _ns_;
+
+ return {
+ bindQhs: function (_qhs_) {
+ qhs = _qhs_;
+ },
+ installOn: function (elem) {
+ elem.on('keydown', keyIn);
+ setupGlobalKeys();
+ },
+ keyBindings: function (x) {
+ if (x === undefined) {
+ return getKeyBindings();
+ } else {
+ setKeyBindings(x);
+ }
+ },
+ unbindKeys: unbindKeys,
+ gestureNotes: function (g) {
+ if (g === undefined) {
+ return keyHandler.viewGestures;
+ } else {
+ keyHandler.viewGestures = fs.isA(g) || [];
+ }
+ },
+ enableKeys: function (b) {
+ enabled = b;
+ }
+ };
+ }]);
+
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/fw/util/prefs.js b/framework/src/onos/web/gui/src/main/webapp/app/fw/util/prefs.js
new file mode 100644
index 00000000..02a23909
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/fw/util/prefs.js
@@ -0,0 +1,128 @@
+/*
+ * 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 -- Util -- User Preference Service
+ */
+(function () {
+ 'use strict';
+
+ // injected refs
+ var $log, $cookies, fs;
+
+ // internal state
+ var cache = {};
+
+ // NOTE: in Angular 1.3.5, $cookies is just a simple object, and
+ // cookie values are just strings. From the 1.3.5 docs:
+ //
+ // "Only a simple Object is exposed and by adding or removing
+ // properties to/from this object, new cookies are created/deleted
+ // at the end of current $eval. The object's properties can only
+ // be strings."
+ //
+ // We may want to upgrade the version of Angular sometime soon
+ // since later version support objects as cookie values.
+
+ // NOTE: prefs represented as simple name/value pairs
+ // => a temporary restriction while we are encoding into cookies
+ /*
+ {
+ foo: 1,
+ bar: 0,
+ goo: 2
+ }
+
+ stored as "foo:1,bar:0,goo:2"
+ */
+
+ // reads cookie with given name and returns an object rep of its value
+ // or null if no such cookie is set
+ function getPrefs(name) {
+ var cook = $cookies[name],
+ bits,
+ obj = {};
+
+ if (cook) {
+ bits = cook.split(',');
+ bits.forEach(function (value) {
+ var x = value.split(':');
+ obj[x[0]] = x[1];
+ });
+
+ // update the cache
+ cache[name] = obj;
+ return obj;
+ }
+ // perhaps we have a cached copy..
+ return cache[name];
+ }
+
+ // converts string values to numbers for selected (or all) keys
+ function asNumbers(obj, keys) {
+ if (!obj) return null;
+
+ if (!keys) {
+ // do them all
+ angular.forEach(obj, function (v, k) {
+ obj[k] = Number(obj[k]);
+ });
+ } else {
+ keys.forEach(function (k) {
+ obj[k] = Number(obj[k]);
+ });
+ }
+ return obj;
+ }
+
+ function setPrefs(name, obj) {
+ var bits = [],
+ str;
+
+ angular.forEach(obj, function (value, key) {
+ bits.push(key + ':' + value);
+ });
+ str = bits.join(',');
+
+ // keep a cached copy of the object
+ cache[name] = obj;
+
+ // The angular way of doing this...
+ // $cookies[name] = str;
+ // ...but it appears that this gets delayed, and doesn't 'stick' ??
+
+ // FORCE cookie to be set by writing directly to document.cookie...
+ document.cookie = name + '=' + encodeURIComponent(str);
+ if (fs.debugOn('prefs')) {
+ $log.debug('<<>> Wrote cookie <'+name+'>:', str);
+ }
+ }
+
+ angular.module('onosUtil')
+ .factory('PrefsService', ['$log', '$cookies', 'FnService',
+ function (_$log_, _$cookies_, _fs_) {
+ $log = _$log_;
+ $cookies = _$cookies_;
+ fs = _fs_;
+
+ return {
+ getPrefs: getPrefs,
+ asNumbers: asNumbers,
+ setPrefs: setPrefs
+ };
+ }]);
+
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/fw/util/random.js b/framework/src/onos/web/gui/src/main/webapp/app/fw/util/random.js
new file mode 100644
index 00000000..2298a944
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/fw/util/random.js
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2014,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 -- Random -- Encapsulated randomness
+ */
+(function () {
+ 'use strict';
+
+ var $log, fs;
+
+ var halfRoot2 = 0.7071;
+
+ // given some value, s, returns an integer between -s/2 and s/2
+ // e.g. s = 100; result in the range [-50..50)
+ function spread(s) {
+ return Math.floor((Math.random() * s) - s / 2);
+ }
+
+ // for a given dimension, d, choose a random value somewhere between
+ // 0 and d where the value is within (d / (2 * sqrt(2))) of d/2.
+ function randDim(d) {
+ return d / 2 + spread(d * halfRoot2);
+ }
+
+ angular.module('onosUtil')
+ .factory('RandomService', ['$log', 'FnService',
+
+ function (_$log_, _fs_) {
+ $log = _$log_;
+ fs = _fs_;
+
+ return {
+ spread: spread,
+ randDim: randDim
+ };
+ }]);
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/fw/util/theme.js b/framework/src/onos/web/gui/src/main/webapp/app/fw/util/theme.js
new file mode 100644
index 00000000..0e0ee649
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/fw/util/theme.js
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2014,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 -- Util -- Theme Service
+ */
+(function () {
+ 'use strict';
+
+ var $log, fs;
+
+ var themes = ['light', 'dark'],
+ themeStr = themes.join(' '),
+ thidx,
+ listeners = {},
+ nextListenerId = 1;
+
+ function init() {
+ thidx = 0;
+ updateBodyClass();
+ }
+
+ function getTheme() {
+ return themes[thidx];
+ }
+
+ function setTheme(t) {
+ var idx = themes.indexOf(t);
+ if (idx > -1 && idx !== thidx) {
+ thidx = idx;
+ updateBodyClass();
+ themeEvent('set');
+ }
+ }
+
+ function toggleTheme() {
+ var i = thidx + 1;
+ thidx = (i===themes.length) ? 0 : i;
+ updateBodyClass();
+ themeEvent('toggle');
+ return getTheme();
+ }
+
+ function updateBodyClass() {
+ var body = d3.select('body');
+ body.classed(themeStr, false);
+ body.classed(getTheme(), true);
+ }
+
+ function themeEvent(w) {
+ var t = getTheme(),
+ m = 'Theme-Change-('+w+'): ' + t;
+ $log.debug(m);
+ angular.forEach(listeners, function(value) {
+ value.cb(
+ {
+ event: 'themeChange',
+ value: t
+ }
+ );
+ });
+ }
+
+ function addListener(callback) {
+ var id = nextListenerId++,
+ cb = fs.isF(callback),
+ o = { id: id, cb: cb };
+
+ if (cb) {
+ listeners[id] = o;
+ } else {
+ $log.error('ThemeService.addListener(): callback not a function');
+ o.error = 'No callback defined';
+ }
+ return o;
+ }
+
+ function removeListener(lsnr) {
+ var id = lsnr && lsnr.id,
+ o = listeners[id];
+ if (o) {
+ delete listeners[id];
+ }
+ }
+
+ angular.module('onosUtil')
+ .factory('ThemeService', ['$log', 'FnService',
+ function (_$log_, _fs_) {
+ $log = _$log_;
+ fs = _fs_;
+ thidx = 0;
+
+ return {
+ init: init,
+ theme: function (x) {
+ if (x === undefined) {
+ return getTheme();
+ } else {
+ setTheme(x);
+ }
+ },
+ toggleTheme: toggleTheme,
+ addListener: addListener,
+ removeListener: removeListener
+ };
+ }]);
+
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/fw/util/util.js b/framework/src/onos/web/gui/src/main/webapp/app/fw/util/util.js
new file mode 100644
index 00000000..dc3b09c2
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/fw/util/util.js
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2014,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 -- Utilities Module
+ */
+(function () {
+ 'use strict';
+
+ angular.module('onosUtil', []);
+
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/fw/widget/button.css b/framework/src/onos/web/gui/src/main/webapp/app/fw/widget/button.css
new file mode 100644
index 00000000..9ae03595
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/fw/widget/button.css
@@ -0,0 +1,120 @@
+/*
+ * 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 -- Button Service -- CSS file
+ */
+
+.button,
+.toggleButton,
+.radioSet {
+ display: inline-block;
+ padding: 0 4px;
+}
+.radioButton {
+ display: inline-block;
+ padding: 0 2px;
+}
+
+.button svg.embeddedIcon,
+.toggleButton svg.embeddedIcon,
+.radioButton svg.embeddedIcon {
+ cursor: pointer;
+}
+.button svg.embeddedIcon .icon rect,
+.toggleButton svg.embeddedIcon .icon rect,
+.radioButton svg.embeddedIcon .icon rect{
+ stroke: none;
+}
+
+/* Selected button color */
+.light .button svg.embeddedIcon .icon rect,
+.light .toggleButton.selected svg.embeddedIcon .icon rect,
+.light .radioButton.selected svg.embeddedIcon .icon rect {
+ fill: #838383;
+}
+
+.light .button:hover svg.embeddedIcon .icon rect,
+.light .toggleButton.selected:hover svg.embeddedIcon .icon rect
+/* NOTE: selected radio button should NOT have hover highlight */
+{
+ fill: #444444;
+}
+
+.light .button svg.embeddedIcon .glyph,
+.light .toggleButton.selected svg.embeddedIcon .glyph,
+.light .radioButton.selected svg.embeddedIcon .glyph {
+ fill: white;
+}
+
+.dark .button svg.embeddedIcon .icon rect,
+.dark .toggleButton.selected svg.embeddedIcon .icon rect,
+.dark .radioButton.selected svg.embeddedIcon .icon rect {
+ fill: #151515;
+}
+
+.dark .button:hover svg.embeddedIcon .icon rect,
+.dark .toggleButton.selected:hover svg.embeddedIcon .icon rect
+/* NOTE: selected radio button should NOT have hover highlight */
+{
+ fill: #444;
+}
+
+.dark .button svg.embeddedIcon .glyph,
+.dark .toggleButton.selected svg.embeddedIcon .glyph,
+.dark .radioButton.selected svg.embeddedIcon .glyph {
+ fill: #B2B2B2;
+}
+
+/* Unselected button color */
+.light .toggleButton svg.embeddedIcon .icon rect,
+.light .radioButton svg.embeddedIcon .icon rect {
+ fill: #eee;
+}
+
+.light .toggleButton:hover svg.embeddedIcon .icon rect,
+.light .radioButton:hover:not(.selected) svg.embeddedIcon .icon rect {
+ fill: #ccc;
+}
+
+.light .toggleButton svg.embeddedIcon .glyph,
+.light .radioButton svg.embeddedIcon .glyph {
+ fill: #bbb;
+}
+.light .toggleButton:hover:not(.selected) svg.embeddedIcon .glyph,
+.light .radioButton:hover:not(.selected) svg.embeddedIcon .glyph {
+ fill: #999;
+}
+
+.dark .toggleButton svg.embeddedIcon .icon rect,
+.dark .radioButton svg.embeddedIcon .icon rect {
+ fill: #303030;
+}
+
+.dark .toggleButton:hover:not(.selected) svg.embeddedIcon .icon rect,
+.dark .radioButton:hover:not(.selected) svg.embeddedIcon .icon rect {
+ fill: #555;
+}
+
+.dark .toggleButton svg.embeddedIcon .glyph,
+.dark .radioButton svg.embeddedIcon .glyph {
+ fill: #565656;
+}
+
+.dark .toggleButton:hover:not(.selected) svg.embeddedIcon .glyph,
+.dark .radioButton:hover:not(.selected) svg.embeddedIcon .glyph {
+ fill: #777;
+}
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/fw/widget/button.js b/framework/src/onos/web/gui/src/main/webapp/app/fw/widget/button.js
new file mode 100644
index 00000000..09cdd43a
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/fw/widget/button.js
@@ -0,0 +1,263 @@
+/*
+ * 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 -- Widget -- Button Service
+ */
+(function () {
+ 'use strict';
+
+ // injected refs
+ var $log, fs, is, tts;
+
+ // configuration
+ var btnSize = 25,
+ btnPadding = 4;
+
+
+ // === Helper Functions
+
+ function divExists(div, msg) {
+ if (!div) {
+ $log.warn('div undefined (' + msg + ')');
+ }
+ return !!div;
+ }
+
+ function createDiv(div, cls, id) {
+ return div.append('div')
+ .classed(cls, true)
+ .attr('id', id);
+ }
+
+ function noop() {}
+
+ function buttonWidth() {
+ return btnSize + 2 * btnPadding;
+ }
+
+ // === BUTTON =================================================
+
+ // div is where to put the button (d3.selection of a DIV element)
+ // id should be globally unique
+ // gid is glyph ID (from Glyph Service)
+ // cb is callback function on click
+ // tooltip is text for tooltip
+ function button(div, id, gid, cb, tooltip) {
+ if (!divExists(div, 'button')) return null;
+
+ var btnDiv = createDiv(div, 'button', id),
+ cbFnc = fs.isF(cb) || noop;
+
+ is.loadIcon(btnDiv, gid, btnSize, true);
+ if (tooltip) { tts.addTooltip(btnDiv, tooltip); }
+
+ btnDiv.on('click', cbFnc);
+
+ return {
+ id: id,
+ width: buttonWidth
+ }
+ }
+
+
+ // === TOGGLE BUTTON ==========================================
+
+ // div is where to put the button (d3.selection of a DIV element)
+ // id should be globally unique
+ // gid is glyph ID (from Glyph Service)
+ // initState is whether the toggle is on or not to begin
+ // cb is callback function on click
+ // tooltip is text for tooltip
+ function toggle(div, id, gid, initState, cb, tooltip) {
+ if (!divExists(div, 'toggle button')) return null;
+
+ var sel = !!initState,
+ togDiv = createDiv(div, 'toggleButton', id),
+ cbFnc = fs.isF(cb) || noop;
+
+ is.loadIcon(togDiv, gid, btnSize, true);
+ togDiv.classed('selected', sel);
+ if (tooltip) { tts.addTooltip(togDiv, tooltip); }
+
+ function _toggle(b, nocb) {
+ sel = (b === undefined) ? !sel : !!b;
+ togDiv.classed('selected', sel);
+ nocb || cbFnc(sel);
+ }
+
+ // toggle the button state without invoking the callback
+ function toggleNoCb() {
+ _toggle(undefined, true);
+ }
+
+ togDiv.on('click', _toggle);
+
+ return {
+ id: id,
+ width: buttonWidth,
+ selected: function () { return sel; },
+ toggle: _toggle,
+ toggleNoCb: toggleNoCb
+ }
+ }
+
+
+ // === RADIO BUTTON SET =======================================
+
+
+ // div is where to put the button (d3.selection of a DIV element)
+ // id should be globally unique
+ // rset is an array of button descriptors of the following form:
+ // {
+ // gid: glyphId,
+ // tooltip: tooltipText,
+ // cb: callbackFunction
+ // }
+ function radioSet(div, id, rset) {
+ if (!divExists(div, 'radio button set')) return null;
+
+ if (!fs.isA(rset) || !rset.length) {
+ $log.warn('invalid array (radio button set)');
+ return null;
+ }
+
+ var rDiv = createDiv(div, 'radioSet', id),
+ rads = [],
+ idxByKey = {},
+ currIdx = 0;
+
+ function rsetWidth() {
+ return ((btnSize + btnPadding) * rads.length) + btnPadding;
+ }
+
+ function rbclick() {
+ var id = d3.select(this).attr('id'),
+ m = /^.*-(\d+)$/.exec(id),
+ idx = Number(m[1]);
+
+ if (idx !== currIdx) {
+ rads[currIdx].el.classed('selected', false);
+ currIdx = idx;
+ rads[currIdx].el.classed('selected', true);
+ invokeCurrent();
+ }
+ }
+
+ // {
+ // gid: gid,
+ // tooltip: ..., (optional)
+ // key: ..., (optional)
+ // cb: cb
+ // id: ... (added by us)
+ // index: ... (added by us)
+ // }
+
+ rset.forEach(function (btn, index) {
+
+ if (!fs.isO(btn)) {
+ $log.warn('radio button descriptor at index ' + index +
+ ' not an object');
+ return;
+ }
+
+ var rid = id + '-' + index,
+ initSel = (index === 0),
+ rbdiv = createDiv(rDiv, 'radioButton', rid);
+
+ rbdiv.classed('selected', initSel);
+ rbdiv.on('click', rbclick);
+ is.loadIcon(rbdiv, btn.gid, btnSize, true);
+ if (btn.tooltip) { tts.addTooltip(rbdiv, btn.tooltip); }
+ angular.extend(btn, {
+ el: rbdiv,
+ id: rid,
+ cb: fs.isF(btn.cb) || noop,
+ index: index
+ });
+
+ if (btn.key) {
+ idxByKey[btn.key] = index;
+ }
+
+ rads.push(btn);
+ });
+
+
+ function invokeCurrent() {
+ var curr = rads[currIdx];
+ curr.cb(curr.index, curr.key);
+ }
+
+ function selected(x) {
+ var curr = rads[currIdx],
+ idx;
+
+ if (x === undefined) {
+ return curr.key || curr.index;
+ } else {
+ idx = idxByKey[x];
+ if (idx === undefined) {
+ $log.warn('no radio button with key:', x);
+ } else {
+ selectedIndex(idx);
+ }
+ }
+ }
+
+ function selectedIndex(x) {
+ if (x === undefined) {
+ return currIdx;
+ } else {
+ if (x >= 0 && x < rads.length) {
+ if (currIdx !== x) {
+ currIdx = x;
+ invokeCurrent();
+ } else {
+ $log.warn('current index already selected:', x);
+ }
+ } else {
+ $log.warn('invalid radio button index:', x);
+ }
+ }
+ }
+
+ return {
+ width: rsetWidth,
+ selected: selected,
+ selectedIndex: selectedIndex
+ }
+ }
+
+
+ angular.module('onosWidget')
+ .factory('ButtonService',
+ ['$log', 'FnService', 'IconService', 'TooltipService',
+
+ function (_$log_, _fs_, _is_, _tts_) {
+ $log = _$log_;
+ fs = _fs_;
+ is = _is_;
+ tts = _tts_;
+
+ return {
+ button: button,
+ toggle: toggle,
+ radioSet: radioSet
+ };
+ }]);
+
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/fw/widget/table.css b/framework/src/onos/web/gui/src/main/webapp/app/fw/widget/table.css
new file mode 100644
index 00000000..18b81ba6
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/fw/widget/table.css
@@ -0,0 +1,215 @@
+/*
+ * 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.
+ */
+
+/* ------ for summary-list tables ------ */
+
+div.summary-list {
+ margin: 0 20px 16px 10px;
+ font-size: 10pt;
+ border-spacing: 0;
+}
+
+div.loading-wheel {
+ display: inline-block;
+ position: absolute;
+ margin-top: 40px;
+ left: 47%;
+ animation: spin reverse 2s ease infinite;
+ z-index: 1000;
+}
+
+@keyframes spin {
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+div.loading-wheel svg.embeddedIcon g.icon .glyph {
+ opacity: .8;
+}
+.light div.loading-wheel svg.embeddedIcon g.icon .glyph {
+ fill: #964949;
+}
+.dark div.loading-wheel svg.embeddedIcon g.icon .glyph {
+ fill: whitesmoke;
+}
+
+div.summary-list table {
+ border-collapse: collapse;
+ table-layout: fixed;
+ empty-cells: show;
+ margin: 0;
+}
+
+div.summary-list div.table-body {
+ overflow-y: scroll;
+}
+
+div.summary-list div.table-body::-webkit-scrollbar {
+ display: none;
+}
+
+div.summary-list tr.no-data td {
+ text-align: center;
+ font-style: italic;
+}
+
+.light div.summary-list tr:nth-child(even) {
+ background-color: #ddd;
+}
+.light div.summary-list tr:nth-child(odd) {
+ background-color: #eee;
+}
+.dark div.summary-list tr:nth-child(even) {
+ background-color: #333;
+}
+.dark div.summary-list tr:nth-child(odd) {
+ background-color: #444;
+}
+
+.light div.summary-list tr.selected {
+ background-color: deepskyblue;
+}
+
+.dark div.summary-list tr.selected {
+ background-color: #304860;
+}
+
+/* highlighting */
+div.summary-list tr {
+ transition: background-color 500ms;
+}
+.light div.summary-list tr.data-change {
+ background-color: #FDFFDC;
+}
+.dark div.summary-list tr.data-change {
+ background-color: #5A5600;
+}
+
+div.summary-list td {
+ padding: 6px;
+ text-align: left;
+ word-wrap: break-word;
+}
+
+div.summary-list .table-header td {
+ letter-spacing: 0.02em;
+ cursor: pointer;
+ font-weight: bold;
+}
+div.summary-list .table-header td:first-child {
+ border-radius: 8px 0 0 0;
+}
+div.summary-list .table-header td:last-child {
+ border-radius: 0 8px 0 0;
+}
+
+.light div.summary-list .table-header td {
+ background-color: #bbb;
+}
+.dark div.summary-list .table-header td {
+ background-color: #222;
+ color: #ccc;
+}
+
+/* rows are selectable */
+div.summary-list .table-body td {
+ cursor: pointer;
+}
+
+.dark div.summary-list td {
+ color: #ccc;
+}
+
+/* Tabular view upper right control buttons */
+
+div.ctrl-btns {
+ display: inline-block;
+ float: right;
+ height: 44px;
+ margin-right: 24px;
+ margin-top: 7px;
+}
+
+
+div.ctrl-btns div {
+ display: inline-block;
+ padding: 4px;
+ cursor: pointer;
+}
+
+div.ctrl-btns div.separator {
+ cursor: auto;
+ width: 24px;
+ border: none;
+}
+
+/* Inactive */
+.light .ctrl-btns div g.icon rect,
+.light .ctrl-btns div:hover g.icon rect {
+ fill: #eee;
+}
+.dark .ctrl-btns div g.icon rect,
+.dark .ctrl-btns div:hover g.icon rect {
+ fill: #222;
+}
+
+.light .ctrl-btns div g.icon use {
+ fill: #ddd;
+}
+.dark .ctrl-btns div g.icon use {
+ fill: #333;
+}
+
+/* Active hover */
+.light .ctrl-btns div.active:hover g.icon rect {
+ fill: #800;
+}
+
+.dark .ctrl-btns div.active:hover g.icon rect {
+ fill: #CE5650;
+}
+
+/* Active */
+.light .ctrl-btns div.active g.icon use {
+ fill: #fff;
+}
+.dark .ctrl-btns div.active g.icon use {
+ fill: #eee;
+}
+
+.light .ctrl-btns div.active g.icon rect {
+ fill: #bbb;
+}
+.dark .ctrl-btns div.active g.icon rect {
+ fill: #444;
+}
+
+/* Refresh button specific */
+.light .ctrl-btns div.refresh.active g.icon rect {
+ fill: #964949;
+}
+
+.dark .ctrl-btns div.refresh.active g.icon rect {
+ fill: #9B4641;
+}
+.light .ctrl-btns div.refresh:hover g.icon rect {
+ fill: #964949;
+}
+
+.dark .ctrl-btns div.refresh:hover g.icon rect {
+ fill: #9B4641;
+}
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/fw/widget/table.js b/framework/src/onos/web/gui/src/main/webapp/app/fw/widget/table.js
new file mode 100644
index 00000000..327aedb9
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/fw/widget/table.js
@@ -0,0 +1,272 @@
+/*
+ * 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 -- Widget -- Table Service
+ */
+(function () {
+ 'use strict';
+
+ // injected refs
+ var $log, $window, fs, mast, is;
+
+ // constants
+ var tableIconTdSize = 33,
+ pdg = 22,
+ flashTime = 1500,
+ colWidth = 'col-width',
+ tableIcon = 'table-icon',
+ asc = 'asc',
+ desc = 'desc',
+ none = 'none';
+
+ // internal state
+ var currCol = {},
+ prevCol = {},
+ cstmWidths = {},
+ sortIconAPI;
+
+ // Functions for resizing a tabular view to the window
+
+ function _width(elem, width) {
+ elem.style('width', width);
+ }
+
+ function findCstmWidths(table) {
+ var headers = table.select('.table-header').selectAll('td');
+
+ headers.each(function (d, i) {
+ var h = d3.select(this),
+ index = i.toString();
+ if (h.classed(tableIcon)) {
+ cstmWidths[index] = tableIconTdSize + 'px';
+ }
+ if (h.attr(colWidth)) {
+ cstmWidths[index] = h.attr(colWidth);
+ }
+ });
+ if (fs.debugOn('widget')) {
+ $log.debug('Headers with custom widths: ', cstmWidths);
+ }
+ }
+
+ function setTdWidths(elem, width) {
+ var tds = elem.select('tr:first-child').selectAll('td');
+ _width(elem, width + 'px');
+
+ tds.each(function (d, i) {
+ var td = d3.select(this),
+ index = i.toString();
+ if (cstmWidths.hasOwnProperty(index)) {
+ _width(td, cstmWidths[index]);
+ }
+ });
+ }
+
+ function setHeight(thead, body, height) {
+ var h = height - (mast.mastHeight() +
+ fs.noPxStyle(d3.select('.tabular-header'), 'height') +
+ fs.noPxStyle(thead, 'height') + pdg);
+ body.style('height', h + 'px');
+ }
+
+ function adjustTable(haveItems, tableElems, width, height) {
+ if (haveItems) {
+ setTdWidths(tableElems.thead, width);
+ setTdWidths(tableElems.tbody, width);
+ setHeight(tableElems.thead, tableElems.table.select('.table-body'), height);
+ } else {
+ setTdWidths(tableElems.thead, width);
+ _width(tableElems.tbody, width + 'px');
+ }
+ }
+
+ // Functions for sorting table rows by header
+
+ function updateSortDirection(thElem) {
+ sortIconAPI.sortNone(thElem.select('div'));
+ currCol.div = thElem.append('div');
+ currCol.colId = thElem.attr('colId');
+
+ if (currCol.colId === prevCol.colId) {
+ (currCol.dir === desc) ? currCol.dir = asc : currCol.dir = desc;
+ prevCol.dir = currCol.dir;
+ } else {
+ currCol.dir = asc;
+ prevCol.dir = none;
+ }
+ (currCol.dir === asc) ?
+ sortIconAPI.sortAsc(currCol.div) : sortIconAPI.sortDesc(currCol.div);
+
+ if (prevCol.colId && prevCol.dir === none) {
+ sortIconAPI.sortNone(prevCol.div);
+ }
+
+ prevCol.colId = currCol.colId;
+ prevCol.div = currCol.div;
+ }
+
+ function sortRequestParams() {
+ return {
+ sortCol: currCol.colId,
+ sortDir: currCol.dir
+ };
+ }
+
+ function resetSort() {
+ if (currCol.div) {
+ sortIconAPI.sortNone(currCol.div);
+ }
+ if (prevCol.div) {
+ sortIconAPI.sortNone(prevCol.div);
+ }
+ currCol = {};
+ prevCol = {};
+ }
+
+ angular.module('onosWidget')
+ .directive('onosTableResize', ['$log','$window',
+ 'FnService', 'MastService',
+
+ function (_$log_, _$window_, _fs_, _mast_) {
+ return function (scope, element) {
+ $log = _$log_;
+ $window = _$window_;
+ fs = _fs_;
+ mast = _mast_;
+
+ var table = d3.select(element[0]),
+ tableElems = {
+ table: table,
+ thead: table.select('.table-header').select('table'),
+ tbody: table.select('.table-body').select('table')
+ },
+ wsz;
+
+ findCstmWidths(table);
+
+ // adjust table on window resize
+ scope.$watchCollection(function () {
+ return {
+ h: $window.innerHeight,
+ w: $window.innerWidth
+ };
+ }, function () {
+ wsz = fs.windowSize(0, 30);
+ adjustTable(
+ scope.tableData.length,
+ tableElems,
+ wsz.width, wsz.height
+ );
+ });
+
+ // adjust table when data changes
+ scope.$watchCollection('tableData', function () {
+ adjustTable(
+ scope.tableData.length,
+ tableElems,
+ wsz.width, wsz.height
+ );
+ });
+
+ scope.$on('$destroy', function () {
+ cstmWidths = {};
+ });
+ };
+ }])
+
+ .directive('onosSortableHeader', ['$log', 'IconService',
+ function (_$log_, _is_) {
+ return function (scope, element) {
+ $log = _$log_;
+ is = _is_;
+ var header = d3.select(element[0]);
+ sortIconAPI = is.sortIcons();
+
+ header.selectAll('td').on('click', function () {
+ var col = d3.select(this);
+
+ if (col.attr('sortable') === '') {
+ updateSortDirection(col);
+ scope.sortParams = sortRequestParams();
+ scope.sortCallback(scope.sortParams);
+ }
+ });
+
+ scope.$on('$destroy', function () {
+ resetSort();
+ });
+ };
+ }])
+
+ .directive('onosFlashChanges',
+ ['$log', '$parse', '$timeout', 'FnService',
+ function ($log, $parse, $timeout, fs) {
+
+ return function (scope, element, attrs) {
+ var idProp = attrs.idProp,
+ table = d3.select(element[0]),
+ trs, promise;
+
+ function highlightRows() {
+ var changedRows = [];
+ function classRows(b) {
+ if (changedRows.length) {
+ angular.forEach(changedRows, function (tr) {
+ tr.classed('data-change', b);
+ });
+ }
+ }
+ // timeout because 'row-id' was the un-interpolated value
+ // "{{link.one}}" for example, instead of link.one evaluated
+ // timeout executes on the next digest -- after evaluation
+ $timeout(function () {
+ if (scope.tableData.length) {
+ trs = table.selectAll('tr');
+ }
+
+ if (trs && !trs.empty()) {
+ trs.each(function () {
+ var tr = d3.select(this);
+ if (fs.find(tr.attr('row-id'),
+ scope.changedData,
+ idProp) > -1) {
+ changedRows.push(tr);
+ }
+ });
+ classRows(true);
+ promise = $timeout(function () {
+ classRows(false);
+ }, flashTime);
+ trs = undefined;
+ }
+ });
+ }
+
+ // new items added:
+ scope.$on('ngRepeatComplete', highlightRows);
+ // items changed in existing set:
+ scope.$watchCollection('changedData', highlightRows);
+
+ scope.$on('$destroy', function () {
+ if (promise) {
+ $timeout.cancel(promise);
+ }
+ });
+ };
+ }]);
+
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/fw/widget/tableBuilder.js b/framework/src/onos/web/gui/src/main/webapp/app/fw/widget/tableBuilder.js
new file mode 100644
index 00000000..24161bbb
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/fw/widget/tableBuilder.js
@@ -0,0 +1,165 @@
+/*
+ * 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 -- Widget -- Table Service
+ */
+(function () {
+ 'use strict';
+
+ // injected refs
+ var $log, $interval, $timeout, fs, wss;
+
+ // constants
+ var refreshInterval = 2000,
+ loadingWait = 500;
+
+ // example params to buildTable:
+ // {
+ // scope: $scope, <- controller scope
+ // tag: 'device', <- table identifier
+ // selCb: selCb, <- row selection callback (optional)
+ // respCb: respCb, <- websocket response callback (optional)
+ // query: params <- query parameters in URL (optional)
+ // }
+ // Note: selCb() is passed the row data model of the selected row,
+ // or null when no row is selected.
+ // Note: query is always an object (empty or containing properties)
+ // it comes from $location.search()
+
+ function buildTable(o) {
+ var handlers = {},
+ root = o.tag + 's',
+ req = o.tag + 'DataRequest',
+ resp = o.tag + 'DataResponse',
+ onSel = fs.isF(o.selCb),
+ onResp = fs.isF(o.respCb),
+ oldTableData = [],
+ loaded = false,
+ refreshPromise, loadingPromise;
+
+ o.scope.tableData = [];
+ o.scope.changedData = [];
+ o.scope.sortParams = {};
+ o.scope.loading = true;
+ o.scope.autoRefresh = true;
+ o.scope.autoRefreshTip = 'Toggle auto refresh';
+
+ // === websocket functions --------------------
+ // response
+ function respCb(data) {
+ loaded = true;
+ o.scope.loading = false;
+ o.scope.tableData = data[root];
+ onResp && onResp();
+
+ // checks if data changed for row flashing
+ if (!angular.equals(o.scope.tableData, oldTableData)) {
+ o.scope.changedData = [];
+ // only flash the row if the data already exists
+ if (oldTableData.length) {
+ angular.forEach(o.scope.tableData, function (item) {
+ if (!fs.containsObj(oldTableData, item)) {
+ o.scope.changedData.push(item);
+ }
+ });
+ }
+ angular.copy(o.scope.tableData, oldTableData);
+ }
+ o.scope.$apply();
+ }
+ handlers[resp] = respCb;
+ wss.bindHandlers(handlers);
+
+ // request
+ function sortCb(params) {
+ var p = angular.extend({}, params, o.query);
+ wss.sendEvent(req, p);
+ stillLoading();
+ }
+ o.scope.sortCallback = sortCb;
+
+ // show loading wheel if it's taking a while for the server to respond
+ function stillLoading() {
+ loaded = false;
+ loadingPromise = $timeout(function () {
+ if (!loaded) {
+ o.scope.loading = true;
+ }
+ }, loadingWait);
+ }
+
+ // === selecting a row functions ----------------
+ function selCb($event, selRow) {
+ o.scope.selId = (o.scope.selId === selRow.id) ? null : selRow.id;
+ onSel && onSel($event, selRow);
+ }
+ o.scope.selectCallback = selCb;
+
+ // === autoRefresh functions ------------------
+ function startRefresh() {
+ refreshPromise = $interval(function () {
+ if (fs.debugOn('widget')) {
+ $log.debug('Refreshing ' + root + ' page');
+ }
+ sortCb(o.scope.sortParams);
+ }, refreshInterval);
+ }
+
+ function stopRefresh() {
+ if (angular.isDefined(refreshPromise)) {
+ $interval.cancel(refreshPromise);
+ refreshPromise = undefined;
+ }
+ }
+
+ function toggleRefresh() {
+ o.scope.autoRefresh = !o.scope.autoRefresh;
+ o.scope.autoRefresh ? startRefresh() : stopRefresh();
+ }
+ o.scope.toggleRefresh = toggleRefresh;
+
+ // === Cleanup on destroyed scope -----------------
+ o.scope.$on('$destroy', function () {
+ wss.unbindHandlers(handlers);
+ stopRefresh();
+ if (angular.isDefined(loadingPromise)) {
+ $timeout.cancel(loadingPromise);
+ loadingPromise = undefined;
+ }
+ });
+
+ sortCb();
+ startRefresh();
+ }
+
+ angular.module('onosWidget')
+ .factory('TableBuilderService',
+ ['$log', '$interval', '$timeout', 'FnService', 'WebSocketService',
+
+ function (_$log_, _$interval_, _$timeout_, _fs_, _wss_) {
+ $log = _$log_;
+ $interval = _$interval_;
+ $timeout = _$timeout_;
+ fs = _fs_;
+ wss = _wss_;
+
+ return {
+ buildTable: buildTable
+ };
+ }]);
+
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/fw/widget/toolbar.css b/framework/src/onos/web/gui/src/main/webapp/app/fw/widget/toolbar.css
new file mode 100644
index 00000000..36a5971b
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/fw/widget/toolbar.css
@@ -0,0 +1,77 @@
+/*
+ * 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 -- Toolbar Service -- CSS file
+ */
+
+.light .tbar-arrow svg.embeddedIcon .icon .glyph {
+ fill: #838383;
+}
+.dark .tbar-arrow svg.embeddedIcon .icon .glyph {
+ fill: #B2B2B2;
+}
+
+
+div.tbar-arrow {
+ position: absolute;
+ top: 53%;
+ left: 96%;
+ margin-right: -4%;
+ transform: translate(-50%, -50%);
+ cursor: pointer;
+}
+.safari div.tbar-arrow {
+ top: 46%;
+}
+.firefox div.tbar-arrow {
+ left: 97%;
+ margin-right: -3%;
+}
+
+.tbar-arrow svg.embeddedIcon .icon rect {
+ stroke: none;
+}
+.light .tbar-arrow svg.embeddedIcon .icon rect {
+ fill: none;
+}
+.dark .tbar-arrow svg.embeddedIcon .icon rect {
+ fill: none;
+}
+
+
+.toolbar {
+ line-height: 125%;
+}
+.tbar-row {
+ display: inline-block;
+}
+
+
+
+.separator {
+ border: 1px solid;
+ margin: 0 4px 0 4px;
+ display: inline-block;
+ height: 23px;
+ width: 0;
+}
+.light .separator {
+ border-color: #ddd;
+}
+.dark .separator {
+ border-color: #1A1A1A;
+}
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/fw/widget/toolbar.js b/framework/src/onos/web/gui/src/main/webapp/app/fw/widget/toolbar.js
new file mode 100644
index 00000000..050afd0f
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/fw/widget/toolbar.js
@@ -0,0 +1,268 @@
+/*
+ * 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 -- Widget -- Toolbar Service
+ */
+// TODO: Augment service to allow toolbars to exist on right edge of screen
+// TODO: also - make toolbar more object aware (rows etc.)
+
+
+(function () {
+ 'use strict';
+
+ // injected refs
+ var $log, fs, ps, bns, is;
+
+ // configuration
+ var arrowSize = 10,
+ sepWidth = 6,
+ defaultSettings = {
+ edge: 'left',
+ width: 20,
+ margin: 0,
+ hideMargin: -20,
+ top: 'auto',
+ bottom: '10px',
+ fade: false,
+ shown: false
+ };
+
+ // internal state
+ var tbars = {};
+
+
+ // === Helper functions --------------------------------------
+
+ // translate uses 50 because the svg viewbox is 50
+ function rotateArrowLeft(adiv) {
+ adiv.select('g')
+ .attr('transform', 'translate(0 50) rotate(-90)');
+ }
+ function rotateArrowRight(adiv) {
+ adiv.select('g')
+ .attr('transform', 'translate(50 0) rotate(90)');
+ }
+
+ function createArrow(panel) {
+ var arrowDiv = panel.append('div')
+ .classed('tbar-arrow', true);
+ is.loadIcon(arrowDiv, 'triangleUp', arrowSize, true);
+ return arrowDiv;
+ }
+
+ function warn(msg, id) {
+ $log.warn('createToolbar: ' + msg + ': [' + id + ']');
+ return null;
+ }
+
+ // ==================================
+
+ function createToolbar(id, opts) {
+ if (!id) return warn('no ID given');
+ if (tbars[id]) return warn('duplicate ID given', id);
+
+ var settings = angular.extend({}, defaultSettings, fs.isO(opts)),
+ items = {},
+ tbid = 'toolbar-' + id,
+ panel = ps.createPanel(tbid, settings),
+ arrowDiv = createArrow(panel),
+ currentRow = panel.append('div').classed('tbar-row', true),
+ rowButtonIds = [], // for removable buttons
+ tbWidth = arrowSize + 2, // empty toolbar width
+ maxWidth = panel.width();
+
+ arrowDiv.on('click', toggle);
+
+ // add a descriptor for this toolbar
+ tbars[id] = {
+ settings: settings,
+ items: items,
+ panel: panel,
+ panelId: tbid
+ };
+
+ panel.classed('toolbar', true)
+ .style('top', settings.top)
+ .style('bottom', settings.bottom);
+
+ // Helper functions
+
+ function dupId(id, caller) {
+ if (items[id]) {
+ $log.warn(caller + ': duplicate ID:', id);
+ return true;
+ }
+ return false;
+ }
+
+ function adjustWidth(btnWidth) {
+ if (fs.noPxStyle(currentRow, 'width') >= maxWidth) {
+ tbWidth += btnWidth;
+ maxWidth = tbWidth;
+ }
+ panel.width(tbWidth);
+ }
+
+ // API functions
+
+ function addButton(id, gid, cb, tooltip) {
+ if (dupId(id, 'addButton')) return null;
+
+ var bid = tbid + '-' + id,
+ btn = bns.button(currentRow, bid, gid, cb, tooltip);
+
+ items[id] = btn;
+ adjustWidth(btn.width());
+ return btn;
+ }
+
+ function addToggle(id, gid, initState, cb, tooltip) {
+ if (dupId(id, 'addToggle')) return null;
+
+ var tid = tbid + '-' + id,
+ tog = bns.toggle(currentRow, tid, gid, initState, cb, tooltip);
+
+ items[id] = tog;
+ adjustWidth(tog.width());
+ return tog;
+ }
+
+ function addRadioSet(id, rset) {
+ if (dupId(id, 'addRadioSet')) return null;
+
+ var rid = tbid + '-' + id,
+ rad = bns.radioSet(currentRow, rid, rset);
+
+ items[id] = rad;
+ adjustWidth(rad.width());
+ return rad;
+ }
+
+ function addSeparator() {
+ currentRow.append('div')
+ .classed('separator', true);
+ tbWidth += sepWidth;
+ }
+
+ function addRow() {
+ if (currentRow.select('div').empty()) {
+ return null;
+ } else {
+ panel.append('br');
+ currentRow = panel.append('div').classed('tbar-row', true);
+
+ // return API to allow caller more access to the row
+ return {
+ clear: rowClear,
+ setText: rowSetText,
+ addButton: rowAddButton,
+ classed: rowClassed
+ };
+ }
+ }
+
+ function rowClear() {
+ currentRow.selectAll('*').remove();
+ rowButtonIds.forEach(function (bid) {
+ delete items[bid];
+ });
+ rowButtonIds = [];
+ }
+
+ // installs a div with text into the button row
+ function rowSetText(text) {
+ rowClear();
+ currentRow.append('div').classed('tbar-row-text', true)
+ .html(text);
+ }
+
+ function rowAddButton(id, gid, cb, tooltip) {
+ var b = addButton(id, gid, cb, tooltip);
+ if (b) {
+ rowButtonIds.push(id);
+ }
+ }
+
+ function rowClassed(classes, bool) {
+ currentRow.classed(classes, bool);
+ }
+
+ function show(cb) {
+ rotateArrowLeft(arrowDiv);
+ panel.show(cb);
+ }
+
+ function hide(cb) {
+ rotateArrowRight(arrowDiv);
+ panel.hide(cb);
+ }
+
+ function toggle(cb) {
+ if (panel.isVisible()) {
+ hide(cb);
+ } else {
+ show(cb);
+ }
+ }
+
+ return {
+ addButton: addButton,
+ addToggle: addToggle,
+ addRadioSet: addRadioSet,
+ addSeparator: addSeparator,
+ addRow: addRow,
+
+ show: show,
+ hide: hide,
+ toggle: toggle
+ };
+ }
+
+ function destroyToolbar(id) {
+ var tb = tbars[id];
+ delete tbars[id];
+
+ if (tb) {
+ ps.destroyPanel(tb.panelId);
+ }
+ }
+
+ // === Module Definition ===
+
+ angular.module('onosWidget')
+ .factory('ToolbarService',
+ ['$log', 'FnService', 'PanelService', 'ButtonService', 'IconService',
+
+ function (_$log_, _fs_, _ps_, _bns_, _is_) {
+ $log = _$log_;
+ fs = _fs_;
+ ps = _ps_;
+ bns = _bns_;
+ is = _is_;
+
+ // this function is only used in testing
+ function init() {
+ tbars = {};
+ }
+
+ return {
+ init: init,
+ createToolbar: createToolbar,
+ destroyToolbar: destroyToolbar
+ };
+ }]);
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/fw/widget/tooltip.css b/framework/src/onos/web/gui/src/main/webapp/app/fw/widget/tooltip.css
new file mode 100644
index 00000000..f967793c
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/fw/widget/tooltip.css
@@ -0,0 +1,44 @@
+/*
+ * 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 -- Tooltip Service -- CSS file
+ */
+
+#tooltip {
+ text-align: center;
+ font-size: 80%;
+ border: 1px solid;
+ padding: 5px;
+ position: absolute;
+ z-index: 5000;
+ display: none;
+ pointer-events: none;
+}
+
+/* colors subject to change */
+
+.light #tooltip {
+ background-color: #ddd;
+ color: #444;
+ border-color: #ccc;
+}
+
+.dark #tooltip {
+ background-color: #151515;
+ color: #B2B2B2;
+ border-color: #252525;
+} \ No newline at end of file
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/fw/widget/tooltip.js b/framework/src/onos/web/gui/src/main/webapp/app/fw/widget/tooltip.js
new file mode 100644
index 00000000..dd8a6950
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/fw/widget/tooltip.js
@@ -0,0 +1,146 @@
+/*
+ * 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 -- Widget -- Tooltip Service
+ */
+
+(function () {
+ 'use strict';
+
+ // injected references
+ var $log, $rootScope, fs;
+
+ // constants
+ var hoverHeight = 35,
+ hoverDelay = 150,
+ exitDelay = 150;
+
+ // internal state
+ var tooltip, currElemId;
+
+ // === Helper functions ---------------------------------------------
+
+ function init() {
+ tooltip = d3.select('#tooltip');
+ tooltip.html('');
+ }
+
+ function tipStyle(mouseX, mouseY) {
+ var winWidth = fs.windowSize().width,
+ winHeight = fs.windowSize().height,
+ style = {
+ display: 'inline-block',
+ left: 'auto',
+ right: 'auto'
+ };
+
+ if (mouseX <= (winWidth / 2)) {
+ style.left = mouseX + 'px';
+ } else {
+ style.right = (winWidth - mouseX) + 'px';
+ }
+
+ if (mouseY <= (winHeight / 2)) {
+ style.top = (mouseY + (hoverHeight - 10)) + 'px';
+ } else {
+ style.top = (mouseY - hoverHeight) + 'px';
+ }
+
+ return style;
+ }
+
+ // === API functions ------------------------------------------------
+
+ function addTooltip(elem, tooltip) {
+ elem.on('mouseover', function () { showTooltip(this, tooltip); });
+ elem.on('mouseout', function () { cancelTooltip(this); });
+ $rootScope.$on('$routeChangeStart', function () {
+ cancelTooltip(elem.node());
+ });
+ }
+
+ function showTooltip(el, msg) {
+ // tooltips don't make sense on mobile devices
+ if (!el || !msg || fs.isMobile()) {
+ return;
+ }
+
+ var elem = d3.select(el),
+ mouseX = d3.event.pageX,
+ mouseY = d3.event.pageY,
+ style = tipStyle(mouseX, mouseY);
+ currElemId = elem.attr('id');
+
+ tooltip.transition()
+ .delay(hoverDelay)
+ .each('start', function () {
+ d3.select(this).style('display', 'none');
+ })
+ .each('end', function () {
+ d3.select(this).style(style)
+ .text(msg);
+ });
+ }
+
+ function cancelTooltip(el) {
+ if (!el) {
+ return;
+ }
+ var elem = d3.select(el);
+
+ if (elem.attr('id') === currElemId) {
+ tooltip.transition()
+ .delay(exitDelay)
+ .style({
+ display: 'none'
+ })
+ .text('');
+ }
+ }
+
+ angular.module('onosWidget')
+
+ .directive('tooltip', ['$rootScope', 'FnService',
+ function (_$rootScope_, _fs_) {
+ $rootScope = _$rootScope_;
+ fs = _fs_;
+
+ init();
+
+ return {
+ restrict: 'A',
+ link: function (scope, elem, attrs) {
+ addTooltip(d3.select(elem[0]), scope[attrs.ttMsg]);
+ }
+ };
+ }])
+
+ .factory('TooltipService', ['$log', '$rootScope', 'FnService',
+ function (_$log_, _$rootScope_, _fs_) {
+ $log = _$log_;
+ $rootScope = _$rootScope_;
+ fs = _fs_;
+
+ init();
+
+ return {
+ addTooltip: addTooltip,
+ showTooltip: showTooltip,
+ cancelTooltip: cancelTooltip
+ };
+ }]);
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/fw/widget/widget.js b/framework/src/onos/web/gui/src/main/webapp/app/fw/widget/widget.js
new file mode 100644
index 00000000..d11c8287
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/fw/widget/widget.js
@@ -0,0 +1,25 @@
+/*
+ * 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 -- Widgets Module
+ */
+(function () {
+ 'use strict';
+
+ angular.module('onosWidget', []);
+
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/onos.css b/framework/src/onos/web/gui/src/main/webapp/app/onos.css
new file mode 100644
index 00000000..b42af35e
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/onos.css
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2014,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 -- core -- CSS file
+ */
+
+html {
+ font-family: sans-serif;
+ -webkit-text-size-adjust: 100%;
+ -ms-text-size-adjust: 100%;
+ height: 100%;
+}
+
+/*
+ overflow hidden is to ensure that the body does not expand to account
+ for any flyout panes, that are positioned "off screen".
+ */
+body {
+ height: 100%;
+ margin: 0;
+ overflow: hidden;
+}
+
+body.light {
+ background-color: white;
+}
+body.dark {
+ background-color: #2b2b2b;
+}
+
+#view {
+ padding: 6px;
+ width: 100%;
+ height: 100%;
+}
+
+#view h2 {
+ padding-left: 12px;
+}
+
+.light #view h2 {
+ color: #800;
+}
+.dark #view h2 {
+ color: #CE5650;
+}
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/app/app.css b/framework/src/onos/web/gui/src/main/webapp/app/view/app/app.css
new file mode 100644
index 00000000..9413369a
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/app/app.css
@@ -0,0 +1,32 @@
+/*
+ * 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 -- Host View -- CSS file
+ */
+
+#ov-app h2 {
+ display: inline-block;
+}
+
+#ov-app div.ctrl-btns {
+ width: 290px;
+}
+
+#ov-app form#inputFileForm,
+#ov-app input#uploadFile {
+ display: none;
+}
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/app/app.html b/framework/src/onos/web/gui/src/main/webapp/app/view/app/app.html
new file mode 100644
index 00000000..85b044fb
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/app/app.html
@@ -0,0 +1,80 @@
+<!-- app partial HTML -->
+<div id="ov-app">
+ <div class="tabular-header">
+ <h2>Applications ({{tableData.length}} total)</h2>
+ <div class="ctrl-btns">
+ <div class="refresh" ng-class="{active: autoRefresh}"
+ icon icon-size="36" icon-id="refresh"
+ tooltip tt-msg="autoRefreshTip"
+ ng-click="toggleRefresh()"></div>
+ <div class="separator"></div>
+
+ <form id="inputFileForm">
+ <input id="uploadFile"
+ type="file" size="50" accept=".oar"
+ file-model="appFile">
+ </form>
+ <div icon icon-size="36" icon-id="plus"
+ class="active" trigger-form
+ tooltip tt-msg="uploadTip">
+ </div>
+ <div icon icon-size="36" icon-id="play"
+ ng-click="appAction('activate')"
+ tooltip tt-msg="activateTip"
+ ng-class="{active: ctrlBtnState.installed}">
+ </div>
+ <div icon icon-size="36" icon-id="stop"
+ ng-click="appAction('deactivate')"
+ tooltip tt-msg="deactivateTip"
+ ng-class="{active: ctrlBtnState.active}">
+ </div>
+ <div icon icon-size="36" icon-id="garbage"
+ ng-click="appAction('uninstall')"
+ tooltip tt-msg="uninstallTip"
+ ng-class="{active: ctrlBtnState.selection}">
+ </div>
+ </div>
+ </div>
+
+ <div class="summary-list" onos-table-resize>
+ <div ng-show="loading" class="loading-wheel"
+ icon icon-id="loading" icon-size="75"></div>
+
+ <div class="table-header" onos-sortable-header>
+ <table>
+ <tr>
+ <td colId="state" class="table-icon" sortable></td>
+ <td colId="id" sortable>App ID </td>
+ <td colId="version" sortable>Version </td>
+ <td colId="origin" sortable>Origin </td>
+ <td colId="desc" col-width="475px">Description </td>
+ </tr>
+ </table>
+ </div>
+
+ <div class="table-body">
+ <table onos-flash-changes id-prop="id">
+ <tr ng-if="!tableData.length" class="no-data">
+ <td colspan="5">
+ No Applications found
+ </td>
+ </tr>
+
+ <tr ng-repeat="app in tableData track by $index"
+ ng-click="selectCallback($event, app)"
+ ng-class="{selected: app.id === selId}"
+ ng-repeat-complete row-id="{{app.id}}">
+ <td class="table-icon">
+ <div icon icon-id="{{app._iconid_state}}"></div>
+ </td>
+ <td>{{app.id}}</td>
+ <td>{{app.version}}</td>
+ <td>{{app.origin}}</td>
+ <td>{{app.desc}}</td>
+ </tr>
+ </table>
+ </div>
+
+ </div>
+
+</div>
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/app/app.js b/framework/src/onos/web/gui/src/main/webapp/app/view/app/app.js
new file mode 100644
index 00000000..7cc081cb
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/app/app.js
@@ -0,0 +1,148 @@
+/*
+ * 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 -- App View Module
+ */
+
+(function () {
+ 'use strict';
+
+ // constants
+ var INSTALLED = 'INSTALLED',
+ ACTIVE = 'ACTIVE',
+ APP_MGMENT_REQ = 'appManagementRequest',
+ FILE_UPLOAD_URL = 'applications/upload';
+
+ angular.module('ovApp', [])
+ .controller('OvAppCtrl',
+ ['$log', '$scope', '$http',
+ 'FnService', 'TableBuilderService', 'WebSocketService', 'UrlFnService',
+ 'KeyService',
+
+ function ($log, $scope, $http, fs, tbs, wss, ufs, ks) {
+ $scope.ctrlBtnState = {};
+ $scope.uploadTip = 'Upload an application (.oar file)';
+ $scope.activateTip = 'Activate selected application';
+ $scope.deactivateTip = 'Deactivate selected application';
+ $scope.uninstallTip = 'Uninstall selected application';
+
+ function selCb($event, row) {
+ // selId comes from tableBuilder
+ $scope.ctrlBtnState.selection = !!$scope.selId;
+ refreshCtrls();
+ }
+
+ function refreshCtrls() {
+ var row, rowIdx;
+ if ($scope.ctrlBtnState.selection) {
+ rowIdx = fs.find($scope.selId, $scope.tableData);
+ row = rowIdx >= 0 ? $scope.tableData[rowIdx] : null;
+
+ $scope.ctrlBtnState.installed = row && row.state === INSTALLED;
+ $scope.ctrlBtnState.active = row && row.state === ACTIVE;
+ } else {
+ $scope.ctrlBtnState.installed = false;
+ $scope.ctrlBtnState.active = false;
+ }
+ }
+
+ tbs.buildTable({
+ scope: $scope,
+ tag: 'app',
+ selCb: selCb,
+ respCb: refreshCtrls
+ });
+
+ // TODO: reexamine where keybindings should be - directive or controller?
+ ks.keyBindings({
+ esc: [$scope.selectCallback, 'Deselect app'],
+ _helpFormat: ['esc']
+ });
+ ks.gestureNotes([
+ ['click row', 'Select / deselect app'],
+ ['scroll down', 'See more apps']
+ ]);
+
+ $scope.appAction = function (action) {
+ if ($scope.ctrlBtnState.selection) {
+ $log.debug('Initiating ' + action + ' of ' + $scope.selId);
+ wss.sendEvent(APP_MGMENT_REQ, {
+ action: action,
+ name: $scope.selId,
+ sortCol: $scope.sortParams.sortCol,
+ sortDir: $scope.sortParams.sortDir
+ });
+ }
+ };
+
+ $scope.$on('FileChanged', function () {
+ var formData = new FormData();
+ if ($scope.appFile) {
+ formData.append('file', $scope.appFile);
+ $http.post(ufs.rsUrl(FILE_UPLOAD_URL), formData, {
+ transformRequest: angular.identity,
+ headers: {
+ 'Content-Type': undefined
+ }
+ })
+ .finally(function () {
+ $scope.sortCallback($scope.sortParams);
+ document.getElementById('inputFileForm').reset();
+ });
+ }
+ });
+
+ $scope.$on('$destroy', function () {
+ ks.unbindKeys();
+ });
+
+ $log.log('OvAppCtrl has been created');
+ }])
+
+ // triggers the input form to appear when button is clicked
+ .directive('triggerForm', function () {
+ return {
+ restrict: 'A',
+ link: function (scope, elem) {
+ elem.bind('click', function () {
+ document.getElementById('uploadFile')
+ .dispatchEvent(new MouseEvent('click'));
+ });
+ }
+ };
+ })
+
+ // binds the model file to the scope in scope.appFile
+ // sends upload request to the server
+ .directive('fileModel', ['$parse',
+ function ($parse) {
+ return {
+ restrict: 'A',
+ link: function (scope, elem, attrs) {
+ var model = $parse(attrs.fileModel),
+ modelSetter = model.assign;
+
+ elem.bind('change', function () {
+ scope.$apply(function () {
+ modelSetter(scope, elem[0].files[0]);
+ });
+ scope.$emit('FileChanged');
+ });
+ }
+ };
+ }]);
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/cluster/cluster.css b/framework/src/onos/web/gui/src/main/webapp/app/view/cluster/cluster.css
new file mode 100644
index 00000000..83ba8d87
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/cluster/cluster.css
@@ -0,0 +1,27 @@
+/*
+ * 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 -- Cluster View -- CSS file
+ */
+
+#ov-cluster h2 {
+ display: inline-block;
+}
+
+#ov-cluster div.ctrl-btns {
+ width: 45px;
+} \ No newline at end of file
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/cluster/cluster.html b/framework/src/onos/web/gui/src/main/webapp/app/view/cluster/cluster.html
new file mode 100644
index 00000000..c5048884
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/cluster/cluster.html
@@ -0,0 +1,68 @@
+<!--
+ ~ 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.
+ -->
+
+<!-- Cluster partial HTML -->
+<div id="ov-cluster">
+ <div class="tabular-header">
+ <h2>Cluster Nodes ({{tableData.length}} total)</h2>
+ <div class="ctrl-btns">
+ <div class="refresh" ng-class="{active: autoRefresh}"
+ icon icon-size="36" icon-id="refresh"
+ tooltip tt-msg="autoRefreshTip"
+ ng-click="toggleRefresh()"></div>
+ </div>
+ </div>
+
+ <div class="summary-list" onos-table-resize>
+ <div ng-show="loading" class="loading-wheel"
+ icon icon-id="loading" icon-size="75"></div>
+
+ <div class="table-header" onos-sortable-header>
+ <table>
+ <tr>
+ <td colId="_iconid_state" class="table-icon" sortable></td>
+ <td colId="id" sortable>ID </td>
+ <td colId="ip" sortable>IP Address </td>
+ <td colId="tcp" sortable>TCP Port </td>
+ <td colId="updated" sortable>Last Updated </td>
+ </tr>
+ </table>
+ </div>
+
+ <div class="table-body">
+ <table onos-flash-changes id-prop="id">
+ <tr ng-if="!tableData.length" class="no-data">
+ <td colspan="5">
+ No Cluster Nodes found
+ </td>
+ </tr>
+
+ <tr ng-repeat="node in tableData track by $index"
+ ng-repeat-complete row-id="{{node.id}}">
+ <td class="table-icon">
+ <div icon icon-id="{{node._iconid_state}}"></div>
+ </td>
+ <td>{{node.id}}</td>
+ <td>{{node.ip}}</td>
+ <td>{{node.tcp}}</td>
+ <td>{{node.updated}}</td>
+ </tr>
+ </table>
+ </div>
+
+ </div>
+
+</div>
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/cluster/cluster.js b/framework/src/onos/web/gui/src/main/webapp/app/view/cluster/cluster.js
new file mode 100644
index 00000000..006808c0
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/cluster/cluster.js
@@ -0,0 +1,36 @@
+/*
+ * 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 -- Cluster View Module
+ */
+
+(function () {
+ 'use strict';
+
+ angular.module('ovCluster', [])
+ .controller('OvClusterCtrl',
+ ['$log', '$scope', 'TableBuilderService',
+
+ function ($log, $scope, tbs) {
+ tbs.buildTable({
+ scope: $scope,
+ tag: 'cluster'
+ });
+
+ $log.log('OvClusterCtrl has been created');
+ }]);
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/device/device.css b/framework/src/onos/web/gui/src/main/webapp/app/view/device/device.css
new file mode 100644
index 00000000..fc08f68b
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/device/device.css
@@ -0,0 +1,144 @@
+/*
+ * 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 -- Device View -- CSS file
+ */
+
+#ov-device h2 {
+ display: inline-block;
+}
+
+#ov-device div.ctrl-btns {
+ width: 240px;
+}
+
+/* More in generic panel.css */
+
+#device-details-panel.floatpanel {
+ -moz-border-radius: 0;
+ border-radius: 0;
+ z-index: 0;
+}
+
+.light #device-details-panel.floatpanel {
+ background-color: rgb(229, 234, 237);
+}
+.dark #device-details-panel.floatpanel {
+ background-color: #3A4042;
+}
+
+#device-details-panel .container {
+ padding: 0 12px;
+}
+
+#device-details-panel .close-btn {
+ position: absolute;
+ right: 10px;
+ top: 0;
+ cursor: pointer;
+}
+.light .close-btn svg.embeddedIcon .icon.plus .glyph {
+ fill: #aaa;
+}
+.dark .close-btn svg.embeddedIcon .icon.plus .glyph {
+ fill: #ccc;
+}
+
+#device-details-panel .dev-icon {
+ display: inline-block;
+ padding: 0 6px 0 0;
+ vertical-align: middle;
+}
+.light .dev-icon svg.embeddedIcon .glyph {
+ fill: rgb(0, 172, 229);
+}
+.dark .dev-icon svg.embeddedIcon .glyph {
+ fill: #486D91;
+}
+
+#device-details-panel h2 {
+ display: inline-block;
+ margin: 8px 0;
+}
+
+#device-details-panel .top div.left {
+ float: left;
+ padding: 0 18px 0 0;
+}
+#device-details-panel .top div.right {
+ display: inline-block;
+}
+
+#device-details-panel td.label {
+ font-style: italic;
+ padding-right: 12px;
+ /* works for both light and dark themes ... */
+ color: #777;
+}
+
+#device-details-panel .actionBtns div {
+ padding: 12px 6px;
+}
+
+#device-details-panel .top hr {
+ width: 95%;
+ margin: 0 auto;
+}
+
+.light #device-details-panel hr {
+ opacity: .5;
+ border-color: #FFF;
+}
+.dark #device-details-panel hr {
+ border-color: #666;
+}
+
+#device-details-panel .bottom table {
+ border-spacing: 0;
+}
+
+#device-details-panel .bottom th {
+ letter-spacing: 0.02em;
+}
+
+.light #device-details-panel .bottom th {
+ background-color: #CCC;
+ /* default text color */
+}
+.dark #device-details-panel .bottom th {
+ background-color: #131313;
+ color: #ccc;
+}
+
+#device-details-panel .bottom th,
+#device-details-panel .bottom td {
+ padding: 6px 12px;
+ text-align: center;
+}
+
+.light #device-details-panel .bottom tr:nth-child(odd) {
+ background-color: #f9f9f9;
+}
+.light #device-details-panel .bottom tr:nth-child(even) {
+ background-color: #EBEDF2;
+}
+.dark #device-details-panel .bottom tr:nth-child(odd) {
+ background-color: #333;
+}
+.dark #device-details-panel .bottom tr:nth-child(even) {
+ background-color: #555;
+}
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/device/device.html b/framework/src/onos/web/gui/src/main/webapp/app/view/device/device.html
new file mode 100644
index 00000000..5d51d1d4
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/device/device.html
@@ -0,0 +1,82 @@
+<!-- Device partial HTML -->
+<div id="ov-device">
+ <div class="tabular-header">
+ <h2>Devices ({{tableData.length}} total)</h2>
+ <div class="ctrl-btns">
+ <div class="refresh" ng-class="{active: autoRefresh}"
+ icon icon-id="refresh" icon-size="36"
+ tooltip tt-msg="autoRefreshTip"
+ ng-click="toggleRefresh()"></div>
+ <div class="separator"></div>
+
+ <div ng-class="{active: !!selId}"
+ icon icon-id="flowTable" icon-size="36"
+ tooltip tt-msg="flowTip"
+ ng-click="nav('flow')"></div>
+
+ <div ng-class="{active: !!selId}"
+ icon icon-id="portTable" icon-size="36"
+ tooltip tt-msg="portTip"
+ ng-click="nav('port')"></div>
+
+ <div ng-class="{active: !!selId}"
+ icon icon-id="groupTable" icon-size="36"
+ tooltip tt-msg="groupTip"
+ ng-click="nav('group')"></div>
+ </div>
+ </div>
+
+ <div class="summary-list" onos-table-resize>
+ <div ng-show="loading" class="loading-wheel"
+ icon icon-id="loading" icon-size="75"></div>
+
+ <div class="table-header" onos-sortable-header>
+ <table>
+ <tr>
+ <td colId="available" class="table-icon" sortable></td>
+ <td colId="type" class="table-icon" sortable></td>
+ <td colId="id" sortable>Device ID </td>
+ <td colId="masterid" sortable>Master Instance </td>
+ <td colId="num_ports" sortable>Ports </td>
+ <td colId="mfr" sortable>Vendor </td>
+ <td colId="hw" sortable>H/W Version </td>
+ <td colId="sw" sortable>S/W Version </td>
+ <td colId="protocol" sortable>Protocol </td>
+ </tr>
+ </table>
+ </div>
+
+ <div class="table-body">
+ <table onos-flash-changes id-prop="id">
+ <tr ng-if="!tableData.length" class="no-data">
+ <td colspan="9">
+ No Devices found
+ </td>
+ </tr>
+
+ <tr ng-repeat="dev in tableData track by $index"
+ ng-click="selectCallback($event, dev)"
+ ng-class="{selected: dev.id === selId}"
+ ng-repeat-complete row-id="{{dev.id}}">
+ <td class="table-icon">
+ <div icon icon-id="{{dev._iconid_available}}"></div>
+ </td>
+ <td class="table-icon">
+ <div icon icon-id="{{dev._iconid_type}}"></div>
+ </td>
+ <td>{{dev.id}}</td>
+ <td>{{dev.masterid}}</td>
+ <td>{{dev.num_ports}}</td>
+ <td>{{dev.mfr}}</td>
+ <td>{{dev.hw}}</td>
+ <td>{{dev.sw}}</td>
+ <td>{{dev.protocol}}</td>
+ </tr>
+ </table>
+ </div>
+
+ </div>
+
+ <device-details-panel></device-details-panel>
+
+</div>
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/device/device.js b/framework/src/onos/web/gui/src/main/webapp/app/view/device/device.js
new file mode 100644
index 00000000..7a2dc4f9
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/device/device.js
@@ -0,0 +1,319 @@
+/*
+ * 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 -- Device View Module
+ */
+
+(function () {
+ 'use strict';
+
+ // injected refs
+ var $log, $scope, $location, fs, mast, ps, wss, is, ns;
+
+ // internal state
+ var detailsPanel,
+ pStartY, pHeight,
+ top, bottom, iconDiv,
+ wSize;
+
+ // constants
+ var topPdg = 13,
+ ctnrPdg = 24,
+ scrollSize = 17,
+ portsTblPdg = 50,
+
+ pName = 'device-details-panel',
+ detailsReq = 'deviceDetailsRequest',
+ detailsResp = 'deviceDetailsResponse',
+
+ propOrder = [
+ 'type', 'masterid', 'chassisid',
+ 'mfr', 'hw', 'sw', 'protocol', 'serial'
+ ],
+ friendlyProps = [
+ 'Type', 'Master ID', 'Chassis ID',
+ 'Vendor', 'H/W Version', 'S/W Version', 'Protocol', 'Serial #'
+ ],
+ portCols = [
+ 'enabled', 'id', 'speed', 'type', 'elinks_dest', 'name'
+ ],
+ friendlyPortCols = [
+ 'Enabled', 'ID', 'Speed', 'Type', 'Egress Links', 'Name'
+ ];
+
+ function closePanel() {
+ if (detailsPanel.isVisible()) {
+ $scope.selId = null;
+ detailsPanel.hide();
+ }
+ }
+
+ function addCloseBtn(div) {
+ is.loadEmbeddedIcon(div, 'plus', 30);
+ div.select('g').attr('transform', 'translate(25, 0) rotate(45)');
+ div.on('click', closePanel);
+ }
+
+ function setUpPanel() {
+ var container, closeBtn, tblDiv;
+ detailsPanel.empty();
+
+ container = detailsPanel.append('div').classed('container', true);
+
+ top = container.append('div').classed('top', true);
+ closeBtn = top.append('div').classed('close-btn', true);
+ addCloseBtn(closeBtn);
+ iconDiv = top.append('div').classed('dev-icon', true);
+ top.append('h2');
+
+ tblDiv = top.append('div').classed('top-tables', true);
+ tblDiv.append('div').classed('left', true).append('table');
+ tblDiv.append('div').classed('right', true).append('table');
+
+ top.append('hr');
+
+ bottom = container.append('div').classed('bottom', true);
+ bottom.append('h2').classed('ports-title', true).html('Ports');
+ bottom.append('table');
+ }
+
+ 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 populateTop(tblDiv, details) {
+ var leftTbl = tblDiv.select('.left')
+ .select('table')
+ .append('tbody'),
+ rightTbl = tblDiv.select('.right')
+ .select('table')
+ .append('tbody');
+
+ is.loadEmbeddedIcon(iconDiv, details._iconid_type, 40);
+ top.select('h2').html(details.id);
+
+ propOrder.forEach(function (prop, i) {
+ // properties are split into two tables
+ addProp(i < 3 ? leftTbl : rightTbl, i, details[prop]);
+ });
+ }
+
+ function addPortRow(tbody, port) {
+ var tr = tbody.append('tr');
+
+ portCols.forEach(function (col) {
+ tr.append('td').html(port[col]);
+ });
+ }
+
+ function populateBottom(table, ports) {
+ var theader = table.append('thead').append('tr'),
+ tbody = table.append('tbody'),
+ tbWidth, tbHeight;
+
+ friendlyPortCols.forEach(function (col) {
+ theader.append('th').html(col);
+ });
+ ports.forEach(function (port) {
+ addPortRow(tbody, port);
+ });
+
+ tbWidth = fs.noPxStyle(tbody, 'width') + scrollSize;
+ tbHeight = pHeight
+ - (fs.noPxStyle(detailsPanel.el()
+ .select('.top'), 'height')
+ + fs.noPxStyle(detailsPanel.el()
+ .select('.ports-title'), 'height')
+ + portsTblPdg);
+
+ table.style({
+ height: tbHeight + 'px',
+ width: tbWidth + 'px',
+ overflow: 'auto',
+ display: 'block'
+ });
+
+ detailsPanel.width(tbWidth + ctnrPdg);
+ }
+
+ function populateDetails(details) {
+ var topTbs, btmTbl, ports;
+ setUpPanel();
+
+ topTbs = top.select('.top-tables');
+ btmTbl = bottom.select('table');
+ ports = details.ports;
+
+ populateTop(topTbs, details);
+ populateBottom(btmTbl, ports);
+
+ detailsPanel.height(pHeight);
+ }
+
+ function respDetailsCb(data) {
+ $scope.panelData = data.details;
+ $scope.$apply();
+ }
+
+ 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();
+ }
+
+ angular.module('ovDevice', [])
+ .controller('OvDeviceCtrl',
+ ['$log', '$scope', '$location', 'TableBuilderService', 'FnService',
+ 'MastService', 'PanelService', 'WebSocketService', 'IconService',
+ 'NavService',
+
+ function (_$log_, _$scope_, _$location_,
+ tbs, _fs_, _mast_, _ps_, _wss_, _is_, _ns_) {
+ $log = _$log_;
+ $scope = _$scope_;
+ $location = _$location_;
+ fs = _fs_;
+ mast = _mast_;
+ ps = _ps_;
+ wss = _wss_;
+ is = _is_;
+ ns = _ns_;
+ var params = $location.search(),
+ handlers = {};
+ $scope.panelData = {};
+ $scope.flowTip = 'Show flow view for selected device';
+ $scope.portTip = 'Show port view for selected device';
+ $scope.groupTip = 'Show group view for selected device';
+
+ // details panel handlers
+ handlers[detailsResp] = respDetailsCb;
+ wss.bindHandlers(handlers);
+
+ // query for if a certain device needs to be highlighted
+ if (params.hasOwnProperty('devId')) {
+ $scope.selId = params['devId'];
+ wss.sendEvent(detailsReq, { id: $scope.selId });
+ }
+
+ function selCb($event, row) {
+ if ($scope.selId) {
+ wss.sendEvent(detailsReq, { id: row.id });
+ } else {
+ $scope.hidePanel();
+ }
+ $log.debug('Got a click on:', row);
+ }
+
+ tbs.buildTable({
+ scope: $scope,
+ tag: 'device',
+ selCb: selCb
+ });
+
+ $scope.nav = function (path) {
+ if ($scope.selId) {
+ ns.navTo(path, { devId: $scope.selId });
+ }
+ };
+
+ $scope.$on('$destroy', function () {
+ wss.unbindHandlers(handlers);
+ });
+
+ $log.log('OvDeviceCtrl has been created');
+ }])
+
+ .directive('deviceDetailsPanel',
+ ['$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: [closePanel, 'Close the details panel'],
+ _helpFormat: ['esc']
+ });
+ ks.gestureNotes([
+ ['click', 'Select a row to show device details'],
+ ['scroll down', 'See more devices']
+ ]);
+
+ // 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();
+ ks.unbindKeys();
+ ps.destroyPanel(pName);
+ });
+ };
+ }]);
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/flow/flow.css b/framework/src/onos/web/gui/src/main/webapp/app/view/flow/flow.css
new file mode 100644
index 00000000..4aa96210
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/flow/flow.css
@@ -0,0 +1,86 @@
+/*
+ * 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 -- Flow View -- CSS file
+ */
+
+#ov-flow h2 {
+ display: inline-block;
+}
+
+#ov-flow div.ctrl-btns {
+ width: 240px;
+}
+
+.light #ov-flow .current-view use {
+ fill: white;
+}
+.dark #ov-flow .current-view use {
+ fill: #304860;
+}
+
+.light #ov-flow .current-view rect {
+ fill: deepskyblue;
+}
+.dark #ov-flow .current-view rect {
+ fill: #eee;
+}
+
+.light #ov-flow tr:nth-child(6n + 1),
+.light #ov-flow tr:nth-child(6n + 2),
+.light #ov-flow tr:nth-child(6n + 3) {
+ background-color: #eee;
+}
+.light #ov-flow tr:nth-child(6n + 4),
+.light #ov-flow tr:nth-child(6n + 5),
+.light #ov-flow tr:nth-child(6n) {
+ background-color: #ddd;
+}
+.dark #ov-flow tr:nth-child(6n + 1),
+.dark #ov-flow tr:nth-child(6n + 2),
+.dark #ov-flow tr:nth-child(6n + 3) {
+ background-color: #444;
+}
+.dark #ov-flow tr:nth-child(6n + 4),
+.dark #ov-flow tr:nth-child(6n + 5),
+.dark #ov-flow tr:nth-child(6n) {
+ background-color: #333;
+}
+
+/* highlighted color */
+.light #ov-flow tr:nth-child(6n + 1).data-change,
+.light #ov-flow tr:nth-child(6n + 2).data-change,
+.light #ov-flow tr:nth-child(6n + 3).data-change,
+.light #ov-flow tr:nth-child(6n + 4).data-change,
+.light #ov-flow tr:nth-child(6n + 5).data-change,
+.light #ov-flow tr:nth-child(6n).data-change {
+ background-color: #FDFFDC;
+}
+.dark #ov-flow tr:nth-child(6n + 1).data-change,
+.dark #ov-flow tr:nth-child(6n + 2).data-change,
+.dark #ov-flow tr:nth-child(6n + 3).data-change,
+.dark #ov-flow tr:nth-child(6n + 4).data-change,
+.dark #ov-flow tr:nth-child(6n + 5).data-change,
+.dark #ov-flow tr:nth-child(6n).data-change {
+ background-color: #5A5600;
+}
+
+#ov-flow td.selector,
+#ov-flow td.treatment {
+ padding-left: 36px;
+ opacity: 0.65;
+} \ No newline at end of file
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/flow/flow.html b/framework/src/onos/web/gui/src/main/webapp/app/view/flow/flow.html
new file mode 100644
index 00000000..5fce98cf
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/flow/flow.html
@@ -0,0 +1,84 @@
+<!-- Flow partial HTML -->
+<div id="ov-flow">
+ <div class="tabular-header">
+ <h2>
+ Flows for Device {{devId || "(No device selected)"}}
+ ({{tableData.length}} total)
+ </h2>
+ <div class="ctrl-btns">
+ <div class="refresh" ng-class="{active: autoRefresh}"
+ icon icon-size="36" icon-id="refresh"
+ tooltip tt-msg="autoRefreshTip"
+ ng-click="toggleRefresh()"></div>
+
+ <div class="separator"></div>
+
+ <div class="current-view"
+ icon icon-id="flowTable" icon-size="36"></div>
+
+ <div class="active"
+ icon icon-id="portTable" icon-size="36"
+ tooltip tt-msg="portTip"
+ ng-click="nav('port')"></div>
+
+ <div class="active"
+ icon icon-id="groupTable" icon-size="36"
+ tooltip tt-msg="groupTip"
+ ng-click="nav('group')"></div>
+ </div>
+ </div>
+
+ <div class="summary-list" onos-table-resize>
+ <div ng-show="loading" class="loading-wheel"
+ icon icon-id="loading" icon-size="75"></div>
+
+ <div class="table-header" onos-sortable-header>
+ <table>
+ <tr>
+ <td colId="id" col-width="180px" sortable>Flow ID </td>
+ <td colId="appId" sortable>App ID </td>
+ <td colId="groupId" sortable>Group ID </td>
+ <td colId="tableId" sortable>Table ID </td>
+ <td colId="priority" sortable>Priority </td>
+ <td colId="timeout" sortable>Timeout </td>
+ <td colId="permanent" sortable>Permanent </td>
+ <td colId="state" sortable>State </td>
+ <td colId="packets" sortable>Packets </td>
+ <td colId="bytes" sortable>Bytes </td>
+ </tr>
+ </table>
+ </div>
+
+ <div class="table-body">
+ <table onos-flash-changes id-prop="id">
+ <tr ng-if="!tableData.length" class="no-data">
+ <td colspan="10">
+ No Flows found
+ </td>
+ </tr>
+
+ <tr ng-repeat-start="flow in tableData track by $index"
+ ng-repeat-complete row-id="{{flow.id}}">
+ <td>{{flow.id}}</td>
+ <td>{{flow.appId}}</td>
+ <td>{{flow.groupId}}</td>
+ <td>{{flow.tableId}}</td>
+ <td>{{flow.priority}}</td>
+ <td>{{flow.timeout}}</td>
+ <td>{{flow.permanent}}</td>
+ <td>{{flow.state}}</td>
+ <td>{{flow.packets}}</td>
+ <td>{{flow.bytes}}</td>
+ </tr>
+ <tr row-id="{{flow.id}}">
+ <td class="selector" colspan="10">{{flow.selector}}</td>
+ </tr>
+ <tr row-id="{{flow.id}}" ng-repeat-end>
+ <td class="treatment" colspan="10">{{flow.treatment}}</td>
+ </tr>
+ </table>
+ </div>
+
+ </div>
+
+</div>
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/flow/flow.js b/framework/src/onos/web/gui/src/main/webapp/app/view/flow/flow.js
new file mode 100644
index 00000000..15678d5e
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/flow/flow.js
@@ -0,0 +1,62 @@
+/*
+ * 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 -- Flow View Module
+ */
+
+(function () {
+ 'use strict';
+
+ // injected references
+ var $log, $scope, $location, fs, tbs, ns;
+
+ angular.module('ovFlow', [])
+ .controller('OvFlowCtrl',
+ ['$log', '$scope', '$location',
+ 'FnService', 'TableBuilderService', 'NavService',
+
+ function (_$log_, _$scope_, _$location_, _fs_, _tbs_, _ns_) {
+ var params;
+ $log = _$log_;
+ $scope = _$scope_;
+ $location = _$location_;
+ fs = _fs_;
+ tbs = _tbs_;
+ ns = _ns_;
+ $scope.portTip = 'Show port view for this device';
+ $scope.groupTip = 'Show group view for this device';
+
+ params = $location.search();
+ if (params.hasOwnProperty('devId')) {
+ $scope.devId = params['devId'];
+ }
+
+ tbs.buildTable({
+ scope: $scope,
+ tag: 'flow',
+ query: params
+ });
+
+ $scope.nav = function (path) {
+ if ($scope.devId) {
+ ns.navTo(path, { devId: $scope.devId });
+ }
+ };
+
+ $log.log('OvFlowCtrl has been created');
+ }]);
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/group/group.css b/framework/src/onos/web/gui/src/main/webapp/app/view/group/group.css
new file mode 100644
index 00000000..42f1c31d
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/group/group.css
@@ -0,0 +1,77 @@
+/*
+ * 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 -- Group View -- CSS file
+ */
+
+#ov-group h2 {
+ display: inline-block;
+}
+
+#ov-group div.ctrl-btns {
+ width: 240px;
+}
+
+.light #ov-group .current-view use {
+ fill: white;
+}
+.dark #ov-group .current-view use {
+ fill: #304860;
+}
+
+.light #ov-group .current-view rect {
+ fill: deepskyblue;
+}
+.dark #ov-group .current-view rect {
+ fill: #eee;
+}
+
+.light #ov-group tr:nth-child(4n + 1),
+.light #ov-group tr:nth-child(4n + 2) {
+ background-color: #eee;
+}
+.light #ov-group tr:nth-child(4n + 3),
+.light #ov-group tr:nth-child(4n) {
+ background-color: #ddd;
+}
+.dark #ov-group tr:nth-child(4n + 1),
+.dark #ov-group tr:nth-child(4n + 2) {
+ background-color: #444;
+}
+.dark #ov-group tr:nth-child(4n + 3),
+.dark #ov-group tr:nth-child(4n) {
+ background-color: #333;
+}
+
+/* highlighted color */
+.light #ov-group tr:nth-child(4n + 1).data-change,
+.light #ov-group tr:nth-child(4n + 2).data-change,
+.light #ov-group tr:nth-child(4n + 3).data-change,
+.light #ov-group tr:nth-child(4n).data-change {
+ background-color: #FDFFDC;
+}
+.dark #ov-group tr:nth-child(4n + 1).data-change,
+.dark #ov-group tr:nth-child(4n + 2).data-change,
+.dark #ov-group tr:nth-child(4n + 3).data-change,
+.dark #ov-group tr:nth-child(4n).data-change {
+ background-color: #5A5600;
+}
+
+#ov-group td.buckets {
+ padding-left: 36px;
+ opacity: 0.65;
+}
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/group/group.html b/framework/src/onos/web/gui/src/main/webapp/app/view/group/group.html
new file mode 100644
index 00000000..b963f469
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/group/group.html
@@ -0,0 +1,90 @@
+<!--
+ ~ 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.
+ -->
+
+<!-- Group partial HTML -->
+<div id="ov-group">
+ <div class="tabular-header">
+ <h2>
+ Groups for Device {{devId || "(No device selected)"}}
+ ({{tableData.length}} total)
+ </h2>
+ <div class="ctrl-btns">
+ <div class="refresh" ng-class="{active: autoRefresh}"
+ icon icon-size="36" icon-id="refresh"
+ tooltip tt-msg="autoRefreshTip"
+ ng-click="toggleRefresh()"></div>
+
+ <div class="separator"></div>
+
+ <div class="active"
+ icon icon-id="flowTable" icon-size="36"
+ tooltip tt-msg="flowTip"
+ ng-click="nav('flow')"></div>
+
+ <div class="active"
+ icon icon-id="portTable" icon-size="36"
+ tooltip tt-msg="portTip"
+ ng-click="nav('port')"></div>
+
+ <div class="current-view"
+ icon icon-id="groupTable" icon-size="36"></div>
+ </div>
+ </div>
+
+ <div class="summary-list" onos-table-resize>
+ <div ng-show="loading" class="loading-wheel"
+ icon icon-id="loading" icon-size="75"></div>
+
+ <div class="table-header" onos-sortable-header>
+ <table>
+ <tr>
+ <td colId="id" sortable>Group ID </td>
+ <td colId="app_id" sortable>App ID </td>
+ <td colId="state" sortable>State </td>
+ <td colId="type" sortable>Type </td>
+ <td colId="packets" sortable>Packets </td>
+ <td colId="bytes" sortable>Bytes </td>
+ </tr>
+ </table>
+ </div>
+
+ <div class="table-body">
+ <table onos-flash-changes id-prop="id">
+ <tr ng-if="!tableData.length" class="no-data">
+ <td colspan="6">
+ No Groups found
+ </td>
+ </tr>
+
+ <tr ng-repeat-start="group in tableData track by $index"
+ ng-repeat-complete row-id="{{group.id}}">
+ <td>{{group.id}}</td>
+ <td>{{group.app_id}}</td>
+ <td>{{group.state}}</td>
+ <td>{{group.type}}</td>
+ <td>{{group.packets}}</td>
+ <td>{{group.bytes}}</td>
+ </tr>
+ <tr row-id="{{group.id}}" ng-repeat-end>
+ <td class="buckets" colspan="6"
+ ng-bind-html="group.buckets"></td>
+ </tr>
+ </table>
+ </div>
+
+ </div>
+
+</div>
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/group/group.js b/framework/src/onos/web/gui/src/main/webapp/app/view/group/group.js
new file mode 100644
index 00000000..2510190b
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/group/group.js
@@ -0,0 +1,70 @@
+/*
+ * 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 -- Group View Module
+ */
+
+(function () {
+ 'use strict';
+
+ // injected references
+ var $log, $scope, $location, fs, tbs, ns;
+
+ angular.module('ovGroup', [])
+ .controller('OvGroupCtrl',
+ ['$log', '$scope', '$location', '$sce',
+ 'FnService', 'TableBuilderService', 'NavService',
+
+ function (_$log_, _$scope_, _$location_, $sce, _fs_, _tbs_, _ns_) {
+ var params;
+ $log = _$log_;
+ $scope = _$scope_;
+ $location = _$location_;
+ fs = _fs_;
+ tbs = _tbs_;
+ ns = _ns_;
+ $scope.flowTip = 'Show flow view for this device';
+ $scope.portTip = 'Show port view for this device';
+
+ params = $location.search();
+ if (params.hasOwnProperty('devId')) {
+ $scope.devId = params['devId'];
+ }
+
+ tbs.buildTable({
+ scope: $scope,
+ tag: 'group',
+ query: params
+ });
+
+ $scope.$watch('tableData', function () {
+ if (!fs.isEmptyObject($scope.tableData)) {
+ $scope.tableData.forEach(function (group) {
+ group.buckets = $sce.trustAsHtml(group.buckets);
+ });
+ }
+ });
+
+ $scope.nav = function (path) {
+ if ($scope.devId) {
+ ns.navTo(path, { devId: $scope.devId });
+ }
+ };
+
+ $log.log('OvGroupCtrl has been created');
+ }]);
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/host/host.css b/framework/src/onos/web/gui/src/main/webapp/app/view/host/host.css
new file mode 100644
index 00000000..60672759
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/host/host.css
@@ -0,0 +1,27 @@
+/*
+ * 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 -- Host View -- CSS file
+ */
+
+#ov-host h2 {
+ display: inline-block;
+}
+
+#ov-host div.ctrl-btns {
+ width: 45px;
+} \ No newline at end of file
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/host/host.html b/framework/src/onos/web/gui/src/main/webapp/app/view/host/host.html
new file mode 100644
index 00000000..19a7ee1d
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/host/host.html
@@ -0,0 +1,54 @@
+<!-- Host partial HTML -->
+<div id="ov-host">
+ <div class="tabular-header">
+ <h2>Hosts ({{tableData.length}} total)</h2>
+ <div class="ctrl-btns">
+ <div class="refresh" ng-class="{active: autoRefresh}"
+ icon icon-size="36" icon-id="refresh"
+ tooltip tt-msg="autoRefreshTip"
+ ng-click="toggleRefresh()"></div>
+ </div>
+ </div>
+
+ <div class="summary-list" onos-table-resize>
+ <div ng-show="loading" class="loading-wheel"
+ icon icon-id="loading" icon-size="75"></div>
+
+ <div class="table-header" onos-sortable-header>
+ <table>
+ <tr>
+ <td colId="type" class="table-icon" sortable></td>
+ <td colId="id" sortable>Host ID </td>
+ <td colId="mac" sortable>MAC Address </td>
+ <td colId="vlan" sortable>VLAN ID </td>
+ <td colId="ips" sortable>IP Addresses </td>
+ <td colId="location" sortable>Location </td>
+ </tr>
+ </table>
+ </div>
+
+ <div class="table-body">
+ <table onos-flash-changes id-prop="id">
+ <tr ng-if="!tableData.length" class="no-data">
+ <td colspan="6">
+ No Hosts found
+ </td>
+ </tr>
+
+ <tr ng-repeat="host in tableData track by $index"
+ ng-repeat-complete row-id="{{host.id}}">
+ <td class="table-icon">
+ <div icon icon-id="{{host._iconid_type}}"></div>
+ </td>
+ <td>{{host.id}}</td>
+ <td>{{host.mac}}</td>
+ <td>{{host.vlan}}</td>
+ <td>{{host.ips}}</td>
+ <td>{{host.location}}</td>
+ </tr>
+ </table>
+ </div>
+
+ </div>
+
+</div>
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/host/host.js b/framework/src/onos/web/gui/src/main/webapp/app/view/host/host.js
new file mode 100644
index 00000000..1ccca401
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/host/host.js
@@ -0,0 +1,36 @@
+/*
+ * 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 -- Host View Module
+ */
+
+(function () {
+ 'use strict';
+
+ angular.module('ovHost', [])
+ .controller('OvHostCtrl',
+ ['$log', '$scope', 'TableBuilderService',
+
+ function ($log, $scope, tbs) {
+ tbs.buildTable({
+ scope: $scope,
+ tag: 'host'
+ });
+
+ $log.log('OvHostCtrl has been created');
+ }]);
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/intent/intent.css b/framework/src/onos/web/gui/src/main/webapp/app/view/intent/intent.css
new file mode 100644
index 00000000..ed9cd48d
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/intent/intent.css
@@ -0,0 +1,71 @@
+/*
+ * 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 -- Intent View -- CSS file
+ */
+
+#ov-intent h2 {
+ display: inline-block;
+}
+
+#ov-intent div.ctrl-btns {
+ width: 45px;
+}
+
+.light #ov-intent tr:nth-child(6n + 1),
+.light #ov-intent tr:nth-child(6n + 2),
+.light #ov-intent tr:nth-child(6n + 3) {
+ background-color: #eee;
+}
+.light #ov-intent tr:nth-child(6n + 4),
+.light #ov-intent tr:nth-child(6n + 5),
+.light #ov-intent tr:nth-child(6n) {
+ background-color: #ddd;
+}
+.dark #ov-intent tr:nth-child(6n + 1),
+.dark #ov-intent tr:nth-child(6n + 2),
+.dark #ov-intent tr:nth-child(6n + 3) {
+ background-color: #444;
+}
+.dark #ov-intent tr:nth-child(6n + 4),
+.dark #ov-intent tr:nth-child(6n + 5),
+.dark #ov-intent tr:nth-child(6n) {
+ background-color: #333;
+}
+
+.light #ov-intent tr:nth-child(6n + 1).data-change,
+.light #ov-intent tr:nth-child(6n + 2).data-change,
+.light #ov-intent tr:nth-child(6n + 3).data-change,
+.light #ov-intent tr:nth-child(6n + 4).data-change,
+.light #ov-intent tr:nth-child(6n + 5).data-change,
+.light #ov-intent tr:nth-child(6n).data-change {
+ background-color: #FDFFDC;
+}
+.dark #ov-intent tr:nth-child(6n + 1).data-change,
+.dark #ov-intent tr:nth-child(6n + 2).data-change,
+.dark #ov-intent tr:nth-child(6n + 3).data-change,
+.dark #ov-intent tr:nth-child(6n + 4).data-change,
+.dark #ov-intent tr:nth-child(6n + 5).data-change,
+.dark #ov-intent tr:nth-child(6n).data-change {
+ background-color: #5A5600;
+}
+
+#ov-intent td.resources,
+#ov-intent td.details {
+ padding-left: 36px;
+ opacity: 0.65;
+}
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/intent/intent.html b/framework/src/onos/web/gui/src/main/webapp/app/view/intent/intent.html
new file mode 100644
index 00000000..4883beed
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/intent/intent.html
@@ -0,0 +1,72 @@
+<!--
+ ~ 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.
+ -->
+
+<!-- Intent partial HTML -->
+<div id="ov-intent">
+ <div class="tabular-header">
+ <h2>Intents ({{tableData.length}} total)</h2>
+ <div class="ctrl-btns">
+ <div class="refresh" ng-class="{active: autoRefresh}"
+ icon icon-size="36" icon-id="refresh"
+ tooltip tt-msg="autoRefreshTip"
+ ng-click="toggleRefresh()"></div>
+ </div>
+ </div>
+
+ <div class="summary-list" onos-table-resize>
+ <div ng-show="loading" class="loading-wheel"
+ icon icon-id="loading" icon-size="75"></div>
+
+ <div class="table-header" onos-sortable-header>
+ <table>
+ <tr>
+ <td colId="appId" sortable>Application ID </td>
+ <td colId="key" sortable>Key </td>
+ <td colId="type" sortable>Type </td>
+ <td colId="priority" sortable>Priority </td>
+ <td colId="state" sortable>State </td>
+ </tr>
+ </table>
+ </div>
+
+ <div class="table-body">
+ <table onos-flash-changes id-prop="key">
+ <tr ng-if="!tableData.length" class="no-data">
+ <td colspan="5">
+ No Intents found
+ </td>
+ </tr>
+
+ <tr ng-repeat-start="intent in tableData track by $index"
+ ng-repeat-complete row-id="{{intent.key}}">
+ <td>{{intent.appId}}</td>
+ <td>{{intent.key}}</td>
+ <td>{{intent.type}}</td>
+ <td>{{intent.priority}}</td>
+ <td>{{intent.state}}</td>
+ </tr>
+ <tr row-id="{{intent.key}}">
+ <td class="resources" colspan="5">{{intent.resources}}</td>
+ </tr>
+ <tr row-id="{{intent.key}}" ng-repeat-end>
+ <td class="details" colspan="5">{{intent.details}}</td>
+ </tr>
+ </table>
+ </div>
+
+ </div>
+
+</div>
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/intent/intent.js b/framework/src/onos/web/gui/src/main/webapp/app/view/intent/intent.js
new file mode 100644
index 00000000..5810f347
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/intent/intent.js
@@ -0,0 +1,36 @@
+/*
+ * 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 -- Intent View Module
+ */
+
+(function () {
+ 'use strict';
+
+ angular.module('ovIntent', [])
+ .controller('OvIntentCtrl',
+ ['$log', '$scope', 'TableBuilderService',
+
+ function ($log, $scope, tbs) {
+ tbs.buildTable({
+ scope: $scope,
+ tag: 'intent'
+ });
+
+ $log.log('OvIntentCtrl has been created');
+ }]);
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/link/link.css b/framework/src/onos/web/gui/src/main/webapp/app/view/link/link.css
new file mode 100644
index 00000000..4f049cdb
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/link/link.css
@@ -0,0 +1,27 @@
+/*
+ * 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 -- Link View -- CSS file
+ */
+
+#ov-link h2 {
+ display: inline-block;
+}
+
+#ov-link div.ctrl-btns {
+ width: 45px;
+} \ No newline at end of file
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/link/link.html b/framework/src/onos/web/gui/src/main/webapp/app/view/link/link.html
new file mode 100644
index 00000000..371d6b13
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/link/link.html
@@ -0,0 +1,70 @@
+<!--
+ ~ 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.
+ -->
+
+<!-- Link partial HTML -->
+<div id="ov-link">
+ <div class="tabular-header">
+ <h2>Links ({{tableData.length}} total)</h2>
+ <div class="ctrl-btns">
+ <div class="refresh" ng-class="{active: autoRefresh}"
+ icon icon-size="36" icon-id="refresh"
+ tooltip tt-msg="autoRefreshTip"
+ ng-click="toggleRefresh()"></div>
+ </div>
+ </div>
+
+ <div class="summary-list" onos-table-resize>
+ <div ng-show="loading" class="loading-wheel"
+ icon icon-id="loading" icon-size="75"></div>
+
+ <div class="table-header" onos-sortable-header>
+ <table>
+ <tr>
+ <td colId="_iconid_state" class="table-icon" sortable></td>
+ <td colId="one" sortable>Port 1 </td>
+ <td colId="two" sortable>Port 2 </td>
+ <td colId="type" sortable>Type </td>
+ <td colId="direction" sortable>Direction </td>
+ <td colId="durable" sortable>Durable </td>
+ </tr>
+ </table>
+ </div>
+
+ <div class="table-body">
+ <table onos-flash-changes id-prop="one">
+ <tr ng-if="!tableData.length" class="no-data">
+ <td colspan="6">
+ No Links found
+ </td>
+ </tr>
+
+ <tr ng-repeat="link in tableData track by $index"
+ ng-repeat-complete row-id="{{link.one}}">
+ <td class="table-icon">
+ <div icon icon-id="{{link._iconid_state}}"></div>
+ </td>
+ <td>{{link.one}}</td>
+ <td>{{link.two}}</td>
+ <td>{{link.type}}</td>
+ <td ng-bind-html="link.direction"></td>
+ <td>{{link.durable}}</td>
+ </tr>
+ </table>
+ </div>
+
+ </div>
+
+</div>
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/link/link.js b/framework/src/onos/web/gui/src/main/webapp/app/view/link/link.js
new file mode 100644
index 00000000..fcf2a83e
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/link/link.js
@@ -0,0 +1,44 @@
+/*
+ * 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 -- Host View Module
+ */
+
+(function () {
+ 'use strict';
+
+ angular.module('ovLink', [])
+ .controller('OvLinkCtrl',
+ ['$log', '$scope', '$sce', 'FnService', 'TableBuilderService',
+
+ function ($log, $scope, $sce, fs, tbs) {
+ tbs.buildTable({
+ scope: $scope,
+ tag: 'link'
+ });
+
+ $scope.$watch('tableData', function () {
+ if (!fs.isEmptyObject($scope.tableData)) {
+ $scope.tableData.forEach(function (link) {
+ link.direction = $sce.trustAsHtml(link.direction);
+ });
+ }
+ });
+
+ $log.log('OvLinkCtrl has been created');
+ }]);
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/port/port.css b/framework/src/onos/web/gui/src/main/webapp/app/view/port/port.css
new file mode 100644
index 00000000..7fcfee3f
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/port/port.css
@@ -0,0 +1,49 @@
+/*
+ * 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 -- Port View -- CSS file
+ */
+
+#ov-port h2 {
+ display: inline-block;
+}
+
+#ov-port div.ctrl-btns {
+ width: 240px;
+}
+
+.light #ov-port .current-view use {
+ fill: white;
+}
+.dark #ov-port .current-view use {
+ fill: #304860;
+}
+
+.light #ov-port .current-view rect {
+ fill: deepskyblue;
+}
+.dark #ov-port .current-view rect {
+ fill: #eee;
+}
+
+#ov-port td {
+ text-align: right;
+}
+
+#ov-port tr.no-data td {
+ text-align: center;
+}
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/port/port.html b/framework/src/onos/web/gui/src/main/webapp/app/view/port/port.html
new file mode 100644
index 00000000..8eecb9d6
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/port/port.html
@@ -0,0 +1,90 @@
+<!--
+ ~ 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.
+ -->
+
+<!-- Port partial HTML -->
+<div id="ov-port">
+ <div class="tabular-header">
+ <h2>
+ Port Statistics for Device {{devId || "(No device selected)"}}
+ ({{tableData.length}} Ports total)
+ </h2>
+ <div class="ctrl-btns">
+ <div class="refresh" ng-class="{active: autoRefresh}"
+ icon icon-size="36" icon-id="refresh"
+ tooltip tt-msg="autoRefreshTip"
+ ng-click="toggleRefresh()"></div>
+
+ <div class="separator"></div>
+
+ <div class="active"
+ icon icon-id="flowTable" icon-size="36"
+ tooltip tt-msg="flowTip"
+ ng-click="nav('flow')"></div>
+
+ <div class="current-view"
+ icon icon-id="portTable" icon-size="36"></div>
+
+ <div class="active"
+ icon icon-id="groupTable" icon-size="36"
+ tooltip tt-msg="groupTip"
+ ng-click="nav('group')"></div>
+ </div>
+ </div>
+
+ <div class="summary-list" onos-table-resize>
+ <div ng-show="loading" class="loading-wheel"
+ icon icon-id="loading" icon-size="75"></div>
+
+ <div class="table-header" onos-sortable-header>
+ <table>
+ <tr>
+ <td colId="id" col-width="60px" sortable>Port ID </td>
+ <td colId="pkt_rx" sortable>Pkts Received </td>
+ <td colId="pkt_tx" sortable>Pkts Sent </td>
+ <td colId="bytes_rx" sortable>Bytes Received </td>
+ <td colId="bytes_tx" sortable>Bytes Sent </td>
+ <td colId="pkt_rx_drp" sortable>Pkts Received Dropped </td>
+ <td colId="pkt_tx_drp" sortable>Pkts Sent Dropped </td>
+ <td colId="duration" sortable>Duration (sec) </td>
+ </tr>
+ </table>
+ </div>
+
+ <div class="table-body">
+ <table onos-flash-changes id-prop="id">
+ <tr ng-if="!tableData.length" class="no-data">
+ <td colspan="8">
+ No Ports found
+ </td>
+ </tr>
+
+ <tr ng-repeat="port in tableData track by $index"
+ ng-repeat-complete row-id="{{port.id}}">
+ <td>{{port.id}}</td>
+ <td>{{port.pkt_rx}}</td>
+ <td>{{port.pkt_tx}}</td>
+ <td>{{port.bytes_rx}}</td>
+ <td>{{port.bytes_tx}}</td>
+ <td>{{port.pkt_rx_drp}}</td>
+ <td>{{port.pkt_tx_drp}}</td>
+ <td>{{port.duration}}</td>
+ </tr>
+ </table>
+ </div>
+
+ </div>
+
+</div>
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/port/port.js b/framework/src/onos/web/gui/src/main/webapp/app/view/port/port.js
new file mode 100644
index 00000000..a157c5be
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/port/port.js
@@ -0,0 +1,62 @@
+/*
+ * 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 -- Port View Module
+ */
+
+(function () {
+ 'use strict';
+
+ // injected references
+ var $log, $scope, $location, fs, tbs, ns;
+
+ angular.module('ovPort', [])
+ .controller('OvPortCtrl',
+ ['$log', '$scope', '$location',
+ 'FnService', 'TableBuilderService', 'NavService',
+
+ function (_$log_, _$scope_, _$location_, _fs_, _tbs_, _ns_) {
+ var params;
+ $log = _$log_;
+ $scope = _$scope_;
+ $location = _$location_;
+ fs = _fs_;
+ tbs = _tbs_;
+ ns = _ns_;
+ $scope.flowTip = 'Show flow view for this device';
+ $scope.groupTip = 'Show group view for this device';
+
+ params = $location.search();
+ if (params.hasOwnProperty('devId')) {
+ $scope.devId = params['devId'];
+ }
+
+ tbs.buildTable({
+ scope: $scope,
+ tag: 'port',
+ query: params
+ });
+
+ $scope.nav = function (path) {
+ if ($scope.devId) {
+ ns.navTo(path, { devId: $scope.devId });
+ }
+ };
+
+ $log.log('OvPortCtrl has been created');
+ }]);
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/settings/settings.css b/framework/src/onos/web/gui/src/main/webapp/app/view/settings/settings.css
new file mode 100644
index 00000000..e748c5ac
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/settings/settings.css
@@ -0,0 +1,27 @@
+/*
+ * 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 -- Settings View -- CSS file
+ */
+
+#ov-settings h2 {
+ display: inline-block;
+}
+
+#ov-settings div.ctrl-btns {
+ width: 45px;
+}
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/settings/settings.html b/framework/src/onos/web/gui/src/main/webapp/app/view/settings/settings.html
new file mode 100644
index 00000000..ee069d37
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/settings/settings.html
@@ -0,0 +1,52 @@
+<!-- settings partial HTML -->
+<div id="ov-settings">
+ <div class="tabular-header">
+ <h2>Component Settings ({{tableData.length}} total)</h2>
+ <div class="ctrl-btns">
+ <div class="refresh" ng-class="{active: autoRefresh}"
+ icon icon-size="36" icon-id="refresh"
+ tooltip tt-msg="autoRefreshTip"
+ ng-click="toggleRefresh()"></div>
+ </div>
+ </div>
+
+ <div class="summary-list" onos-table-resize>
+ <div ng-show="loading" class="loading-wheel"
+ icon icon-id="loading" icon-size="75"></div>
+
+ <div class="table-header" onos-sortable-header>
+ <table>
+ <tr>
+ <td colId="component" sortable col-width="200px">Component </td>
+ <td colId="id" sortable>Property </td>
+ <td colId="type" sortable col-width="70px">Type </td>
+ <td colId="value" sortable>Value </td>
+ <td colId="defValue" sortable>Default </td>
+ <td colId="desc" col-width="400px">Description </td>
+ </tr>
+ </table>
+ </div>
+
+ <div class="table-body">
+ <table onos-flash-changes id-prop="id">
+ <tr ng-if="!tableData.length" class="no-data">
+ <td colspan="6">
+ No Settings found
+ </td>
+ </tr>
+
+ <tr ng-repeat="prop in tableData track by $index"
+ ng-repeat-complete row-id="{{prop.id}}">
+ <td>{{prop.component}}</td>
+ <td>{{prop.id}}</td>
+ <td>{{prop.type}}</td>
+ <td>{{prop.value}}</td>
+ <td>{{prop.defValue}}</td>
+ <td>{{prop.desc}}</td>
+ </tr>
+ </table>
+ </div>
+
+ </div>
+
+</div>
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/settings/settings.js b/framework/src/onos/web/gui/src/main/webapp/app/view/settings/settings.js
new file mode 100644
index 00000000..cec0e70e
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/settings/settings.js
@@ -0,0 +1,36 @@
+/*
+ * 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 -- Component Settings View Module
+ */
+
+(function () {
+ 'use strict';
+
+ angular.module('ovSettings', [])
+ .controller('OvSettingsCtrl',
+ ['$log', '$scope', 'TableBuilderService',
+
+ function ($log, $scope, tbs) {
+ tbs.buildTable({
+ scope: $scope,
+ tag: 'setting'
+ });
+
+ $log.log('OvSettingsCtrl has been created');
+ }]);
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/topo/README.txt b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/README.txt
new file mode 100644
index 00000000..61089bf6
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/README.txt
@@ -0,0 +1,3 @@
+# ONOS Topology View
+
+Code and resources for implementing the client-side for the Topology View.
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topo.css b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topo.css
new file mode 100644
index 00000000..f4b089a0
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topo.css
@@ -0,0 +1,729 @@
+/*
+ * Copyright 2014,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 View -- CSS file
+ */
+
+/* --- Base SVG Layer --- */
+
+#ov-topo svg {
+ /* prevents the little cut/copy/paste square that would appear on iPad */
+ -webkit-user-select: none;
+}
+.light #ov-topo svg {
+ background-color: #fff;
+}
+.dark #ov-topo svg {
+ background-color: #2b2b2b;
+}
+
+
+/* --- "No Devices" Layer --- */
+
+#ov-topo svg #topo-noDevsLayer {
+ visibility: hidden;
+}
+
+.light #ov-topo svg .noDevsBird {
+ fill: #ecd;
+}
+.dark #ov-topo svg .noDevsBird {
+ fill: #683434;
+}
+
+#ov-topo svg #topo-noDevsLayer text {
+ font-size: 60pt;
+ font-style: italic;
+}
+.light #ov-topo svg #topo-noDevsLayer text {
+ fill: #dde;
+}
+.dark #ov-topo svg #topo-noDevsLayer text {
+ fill: #3b3b4f;
+}
+
+
+/* --- Topo Map --- */
+
+#ov-topo svg #topo-map {
+ stroke-width: 2px;
+ fill: transparent;
+}
+
+.light #ov-topo svg #topo-map {
+ stroke: #ddd;
+}
+.dark #ov-topo svg #topo-map {
+ stroke: #444;
+}
+
+
+/* --- Topo Summary Panel --- */
+
+#topo-p-summary {
+ /* Base css from panel.css */
+}
+
+/* --- Topo Detail Panel --- */
+
+#topo-p-detail {
+ /* Base css from panel.css */
+ top: 320px;
+}
+html[data-platform='iPad'] #topo-p-detail {
+ top: 336px;
+}
+
+#topo-p-detail .actionBtns .actionBtn {
+ display: inline-block;
+}
+#topo-p-detail .actionBtns .actionBtn svg {
+ width: 30px;
+ height: 30px;
+}
+
+/* --- general topo-panel styling --- */
+
+.topo-p div.header div.icon {
+ vertical-align: middle;
+ display: inline-block;
+}
+.topo-p div.body {
+ overflow-y: scroll;
+}
+
+.topo-p div.body::-webkit-scrollbar {
+ display: none;
+}
+
+.topo-p svg {
+ display: inline-block;
+ width: 42px;
+ height: 42px;
+}
+
+.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 {
+ padding: 0 4px;
+ margin: 0;
+ word-wrap: break-word;
+ display: inline-block;
+ width: 210px;
+ vertical-align: middle;
+}
+.light .topo-p h2 {
+ color: black;
+}
+.dark .topo-p h2 {
+ color: #ddd;
+}
+
+.topo-p h3 {
+ padding: 0 4px;
+ margin: 0;
+ word-wrap: break-word;
+ top: 20px;
+ left: 50px;
+}
+.light .topo-p h3 {
+ color: black;
+}
+.dark .topo-p h3 {
+ color: #ddd;
+}
+
+.topo-p p, table {
+ padding: 4px;
+ margin: 0;
+}
+
+.topo-p td {
+ word-wrap: break-word;
+}
+.topo-p td.label {
+ font-style: italic;
+ padding-right: 12px;
+ /* works for both light and dark themes ... */
+ color: #777;
+}
+.topo-p td.value {
+}
+
+.topo-p hr {
+ height: 1px;
+ border: 0;
+}
+.light .topo-p hr {
+ background-color: #ccc;
+ color: #ccc;
+}
+.dark .topo-p hr {
+ background-color: #888;
+ color: #888;
+}
+
+/* --- Topo Instance Panel --- */
+
+#topo-p-instance {
+ height: 100px;
+}
+
+#topo-p-instance div.onosInst {
+ display: inline-block;
+ width: 170px;
+ height: 85px;
+ cursor: pointer;
+}
+
+#topo-p-instance svg rect {
+ stroke-width: 3.5;
+}
+#topo-p-instance .online svg rect {
+ opacity: 1;
+}
+.light #topo-p-instance svg rect {
+ fill: #ccc;
+ stroke: #aaa;
+}
+.light #topo-p-instance .online svg rect {
+ fill: #9cf;
+ stroke: #555;
+}
+.dark #topo-p-instance svg rect {
+ fill: #666;
+ stroke: #222;
+}
+.dark #topo-p-instance .online svg rect {
+ fill: #9cf;
+ stroke: #999;
+}
+
+
+#topo-p-instance svg .glyph {
+ fill: #888;
+ fill-rule: evenodd;
+}
+#topo-p-instance .online svg .glyph {
+ fill: #000;
+}
+
+#topo-p-instance svg .badgeIcon {
+ fill-rule: evenodd;
+ opacity: 0.4;
+}
+.light #topo-p-instance svg .badgeIcon {
+ fill: #777;
+}
+.dark #topo-p-instance svg .badgeIcon {
+ fill: #555;
+}
+
+#topo-p-instance .online svg .badgeIcon {
+ opacity: 1.0;
+}
+.light #topo-p-instance .online svg .badgeIcon {
+ fill: #fff;
+}
+.dark #topo-p-instance .online svg .badgeIcon {
+ fill: #fff;
+}
+
+#topo-p-instance svg text {
+ text-anchor: middle;
+ opacity: 0.3;
+}
+#topo-p-instance .online svg text {
+ opacity: 1.0;
+}
+.light #topo-p-instance svg text {
+ fill: #444;
+}
+.light #topo-p-instance .online svg text {
+ fill: #eee;
+}
+.dark #topo-p-instance svg text {
+ fill: #aaa;
+}
+.dark #topo-p-instance .online svg text {
+ fill: #ccc;
+}
+
+#topo-p-instance svg text.instTitle {
+ font-size: 11pt;
+ font-weight: bold;
+}
+#topo-p-instance svg text.instLabel {
+ font-size: 9pt;
+ font-style: italic;
+}
+
+#topo-p-instance .onosInst.mastership {
+ opacity: 0.3;
+}
+#topo-p-instance .onosInst.mastership.affinity {
+ opacity: 1.0;
+}
+.light #topo-p-instance .onosInst.mastership.affinity svg rect {
+ filter: url(#blue-glow);
+}
+.dark #topo-p-instance .onosInst.mastership.affinity svg rect {
+ filter: url(#yellow-glow);
+}
+.light.firefox #topo-p-instance .onosInst.mastership.affinity svg rect {
+ filter: url("data:image/svg+xml;utf8, <svg xmlns = \'http://www.w3.org/2000/svg\'><filter x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\" id=\"blue-glow\"><feColorMatrix type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.7 0 0 0 1 0 \"></feColorMatrix><feGaussianBlur stdDeviation=\"3\" result=\"coloredBlur\"></feGaussianBlur><feMerge><feMergeNode in=\"coloredBlur\"></feMergeNode><feMergeNode in=\"SourceGraphic\"></feMergeNode></feMerge></filter></svg>#blue-glow");
+}
+.dark.firefox #topo-p-instance .onosInst.mastership.affinity svg rect {
+ filter: url("data:image/svg+xml;utf8, <svg xmlns = \'http://www.w3.org/2000/svg\'><filter x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\" id=\"yellow-glow\"><feColorMatrix type=\"matrix\" values=\"0 0 0 0 1.0 0 0 0 0 1.0 0 0 0 0 0.3 0 0 0 1 0 \"></feColorMatrix><feGaussianBlur stdDeviation=\"3\" result=\"coloredBlur\"></feGaussianBlur><feMerge><feMergeNode in=\"coloredBlur\"></feMergeNode><feMergeNode in=\"SourceGraphic\"></feMergeNode></feMerge></filter></svg>#yellow-glow");
+}
+
+
+/* --- Toolbar --- */
+
+#toolbar-topo-tbar .tbar-row.right {
+ width: 100%;
+}
+
+#toolbar-topo-tbar .tbar-row-text {
+ height: 21px;
+ text-align: right;
+ padding: 8px 60px 0 0;
+ font-style: italic;
+}
+
+
+/* --- Topo Nodes --- */
+
+#ov-topo svg .suppressed {
+ opacity: 0.5 !important;
+}
+
+#ov-topo svg .suppressedmax {
+ opacity: 0.2 !important;
+}
+
+#ov-topo svg .node {
+ cursor: pointer;
+}
+
+.light #ov-topo svg .node.selected rect,
+.light #ov-topo svg .node.selected circle {
+ fill: #f90;
+ filter: url(#blue-glow);
+}
+.dark #ov-topo svg .node.selected rect,
+.dark #ov-topo svg .node.selected circle {
+ fill: #f90;
+ filter: url(#yellow-glow);
+}
+.light.firefox #ov-topo svg .node.selected rect,
+.light.firefox #ov-topo svg .node.selected circle {
+ filter: url("data:image/svg+xml;utf8, <svg xmlns = \'http://www.w3.org/2000/svg\'><filter x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\" id=\"blue-glow\"><feColorMatrix type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.7 0 0 0 1 0 \"></feColorMatrix><feGaussianBlur stdDeviation=\"3\" result=\"coloredBlur\"></feGaussianBlur><feMerge><feMergeNode in=\"coloredBlur\"></feMergeNode><feMergeNode in=\"SourceGraphic\"></feMergeNode></feMerge></filter></svg>#blue-glow");
+}
+.dark.firefox #ov-topo svg .node.selected rect,
+.dark.firefox #ov-topo svg .node.selected circle {
+ filter: url("data:image/svg+xml;utf8, <svg xmlns = \'http://www.w3.org/2000/svg\'><filter x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\" id=\"yellow-glow\"><feColorMatrix type=\"matrix\" values=\"0 0 0 0 1.0 0 0 0 0 1.0 0 0 0 0 0.3 0 0 0 1 0 \"></feColorMatrix><feGaussianBlur stdDeviation=\"3\" result=\"coloredBlur\"></feGaussianBlur><feMerge><feMergeNode in=\"coloredBlur\"></feMergeNode><feMergeNode in=\"SourceGraphic\"></feMergeNode></feMerge></filter></svg>#yellow-glow");
+}
+
+#ov-topo svg .node text {
+ pointer-events: none;
+}
+
+/* Device Nodes */
+
+#ov-topo svg .node.device {
+}
+
+#ov-topo svg .node.device rect {
+ stroke-width: 1.5;
+}
+
+#ov-topo svg .node.device.fixed rect {
+ stroke-width: 1.5;
+}
+.light #ov-topo svg .node.device.fixed rect {
+ stroke: #aaa;
+}
+.dark #ov-topo svg .node.device.fixed rect {
+ stroke: #999;
+}
+
+/* note: device is offline without the 'online' class */
+.light #ov-topo svg .node.device {
+ fill: #777;
+}
+.dark #ov-topo svg .node.device {
+ fill: #555;
+}
+
+.light #ov-topo svg .node.device rect {
+ stroke: #666;
+}
+.light #ov-topo svg .node.device rect {
+ stroke: #999;
+}
+
+.light #ov-topo svg .node.device.online {
+ fill: #6e7fa3;
+}
+.dark #ov-topo svg .node.device.online {
+ fill: #4E5C7F;
+}
+
+/* note: device is offline without the 'online' class */
+#ov-topo svg .node.device text {
+ fill: #bbb;
+ font: 10pt sans-serif;
+}
+
+#ov-topo svg .node.device.online text {
+ fill: white;
+}
+
+#ov-topo svg .node.device .svgIcon rect {
+ fill: #aaa;
+}
+#ov-topo svg .node.device .svgIcon use {
+ fill: #777;
+}
+#ov-topo svg .node.device.selected .svgIcon rect {
+ fill: #f90;
+}
+#ov-topo svg .node.device.online .svgIcon rect {
+ fill: #ccc;
+}
+#ov-topo svg .node.device.online .svgIcon use {
+ fill: #000;
+}
+#ov-topo svg .node.device.online.selected .svgIcon rect {
+ fill: #f90;
+}
+
+
+/* Host Nodes */
+
+#ov-topo svg .node.host {
+}
+
+#ov-topo svg .node.host text {
+ stroke: none;
+ font: 9pt sans-serif;
+}
+.light #ov-topo svg .node.host text {
+ fill: #846;
+}
+.dark #ov-topo svg .node.host text {
+ fill: #BB809D;
+}
+
+.light svg .node.host circle {
+ stroke: #000;
+ fill: #edb;
+}
+.dark svg .node.host circle {
+ stroke: #eee;
+ fill: #B2A180;
+}
+
+.light svg .node.host .svgIcon {
+ fill: #444;
+}
+.dark svg .node.host .svgIcon {
+ fill: #222;
+}
+
+/* --- Topo Links --- */
+
+#ov-topo svg .link {
+ opacity: .9;
+}
+
+#ov-topo svg .link.selected,
+#ov-topo svg .link.enhanced {
+ stroke-width: 4.5px;
+}
+.light #ov-topo svg .link.selected,
+.light #ov-topo svg .link.enhanced {
+ filter: url(#blue-glow);
+}
+.dark #ov-topo svg .link.selected,
+.dark #ov-topo svg .link.enhanced {
+ filter: url(#yellow-glow);
+}
+.light.firefox #ov-topo svg .link.selected,
+.light.firefox #ov-topo svg .link.enhanced {
+ filter: url("data:image/svg+xml;utf8, <svg xmlns = \'http://www.w3.org/2000/svg\'><filter x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\" id=\"blue-glow\"><feColorMatrix type=\"matrix\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.7 0 0 0 1 0 \"></feColorMatrix><feGaussianBlur stdDeviation=\"3\" result=\"coloredBlur\"></feGaussianBlur><feMerge><feMergeNode in=\"coloredBlur\"></feMergeNode><feMergeNode in=\"SourceGraphic\"></feMergeNode></feMerge></filter></svg>#blue-glow");
+}
+.dark.firefox #ov-topo svg .link.selected,
+.dark.firefox #ov-topo svg .link.enhanced {
+ filter: url("data:image/svg+xml;utf8, <svg xmlns = \'http://www.w3.org/2000/svg\'><filter x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\" id=\"yellow-glow\"><feColorMatrix type=\"matrix\" values=\"0 0 0 0 1.0 0 0 0 0 1.0 0 0 0 0 0.3 0 0 0 1 0 \"></feColorMatrix><feGaussianBlur stdDeviation=\"3\" result=\"coloredBlur\"></feGaussianBlur><feMerge><feMergeNode in=\"coloredBlur\"></feMergeNode><feMergeNode in=\"SourceGraphic\"></feMergeNode></feMerge></filter></svg>#yellow-glow");
+
+}
+
+#ov-topo svg .link.inactive {
+ opacity: .5;
+ stroke-dasharray: 8 4;
+}
+
+#ov-topo svg .link.secondary {
+ stroke-width: 3px;
+}
+.light #ov-topo svg .link.secondary {
+ stroke: rgba(0,153,51,0.5);
+}
+.dark #ov-topo svg .link.secondary {
+ stroke: rgba(121,231,158,0.5);
+}
+
+/* Port traffic color visualization for Kbps, Mbps, and Gbps */
+
+.light #ov-topo svg .link.secondary.port-traffic-Kbps {
+ stroke: rgb(0,153,51);
+ stroke-width: 5.0;
+}
+.dark #ov-topo svg .link.secondary.port-traffic-Kbps {
+ stroke: rgb(98, 153, 118);
+ stroke-width: 5.0;
+}
+
+.light #ov-topo svg .link.secondary.port-traffic-Mbps {
+ stroke: rgb(128,145,27);
+ stroke-width: 6.5;
+}
+.dark #ov-topo svg .link.secondary.port-traffic-Mbps {
+ stroke: rgb(91, 109, 54);
+ stroke-width: 6.5;
+}
+
+.light #ov-topo svg .link.secondary.port-traffic-Gbps {
+ stroke: rgb(255, 137, 3);
+ stroke-width: 8.0;
+}
+.dark #ov-topo svg .link.secondary.port-traffic-Gbps {
+ stroke: rgb(174, 119, 55);
+ stroke-width: 8.0;
+}
+
+.light #ov-topo svg .link.secondary.port-traffic-Gbps-choked {
+ stroke: rgb(183, 30, 21);
+ stroke-width: 8.0;
+}
+.dark #ov-topo svg .link.secondary.port-traffic-Gbps-choked {
+ stroke: rgb(127, 40, 39);
+ stroke-width: 8.0;
+}
+
+
+#ov-topo svg .link.animated {
+ stroke-dasharray: 8 5;
+ animation: ants 5s infinite linear;
+ /* below line will be added via Javascript based on path */
+ /*animation-direction: reverse;*/
+}
+@keyframes ants {
+ from {
+ stroke-dashoffset: 0;
+ }
+ to {
+ stroke-dashoffset: 400;
+ }
+}
+
+#ov-topo svg .link.primary {
+ stroke-width: 4px;
+}
+.light #ov-topo svg .link.primary {
+ stroke: #ffA300;
+}
+.dark #ov-topo svg .link.primary {
+ stroke: #D58E0F;
+}
+
+#ov-topo svg .link.secondary.optical {
+ stroke-width: 4px;
+}
+.light #ov-topo svg .link.secondary.optical {
+ stroke: rgba(128,64,255,0.5);
+}
+.dark #ov-topo svg .link.secondary.optical {
+ stroke: rgba(164,139,215,0.5);
+}
+
+#ov-topo svg .link.primary.optical {
+ stroke-width: 6px;
+}
+.light #ov-topo svg .link.primary.optical {
+ stroke: #74f;
+}
+.dark #ov-topo svg .link.primary.optical {
+ stroke: #7352CD;
+}
+
+/* Link Labels */
+
+#ov-topo svg .linkLabel rect {
+ stroke: none;
+}
+.light #ov-topo svg .linkLabel rect {
+ fill: #eee;
+}
+.dark #ov-topo svg .linkLabel rect {
+ fill: #555;
+}
+
+#ov-topo svg .linkLabel text {
+ text-anchor: middle;
+ stroke-width: 0.1;
+ font-size: 9pt;
+}
+.light #ov-topo svg .linkLabel text {
+ fill: #444;
+}
+.dark #ov-topo svg .linkLabel text {
+ fill: #eee;
+}
+
+/* Port Labels */
+
+#ov-topo svg .portLabel rect {
+ stroke: none;
+}
+.light #ov-topo svg .portLabel rect {
+ fill: #eee;
+}
+.dark #ov-topo svg .portLabel rect {
+ fill: #222;
+}
+
+#ov-topo svg .portLabel text {
+ text-anchor: middle;
+ stroke-width: 0.1;
+ font-size: 11pt;
+}
+.light #ov-topo svg .portLabel text {
+ fill: #444;
+}
+.dark #ov-topo svg .portLabel text {
+ fill: #eee;
+}
+
+/* Number of Links Labels */
+#ov-topo line.numLinkHash {
+ stroke-width: 3;
+}
+
+#ov-topo text.numLinkText {
+ font-size: 15pt;
+}
+
+#ov-topo text.numLinkText {
+ text-anchor: middle;
+}
+
+.light #ov-topo text.numLinkText {
+ fill: #444;
+}
+.dark #ov-topo text.numLinkText {
+ fill: #eee;
+}
+
+/* ------------------------------------------------- */
+/* Sprite Layer */
+
+#ov-topo svg #topo-sprites use {
+ stroke-width: 2;
+}
+#ov-topo svg #topo-sprites text {
+ text-anchor: middle;
+ font-size: 20pt;
+ font-style: italic;
+}
+
+.light #ov-topo svg #topo-sprites .gold1 use {
+ stroke: #fda;
+ fill: none;
+}
+.dark #ov-topo svg #topo-sprites .gold1 use {
+ stroke: #541;
+ fill: none;
+}
+.light #ov-topo svg #topo-sprites .gold1 text {
+ fill: #eda;
+}
+.dark #ov-topo svg #topo-sprites .gold1 text {
+ fill: #543;
+}
+
+.light #ov-topo svg #topo-sprites .blue1 use {
+ stroke: #bbd;
+ fill: none;
+}
+.dark #ov-topo svg #topo-sprites .blue1 use {
+ stroke: #445;
+ fill: none;
+}
+.light #ov-topo svg #topo-sprites .blue1 text {
+ fill: #cce;
+}
+.dark #ov-topo svg #topo-sprites .blue1 text {
+ fill: #446;
+}
+
+.light #ov-topo svg #topo-sprites .gray1 use {
+ stroke: #ccc;
+ fill: none;
+}
+.dark #ov-topo svg #topo-sprites .gray1 use {
+ stroke: #333;
+ fill: none;
+}
+.light #ov-topo svg #topo-sprites .gray1 text {
+ fill: #ddd;
+}
+.dark #ov-topo svg #topo-sprites .gray1 text {
+ fill: #444;
+}
+
+/* fills */
+.light #ov-topo svg #topo-sprites use.fill-gray2 {
+ fill: #eee;
+}
+.dark #ov-topo svg #topo-sprites use.fill-gray2 {
+ fill: #444;
+}
+
+.light #ov-topo svg #topo-sprites use.fill-blue2 {
+ fill: #bce;
+}
+.dark #ov-topo svg #topo-sprites use.fill-blue2 {
+ fill: #447;
+}
+
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topo.html b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topo.html
new file mode 100644
index 00000000..8a64abb5
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topo.html
@@ -0,0 +1,7 @@
+<!-- Topology View partial HTML -->
+<div id="ov-topo">
+ <svg viewBox="0 0 1000 1000"
+ resize offset-height="56" offset-width="12"
+ notifier="notifyResize()">
+ </svg>
+</div>
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topo.js b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topo.js
new file mode 100644
index 00000000..21894100
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topo.js
@@ -0,0 +1,529 @@
+/*
+ * Copyright 2014,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 View Module
+ */
+
+(function () {
+ 'use strict';
+
+ var moduleDependencies = [
+ 'ngCookies',
+ 'onosUtil',
+ 'onosSvg',
+ 'onosRemote'
+ ];
+
+ // references to injected services etc.
+ var $scope, $log, $cookies, fs, ks, zs, gs, ms, sus, flash, wss, ps,
+ tes, tfs, tps, tis, tss, tls, tts, tos, fltr, ttbs, ttip, tov;
+
+ // DOM elements
+ var ovtopo, svg, defs, zoomLayer, mapG, spriteG, forceG, noDevsLayer;
+
+ // Internal state
+ var zoomer, actionMap;
+
+ // --- Short Cut Keys ------------------------------------------------
+
+ function setUpKeys(overlayKeys) {
+ // key bindings need to be made after the services have been injected
+ // thus, deferred to here...
+ actionMap = {
+ I: [toggleInstances, 'Toggle ONOS instances panel'],
+ O: [toggleSummary, 'Toggle ONOS summary panel'],
+ D: [toggleUseDetailsFlag, 'Disable / enable details panel'],
+
+ H: [toggleHosts, 'Toggle host visibility'],
+ M: [toggleOffline, 'Toggle offline visibility'],
+ P: [togglePorts, 'Toggle Port Highlighting'],
+ dash: [tfs.showBadLinks, 'Show bad links'],
+ B: [toggleMap, 'Toggle background map'],
+ S: [toggleSprites, 'Toggle sprite layer'],
+
+ //X: [toggleNodeLock, 'Lock / unlock node positions'],
+ Z: [tos.toggleOblique, 'Toggle oblique view (Experimental)'],
+ N: [fltr.clickAction, 'Cycle node layers'],
+ L: [tfs.cycleDeviceLabels, 'Cycle device labels'],
+ U: [tfs.unpin, 'Unpin node (hover mouse over)'],
+ R: [resetZoom, 'Reset pan / zoom'],
+ dot: [ttbs.toggleToolbar, 'Toggle Toolbar'],
+
+ E: [equalizeMasters, 'Equalize mastership roles'],
+
+ esc: handleEscape,
+
+ _keyListener: ttbs.keyListener,
+
+ _helpFormat: [
+ ['I', 'O', 'D', 'H', 'M', 'P', 'dash', 'B', 'S' ],
+ ['X', 'Z', 'N', 'L', 'U', 'R', '-', 'E', '-', 'dot'],
+ [] // this column reserved for overlay actions
+ ]
+ };
+
+ if (fs.isO(overlayKeys)) {
+ mergeKeys(overlayKeys);
+ }
+
+ ks.keyBindings(actionMap);
+
+ ks.gestureNotes([
+ ['click', 'Select the item and show details'],
+ ['shift-click', 'Toggle selection state'],
+ ['drag', 'Reposition (and pin) device / host'],
+ ['cmd-scroll', 'Zoom in / out'],
+ ['cmd-drag', 'Pan']
+ ]);
+ }
+
+ // when a topology overlay is activated, we need to bind their keystrokes
+ // and include them in the quick-help panel
+ function mergeKeys(extra) {
+ var _hf = actionMap._helpFormat[2];
+ extra._keyOrder.forEach(function (k) {
+ var d = extra[k],
+ cb = d && d.cb,
+ tt = d && d.tt;
+ // NOTE: ignore keys that are already defined
+ if (d && !actionMap[k]) {
+ actionMap[k] = [cb, tt];
+ _hf.push(k);
+ }
+ });
+ }
+
+ // --- Keystroke functions -------------------------------------------
+
+ function toggleInstances(x) {
+ updatePrefsState('insts', tis.toggle(x));
+ tfs.updateDeviceColors();
+ }
+
+ function toggleSummary(x) {
+ updatePrefsState('summary', tps.toggleSummary(x));
+ }
+
+ function toggleUseDetailsFlag(x) {
+ updatePrefsState('detail', tps.toggleUseDetailsFlag(x));
+ }
+
+ function toggleHosts(x) {
+ updatePrefsState('hosts', tfs.toggleHosts(x));
+ }
+
+ function toggleOffline(x) {
+ updatePrefsState('offdev', tfs.toggleOffline(x));
+ }
+
+ function togglePorts(x) {
+ updatePrefsState('porthl', tfs.togglePorts(x));
+ }
+
+ function _togSvgLayer(x, G, tag, what) {
+ var on = (x === 'keyev') ? !sus.visible(G) : !!x,
+ verb = on ? 'Show' : 'Hide';
+ sus.visible(G, on);
+ updatePrefsState(tag, on);
+ flash.flash(verb + ' ' + what);
+ }
+
+ function toggleMap(x) {
+ _togSvgLayer(x, mapG, 'bg', 'background map');
+ }
+
+ function toggleSprites(x) {
+ _togSvgLayer(x, spriteG, 'spr', 'sprite layer');
+ }
+
+ function resetZoom() {
+ zoomer.reset();
+ flash.flash('Pan and zoom reset');
+ }
+
+ function equalizeMasters() {
+ wss.sendEvent('equalizeMasters');
+ flash.flash('Equalizing master roles');
+ }
+
+ function handleEscape() {
+ if (tis.showMaster()) {
+ // if an instance is selected, cancel the affinity mapping
+ tis.cancelAffinity()
+
+ } else if (tov.hooks.escape()) {
+ // else if the overlay consumed the ESC event...
+ // (work already done)
+
+ } else if (tss.deselectAll()) {
+ // else if we have node selections, deselect them all
+ // (work already done)
+
+ } else if (tls.deselectLink()) {
+ // else if we have a link selected, deselect it
+ // (work already done)
+
+ } else if (tis.isVisible()) {
+ // else if the Instance Panel is visible, hide it
+ tis.hide();
+ tfs.updateDeviceColors();
+
+ } else if (tps.summaryVisible()) {
+ // else if the Summary Panel is visible, hide it
+ tps.hideSummaryPanel();
+ }
+ }
+
+ // --- Toolbar Functions ---------------------------------------------
+
+ function notValid(what) {
+ $log.warn('topo.js getActionEntry(): Not a valid ' + what);
+ }
+
+ function getActionEntry(key) {
+ var entry;
+
+ if (!key) {
+ notValid('key');
+ return null;
+ }
+
+ entry = actionMap[key];
+
+ if (!entry) {
+ notValid('actionMap entry');
+ return null;
+ }
+ return fs.isA(entry) || [entry, ''];
+ }
+
+ function setUpToolbar() {
+ ttbs.init({
+ getActionEntry: getActionEntry,
+ setUpKeys: setUpKeys
+ });
+ ttbs.createToolbar();
+ }
+
+ // --- Glyphs, Icons, and the like -----------------------------------
+
+ function setUpDefs() {
+ defs = svg.append('defs');
+ gs.loadDefs(defs);
+ sus.loadGlowDefs(defs);
+ }
+
+
+ // --- Pan and Zoom --------------------------------------------------
+
+ // zoom enabled predicate. ev is a D3 source event.
+ function zoomEnabled(ev) {
+ return fs.isMobile() || (ev.metaKey || ev.altKey);
+ }
+
+ function zoomCallback() {
+ var sc = zoomer.scale(),
+ tr = zoomer.translate();
+
+ ps.setPrefs('topo_zoom', {tx:tr[0], ty:tr[1], sc:sc});
+
+ // keep the map lines constant width while zooming
+ mapG.style('stroke-width', (2.0 / sc) + 'px');
+ }
+
+ function setUpZoom() {
+ zoomLayer = svg.append('g').attr('id', 'topo-zoomlayer');
+ zoomer = zs.createZoomer({
+ svg: svg,
+ zoomLayer: zoomLayer,
+ zoomEnabled: zoomEnabled,
+ zoomCallback: zoomCallback
+ });
+ }
+
+
+ // callback invoked when the SVG view has been resized..
+ function svgResized(s) {
+ tfs.newDim([s.width, s.height]);
+ }
+
+ // --- Background Map ------------------------------------------------
+
+ function setUpNoDevs() {
+ var g, box;
+ noDevsLayer = svg.append('g').attr({
+ id: 'topo-noDevsLayer',
+ transform: sus.translate(500,500)
+ });
+ // Note, SVG viewbox is '0 0 1000 1000', defined in topo.html.
+ // We are translating this layer to have its origin at the center
+
+ g = noDevsLayer.append('g');
+ gs.addGlyph(g, 'bird', 100).attr('class', 'noDevsBird');
+ g.append('text').text('No devices are connected')
+ .attr({ x: 120, y: 80});
+
+ box = g.node().getBBox();
+ box.x -= box.width/2;
+ box.y -= box.height/2;
+ g.attr('transform', sus.translate(box.x, box.y));
+
+ showNoDevs(true);
+ }
+
+ function showNoDevs(b) {
+ sus.visible(noDevsLayer, b);
+ }
+
+
+ var countryFilters = {
+ world: function (c) {
+ return c.properties.continent !== 'Antarctica';
+ },
+
+ // NOTE: for "usa" we are using our hand-crafted topojson file
+
+ s_america: function (c) {
+ return c.properties.continent === 'South America';
+ },
+
+ japan: function (c) {
+ return c.properties.geounit === 'Japan';
+ },
+
+ europe: function (c) {
+ return c.properties.continent === 'Europe';
+ },
+
+ italy: function (c) {
+ return c.properties.geounit === 'Italy';
+ },
+
+ uk: function (c) {
+ // technically, Ireland is not part of the United Kingdom,
+ // but the map looks weird without it showing.
+ return c.properties.adm0_a3 === 'GBR' ||
+ c.properties.adm0_a3 === 'IRL';
+ },
+
+ s_korea: function (c) {
+ return c.properties.adm0_a3 === 'KOR';
+ },
+
+ australia: function (c) {
+ return c.properties.adm0_a3 === 'AUS';
+ }
+ };
+
+
+ function setUpMap($loc) {
+ var s1 = $loc.search().mapid,
+ s2 = ps.getPrefs('topo_mapid'),
+ mapId = s1 || (s2 && s2.id) || 'usa',
+ promise,
+ cfilter,
+ opts;
+
+ mapG = zoomLayer.append('g').attr('id', 'topo-map');
+ if (mapId === 'usa') {
+ promise = ms.loadMapInto(mapG, '*continental_us');
+ } else {
+ ps.setPrefs('topo_mapid', {id:mapId});
+ cfilter = countryFilters[mapId] || countryFilters.world;
+ opts = { countryFilter: cfilter };
+ promise = ms.loadMapRegionInto(mapG, opts);
+ }
+ return promise;
+ }
+
+ function opacifyMap(b) {
+ mapG.transition()
+ .duration(1000)
+ .attr('opacity', b ? 1 : 0);
+ }
+
+ function setUpSprites($loc, tspr) {
+ var s1 = $loc.search().sprites,
+ s2 = ps.getPrefs('topo_sprites'),
+ sprId = s1 || (s2 && s2.id);
+
+ spriteG = zoomLayer.append ('g').attr('id', 'topo-sprites');
+ if (sprId) {
+ ps.setPrefs('topo_sprites', {id:sprId});
+ tspr.loadSprites(spriteG, defs, sprId);
+ }
+ }
+
+ // --- User Preferemces ----------------------------------------------
+
+ var prefsState = {};
+
+ function updatePrefsState(what, b) {
+ prefsState[what] = b ? 1 : 0;
+ ps.setPrefs('topo_prefs', prefsState);
+ }
+
+
+ function restoreConfigFromPrefs() {
+ // NOTE: toolbar will have set this for us..
+ prefsState = ps.asNumbers(ps.getPrefs('topo_prefs'));
+
+ $log.debug('TOPO- Prefs State:', prefsState);
+
+ flash.enable(false);
+ toggleInstances(prefsState.insts);
+ toggleSummary(prefsState.summary);
+ toggleUseDetailsFlag(prefsState.detail);
+ toggleHosts(prefsState.hosts);
+ toggleOffline(prefsState.offdev);
+ togglePorts(prefsState.porthl);
+ toggleMap(prefsState.bg);
+ toggleSprites(prefsState.spr);
+ flash.enable(true);
+ }
+
+
+ // somewhat hackish, because summary update cannot happen until we
+ // have opened the websocket to the server; hence this extra function
+ // invoked after tes.start()
+ function restoreSummaryFromPrefs() {
+ prefsState = ps.asNumbers(ps.getPrefs('topo_prefs'));
+ $log.debug('TOPO- Prefs SUMMARY State:', prefsState.summary);
+
+ flash.enable(false);
+ toggleSummary(prefsState.summary);
+ flash.enable(true);
+ }
+
+
+ // --- Controller Definition -----------------------------------------
+
+ angular.module('ovTopo', moduleDependencies)
+ .controller('OvTopoCtrl', ['$scope', '$log', '$location', '$timeout',
+ '$cookies', 'FnService', 'MastService', 'KeyService', 'ZoomService',
+ 'GlyphService', 'MapService', 'SvgUtilService', 'FlashService',
+ 'WebSocketService', 'PrefsService',
+ 'TopoEventService', 'TopoForceService', 'TopoPanelService',
+ 'TopoInstService', 'TopoSelectService', 'TopoLinkService',
+ 'TopoTrafficService', 'TopoObliqueService', 'TopoFilterService',
+ 'TopoToolbarService', 'TopoSpriteService', 'TooltipService',
+ 'TopoOverlayService',
+
+ function (_$scope_, _$log_, $loc, $timeout, _$cookies_, _fs_, mast, _ks_,
+ _zs_, _gs_, _ms_, _sus_, _flash_, _wss_, _ps_, _tes_, _tfs_,
+ _tps_, _tis_, _tss_, _tls_, _tts_, _tos_, _fltr_, _ttbs_, tspr,
+ _ttip_, _tov_) {
+ var projection,
+ dim,
+ uplink = {
+ // provides function calls back into this space
+ showNoDevs: showNoDevs,
+ projection: function () { return projection; },
+ zoomLayer: function () { return zoomLayer; },
+ zoomer: function () { return zoomer; },
+ opacifyMap: opacifyMap
+ };
+
+ $scope = _$scope_;
+ $log = _$log_;
+ $cookies = _$cookies_;
+ fs = _fs_;
+ ks = _ks_;
+ zs = _zs_;
+ gs = _gs_;
+ ms = _ms_;
+ sus = _sus_;
+ flash = _flash_;
+ wss = _wss_;
+ ps = _ps_;
+ tes = _tes_;
+ tfs = _tfs_;
+ // TODO: consider funnelling actions through TopoForceService...
+ // rather than injecting references to these 'sub-modules',
+ // just so we can invoke functions on them.
+ tps = _tps_;
+ tis = _tis_;
+ tss = _tss_;
+ tls = _tls_;
+ tts = _tts_;
+ tos = _tos_;
+ fltr = _fltr_;
+ ttbs = _ttbs_;
+ ttip = _ttip_;
+ tov = _tov_;
+
+ $scope.notifyResize = function () {
+ svgResized(fs.windowSize(mast.mastHeight()));
+ };
+
+ // Cleanup on destroyed scope..
+ $scope.$on('$destroy', function () {
+ $log.log('OvTopoCtrl is saying Buh-Bye!');
+ tes.stop();
+ ks.unbindKeys();
+ tps.destroyPanels();
+ tis.destroyInst();
+ tfs.destroyForce();
+ ttbs.destroyToolbar();
+ });
+
+ // svg layer and initialization of components
+ ovtopo = d3.select('#ov-topo');
+ svg = ovtopo.select('svg');
+ // set the svg size to match that of the window, less the masthead
+ svg.attr(fs.windowSize(mast.mastHeight()));
+ dim = [svg.attr('width'), svg.attr('height')];
+
+ setUpKeys();
+ setUpToolbar();
+ setUpDefs();
+ setUpZoom();
+ setUpNoDevs();
+ setUpMap($loc).then(
+ function (proj) {
+ var z = ps.getPrefs('topo_zoom') || {tx:0, ty:0, sc:1};
+ zoomer.panZoom([z.tx, z.ty], z.sc);
+ $log.debug('** Zoom restored:', z);
+
+ projection = proj;
+ $log.debug('** We installed the projection:', proj);
+ flash.enable(false);
+ toggleMap(prefsState.bg);
+ flash.enable(true);
+
+ // now we have the map projection, we are ready for
+ // the server to send us device/host data...
+ tes.start();
+ // need to do the following so we immediately get
+ // the summary panel data back from the server
+ restoreSummaryFromPrefs();
+ }
+ );
+ setUpSprites($loc, tspr);
+
+ forceG = zoomLayer.append('g').attr('id', 'topo-force');
+ tfs.initForce(svg, forceG, uplink, dim);
+ tis.initInst({ showMastership: tfs.showMastership });
+ tps.initPanels();
+
+ // temporary solution for persisting user settings
+ restoreConfigFromPrefs();
+
+ $log.debug('registered overlays...', tov.list());
+ $log.log('OvTopoCtrl has been created');
+ }]);
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoD3.js b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoD3.js
new file mode 100644
index 00000000..d29748b1
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoD3.js
@@ -0,0 +1,576 @@
+/*
+ * 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 D3 Module.
+ Functions for manipulating the D3 visualizations of the Topology
+ */
+
+(function () {
+ 'use strict';
+
+ // injected refs
+ var $log, fs, sus, is, ts;
+
+ // api to topoForce
+ var api;
+ /*
+ node() // get ref to D3 selection of nodes
+ link() // get ref to D3 selection of links
+ linkLabel() // get ref to D3 selection of link labels
+ instVisible() // true if instances panel is visible
+ posNode() // position node
+ showHosts() // true if hosts are to be shown
+ restyleLinkElement() // update link styles based on backing data
+ updateLinkLabelModel() // update backing data for link labels
+ */
+
+ // configuration
+ var devCfg = {
+ xoff: -20,
+ yoff: -18
+ },
+ labelConfig = {
+ imgPad: 16,
+ padLR: 4,
+ padTB: 3,
+ marginLR: 3,
+ marginTB: 2,
+ port: {
+ gap: 3,
+ width: 18,
+ height: 14
+ }
+ },
+ icfg;
+
+ // internal state
+ var deviceLabelIndex = 0,
+ hostLabelIndex = 0;
+
+
+ var dCol = {
+ black: '#000',
+ paleblue: '#acf',
+ offwhite: '#ddd',
+ darkgrey: '#444',
+ midgrey: '#888',
+ lightgrey: '#bbb',
+ orange: '#f90'
+ };
+
+ // note: these are the device icon colors without affinity
+ var dColTheme = {
+ light: {
+ rfill: dCol.offwhite,
+ online: {
+ glyph: dCol.darkgrey,
+ rect: dCol.paleblue
+ },
+ offline: {
+ glyph: dCol.midgrey,
+ rect: dCol.lightgrey
+ }
+ },
+ dark: {
+ rfill: dCol.midgrey,
+ online: {
+ glyph: dCol.darkgrey,
+ rect: dCol.paleblue
+ },
+ offline: {
+ glyph: dCol.midgrey,
+ rect: dCol.darkgrey
+ }
+ }
+ };
+
+ function devBaseColor(d) {
+ var o = d.online ? 'online' : 'offline';
+ return dColTheme[ts.theme()][o];
+ }
+
+ function setDeviceColor(d) {
+ var o = d.online,
+ s = d.el.classed('selected'),
+ c = devBaseColor(d),
+ a = instColor(d.master, o),
+ icon = d.el.select('g.deviceIcon'),
+ g, r;
+
+ if (s) {
+ g = c.glyph;
+ r = dCol.orange;
+ } else if (api.instVisible()) {
+ g = o ? a : c.glyph;
+ r = o ? c.rfill : a;
+ } else {
+ g = c.glyph;
+ r = c.rect;
+ }
+
+ icon.select('use').style('fill', g);
+ icon.select('rect').style('fill', r);
+ }
+
+ function instColor(id, online) {
+ return sus.cat7().getColor(id, !online, ts.theme());
+ }
+
+ // ====
+
+ function incDevLabIndex() {
+ deviceLabelIndex = (deviceLabelIndex+1) % 3;
+ switch(deviceLabelIndex) {
+ case 0: return 'Hide device labels';
+ case 1: return 'Show friendly device labels';
+ case 2: return 'Show device ID labels';
+ }
+ }
+
+ // Returns the newly computed bounding box of the rectangle
+ function adjustRectToFitText(n) {
+ var text = n.select('text'),
+ box = text.node().getBBox(),
+ lab = labelConfig;
+
+ text.attr('text-anchor', 'middle')
+ .attr('y', '-0.8em')
+ .attr('x', lab.imgPad/2);
+
+ // translate the bbox so that it is centered on [x,y]
+ box.x = -box.width / 2;
+ box.y = -box.height / 2;
+
+ // add padding
+ box.x -= (lab.padLR + lab.imgPad/2);
+ box.width += lab.padLR * 2 + lab.imgPad;
+ box.y -= lab.padTB;
+ box.height += lab.padTB * 2;
+
+ return box;
+ }
+
+ function hostLabel(d) {
+ var idx = (hostLabelIndex < d.labels.length) ? hostLabelIndex : 0;
+ return d.labels[idx];
+ }
+ function deviceLabel(d) {
+ var idx = (deviceLabelIndex < d.labels.length) ? deviceLabelIndex : 0;
+ return d.labels[idx];
+ }
+ function trimLabel(label) {
+ return (label && label.trim()) || '';
+ }
+
+ function emptyBox() {
+ return {
+ x: -2,
+ y: -2,
+ width: 4,
+ height: 4
+ };
+ }
+
+
+ function updateDeviceLabel(d) {
+ var label = trimLabel(deviceLabel(d)),
+ noLabel = !label,
+ node = d.el,
+ dim = icfg.device.dim,
+ box, dx, dy;
+
+ node.select('text')
+ .text(label)
+ .style('opacity', 0)
+ .transition()
+ .style('opacity', 1);
+
+ if (noLabel) {
+ box = emptyBox();
+ dx = -dim/2;
+ dy = -dim/2;
+ } else {
+ box = adjustRectToFitText(node);
+ dx = box.x + devCfg.xoff;
+ dy = box.y + devCfg.yoff;
+ }
+
+ node.select('rect')
+ .transition()
+ .attr(box);
+
+ node.select('g.deviceIcon')
+ .transition()
+ .attr('transform', sus.translate(dx, dy));
+ }
+
+ function updateHostLabel(d) {
+ var label = trimLabel(hostLabel(d));
+ d.el.select('text').text(label);
+ }
+
+ function updateDeviceColors(d) {
+ if (d) {
+ setDeviceColor(d);
+ } else {
+ api.node().filter('.device').each(function (d) {
+ setDeviceColor(d);
+ });
+ }
+ }
+
+
+ // ==========================
+ // updateNodes - subfunctions
+
+ function deviceExisting(d) {
+ var node = d.el;
+ node.classed('online', d.online);
+ updateDeviceLabel(d);
+ api.posNode(d, true);
+ }
+
+ function hostExisting(d) {
+ updateHostLabel(d);
+ api.posNode(d, true);
+ }
+
+ function deviceEnter(d) {
+ var node = d3.select(this),
+ glyphId = d.type || 'unknown',
+ label = trimLabel(deviceLabel(d)),
+ //devCfg = deviceIconConfig,
+ noLabel = !label,
+ box, dx, dy, icon;
+
+ d.el = node;
+
+ node.append('rect').attr({ rx: 5, ry: 5 });
+ node.append('text').text(label).attr('dy', '1.1em');
+ box = adjustRectToFitText(node);
+ node.select('rect').attr(box);
+
+ icon = is.addDeviceIcon(node, glyphId);
+
+ if (noLabel) {
+ dx = -icon.dim/2;
+ dy = -icon.dim/2;
+ } else {
+ box = adjustRectToFitText(node);
+ dx = box.x + devCfg.xoff;
+ dy = box.y + devCfg.yoff;
+ }
+
+ icon.attr('transform', sus.translate(dx, dy));
+ }
+
+ function hostEnter(d) {
+ var node = d3.select(this),
+ gid = d.type || 'unknown',
+ rad = icfg.host.radius,
+ r = d.type ? rad.withGlyph : rad.noGlyph,
+ textDy = r + 10;
+
+ d.el = node;
+ sus.visible(node, api.showHosts());
+
+ is.addHostIcon(node, r, gid);
+
+ node.append('text')
+ .text(hostLabel)
+ .attr('dy', textDy)
+ .attr('text-anchor', 'middle');
+ }
+
+ function hostExit(d) {
+ var node = d.el;
+ node.select('use')
+ .style('opacity', 0.5)
+ .transition()
+ .duration(800)
+ .style('opacity', 0);
+
+ node.select('text')
+ .style('opacity', 0.5)
+ .transition()
+ .duration(800)
+ .style('opacity', 0);
+
+ node.select('circle')
+ .style('stroke-fill', '#555')
+ .style('fill', '#888')
+ .style('opacity', 0.5)
+ .transition()
+ .duration(1500)
+ .attr('r', 0);
+ }
+
+ function deviceExit(d) {
+ var node = d.el;
+ node.select('use')
+ .style('opacity', 0.5)
+ .transition()
+ .duration(800)
+ .style('opacity', 0);
+
+ node.selectAll('rect')
+ .style('stroke-fill', '#555')
+ .style('fill', '#888')
+ .style('opacity', 0.5);
+ }
+
+
+ // ==========================
+ // updateLinks - subfunctions
+
+ function linkEntering(d) {
+ var link = d3.select(this);
+ d.el = link;
+ api.restyleLinkElement(d);
+ if (d.type() === 'hostLink') {
+ sus.visible(link, api.showHosts());
+ }
+ }
+
+ var linkLabelOffset = '0.3em';
+
+ function applyLinkLabels() {
+ var entering;
+
+ api.updateLinkLabelModel();
+
+ // for elements already existing, we need to update the text
+ // and adjust the rectangle size to fit
+ api.linkLabel().each(function (d) {
+ var el = d3.select(this),
+ rect = el.select('rect'),
+ text = el.select('text');
+ text.text(d.label);
+ rect.attr(rectAroundText(el));
+ });
+
+ entering = api.linkLabel().enter().append('g')
+ .classed('linkLabel', true)
+ .attr('id', function (d) { return d.id; });
+
+ entering.each(function (d) {
+ var el = d3.select(this),
+ rect,
+ text;
+
+ if (d.ldata.type() === 'hostLink') {
+ el.classed('hostLinkLabel', true);
+ sus.visible(el, api.showHosts());
+ }
+
+ d.el = el;
+ rect = el.append('rect');
+ text = el.append('text').text(d.label);
+ rect.attr(rectAroundText(el));
+ text.attr('dy', linkLabelOffset);
+
+ el.attr('transform', transformLabel(d.ldata.position));
+ });
+
+ // Remove any labels that are no longer required.
+ api.linkLabel().exit().remove();
+ }
+
+ function rectAroundText(el) {
+ var text = el.select('text'),
+ box = text.node().getBBox();
+
+ // translate the bbox so that it is centered on [x,y]
+ box.x = -box.width / 2;
+ box.y = -box.height / 2;
+
+ // add padding
+ box.x -= 1;
+ box.width += 2;
+ return box;
+ }
+
+ function transformLabel(p) {
+ var dx = p.x2 - p.x1,
+ dy = p.y2 - p.y1,
+ xMid = dx/2 + p.x1,
+ yMid = dy/2 + p.y1;
+ return sus.translate(xMid, yMid);
+ }
+
+ function applyPortLabels(data, portLabelG) {
+ var entering = portLabelG.selectAll('.portLabel')
+ .data(data).enter().append('g')
+ .classed('portLabel', true)
+ .attr('id', function (d) { return d.id; });
+
+ entering.each(function (d) {
+ var el = d3.select(this),
+ rect = el.append('rect'),
+ text = el.append('text').text(d.num);
+
+ rect.attr(rectAroundText(el));
+ text.attr('dy', linkLabelOffset);
+ el.attr('transform', sus.translate(d.x, d.y));
+ });
+ }
+
+ function labelPoint(linkPos) {
+ var lengthUpLine = 1 / 3,
+ dx = linkPos.x2 - linkPos.x1,
+ dy = linkPos.y2 - linkPos.y1,
+ movedX = dx * lengthUpLine,
+ movedY = dy * lengthUpLine;
+
+ return {
+ x: movedX,
+ y: movedY
+ };
+ }
+
+ function calcGroupPos(linkPos) {
+ var moved = labelPoint(linkPos);
+ return sus.translate(linkPos.x1 + moved.x, linkPos.y1 + moved.y);
+ }
+
+ // calculates where on the link that the hash line for 5+ label appears
+ function hashAttrs(linkPos) {
+ var hashLength = 25,
+ halfLength = hashLength / 2,
+ dx = linkPos.x2 - linkPos.x1,
+ dy = linkPos.y2 - linkPos.y1,
+ length = Math.sqrt((dx * dx) + (dy * dy)),
+ moveAmtX = (dx / length) * halfLength,
+ moveAmtY = (dy / length) * halfLength,
+ mid = labelPoint(linkPos),
+ angle = Math.atan(dy / dx) + 45;
+
+ return {
+ x1: mid.x - moveAmtX,
+ y1: mid.y - moveAmtY,
+ x2: mid.x + moveAmtX,
+ y2: mid.y + moveAmtY,
+ stroke: api.linkConfig()[ts.theme()].baseColor,
+ transform: 'rotate(' + angle + ',' + mid.x + ',' + mid.y + ')'
+ };
+ }
+
+ function textLabelPos(linkPos) {
+ var point = labelPoint(linkPos),
+ dist = 20;
+ return {
+ x: point.x + dist,
+ y: point.y + dist
+ };
+ }
+
+ function applyNumLinkLabels(data, lblsG) {
+ var labels = lblsG.selectAll('g.numLinkLabel')
+ .data(data, function (d) { return 'pair-' + d.id; }),
+ entering;
+
+ // update existing labels
+ labels.each(function (d) {
+ var el = d3.select(this);
+
+ el.attr({
+ transform: function (d) { return calcGroupPos(d.linkCoords); }
+ });
+ el.select('line')
+ .attr(hashAttrs(d.linkCoords));
+ el.select('text')
+ .attr(textLabelPos(d.linkCoords))
+ .text(d.num);
+ });
+
+ // add new labels
+ entering = labels
+ .enter()
+ .append('g')
+ .attr({
+ transform: function (d) { return calcGroupPos(d.linkCoords); },
+ id: function (d) { return 'pair-' + d.id; }
+ })
+ .classed('numLinkLabel', true);
+
+ entering.each(function (d) {
+ var el = d3.select(this);
+
+ el.append('line')
+ .classed('numLinkHash', true)
+ .attr(hashAttrs(d.linkCoords));
+ el.append('text')
+ .classed('numLinkText', true)
+ .attr(textLabelPos(d.linkCoords))
+ .text(d.num);
+ });
+
+ // remove old labels
+ labels.exit().remove();
+ }
+
+ // ==========================
+ // Module definition
+
+ angular.module('ovTopo')
+ .factory('TopoD3Service',
+ ['$log', 'FnService', 'SvgUtilService', 'IconService', 'ThemeService',
+
+ function (_$log_, _fs_, _sus_, _is_, _ts_) {
+ $log = _$log_;
+ fs = _fs_;
+ sus = _sus_;
+ is = _is_;
+ ts = _ts_;
+
+ icfg = is.iconConfig();
+
+ function initD3(_api_) {
+ api = _api_;
+ }
+
+ function destroyD3() { }
+
+ return {
+ initD3: initD3,
+ destroyD3: destroyD3,
+
+ incDevLabIndex: incDevLabIndex,
+ adjustRectToFitText: adjustRectToFitText,
+ hostLabel: hostLabel,
+ deviceLabel: deviceLabel,
+ trimLabel: trimLabel,
+
+ updateDeviceLabel: updateDeviceLabel,
+ updateHostLabel: updateHostLabel,
+ updateDeviceColors: updateDeviceColors,
+
+ deviceExisting: deviceExisting,
+ hostExisting: hostExisting,
+ deviceEnter: deviceEnter,
+ hostEnter: hostEnter,
+ hostExit: hostExit,
+ deviceExit: deviceExit,
+
+ linkEntering: linkEntering,
+ applyLinkLabels: applyLinkLabels,
+ transformLabel: transformLabel,
+ applyPortLabels: applyPortLabels,
+ applyNumLinkLabels: applyNumLinkLabels
+ };
+ }]);
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoEvent.js b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoEvent.js
new file mode 100644
index 00000000..5fd38bf6
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoEvent.js
@@ -0,0 +1,131 @@
+/*
+ * 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 Event Module.
+
+ Defines the conduit between the client and the server:
+ - provides a clean API for sending events to the server
+ - dispatches incoming events from the server to the appropriate sub-module
+
+ */
+
+(function () {
+ 'use strict';
+
+ // injected refs
+ var $log, $interval, wss, tps, tis, tfs, tss, tov, tspr;
+
+ // internal state
+ var handlerMap,
+ openListener,
+ heartbeatTimer;
+
+ var heartbeatPeriod = 9000; // 9 seconds
+
+ // ==========================
+
+ function createHandlerMap() {
+ handlerMap = {
+ showSummary: tps,
+
+ showDetails: tss,
+
+ showHighlights: tov,
+
+ addInstance: tis,
+ updateInstance: tis,
+ removeInstance: tis,
+
+ addDevice: tfs,
+ updateDevice: tfs,
+ removeDevice: tfs,
+ addHost: tfs,
+ updateHost: tfs,
+ removeHost: tfs,
+ addLink: tfs,
+ updateLink: tfs,
+ removeLink: tfs,
+
+ spriteListResponse: tspr,
+ spriteDataResponse: tspr
+ };
+ }
+
+ function wsOpen(host, url) {
+ $log.debug('TOPO: web socket open - cluster node:', host, 'URL:', url);
+ // Request batch of initial data from the new server
+ wss.sendEvent('topoStart');
+ }
+
+ function cancelHeartbeat() {
+ if (heartbeatTimer) {
+ $interval.cancel(heartbeatTimer);
+ }
+ heartbeatTimer = null;
+ }
+
+ function scheduleHeartbeat() {
+ cancelHeartbeat();
+ heartbeatTimer = $interval(function () {
+ wss.sendEvent('topoHeartbeat');
+ }, heartbeatPeriod);
+ }
+
+
+ angular.module('ovTopo')
+ .factory('TopoEventService',
+ ['$log', '$interval', 'WebSocketService',
+ 'TopoPanelService', 'TopoInstService', 'TopoForceService',
+ 'TopoSelectService', 'TopoOverlayService', 'TopoSpriteService',
+
+ function (_$log_, _$interval_, _wss_,
+ _tps_, _tis_, _tfs_, _tss_, _tov_, _tspr_) {
+ $log = _$log_;
+ $interval = _$interval_;
+ wss = _wss_;
+ tps = _tps_;
+ tis = _tis_;
+ tfs = _tfs_;
+ tss = _tss_;
+ tov = _tov_;
+ tspr = _tspr_;
+
+ createHandlerMap();
+
+ function start() {
+ openListener = wss.addOpenListener(wsOpen);
+ wss.bindHandlers(handlerMap);
+ wss.sendEvent('topoStart');
+ scheduleHeartbeat();
+ $log.debug('topo comms started');
+ }
+
+ function stop() {
+ cancelHeartbeat();
+ wss.sendEvent('topoStop');
+ wss.unbindHandlers(handlerMap);
+ wss.removeOpenListener(openListener);
+ openListener = null;
+ $log.debug('topo comms stopped');
+ }
+
+ return {
+ start: start,
+ stop: stop
+ };
+ }]);
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoFilter.js b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoFilter.js
new file mode 100644
index 00000000..f9b96ae8
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoFilter.js
@@ -0,0 +1,149 @@
+/*
+ * 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 layer filtering Module.
+ Provides functionality to visually differentiate between the packet and
+ optical layers of the topology.
+ */
+
+(function () {
+ 'use strict';
+
+ // injected refs
+ var $log, fs, flash, tps, tts;
+
+ // api to topoForce
+ var api;
+ /*
+ node() // get ref to D3 selection of nodes
+ link() // get ref to D3 selection of links
+ */
+
+ var smax = 'suppressedmax';
+
+ // which "layer" a particular item "belongs to"
+ var layerLookup = {
+ host: {
+ endstation: 'pkt', // default, if host event does not define type
+ router: 'pkt',
+ bgpSpeaker: 'pkt'
+ },
+ device: {
+ switch: 'pkt',
+ roadm: 'opt'
+ },
+ link: {
+ hostLink: 'pkt',
+ direct: 'pkt',
+ indirect: '',
+ tunnel: '',
+ optical: 'opt'
+ }
+ },
+ // order of layer cycling in button
+ dispatch = [
+ {
+ type: 'all',
+ action: function () { suppressLayers(false); },
+ msg: 'All Layers Shown'
+ },
+ {
+ type: 'pkt',
+ action: function () { showLayer('pkt'); },
+ msg: 'Packet Layer Shown'
+ },
+ {
+ type: 'opt',
+ action: function () { showLayer('opt'); },
+ msg: 'Optical Layer Shown'
+ }
+ ],
+ layer = 0;
+
+ function clickAction() {
+ layer = (layer + 1) % dispatch.length;
+ dispatch[layer].action();
+ flash.flash(dispatch[layer].msg);
+ }
+
+ function selected() {
+ return dispatch[layer].type;
+ }
+
+ function inLayer(d, layer) {
+ var type = d.class === 'link' ? d.type() : d.type,
+ look = layerLookup[d.class],
+ lyr = look && look[type];
+ return lyr === layer;
+ }
+
+ function unsuppressLayer(which) {
+ api.node().each(function (d) {
+ var node = d.el;
+ if (inLayer(d, which)) {
+ node.classed(smax, false);
+ }
+ });
+
+ api.link().each(function (d) {
+ var link = d.el;
+ if (inLayer(d, which)) {
+ link.classed(smax, false);
+ }
+ });
+ }
+
+ function suppressLayers(b) {
+ api.node().classed(smax, b);
+ api.link().classed(smax, b);
+ }
+
+ function showLayer(which) {
+ suppressLayers(true);
+ unsuppressLayer(which);
+ }
+
+ // === -----------------------------------------------------
+ // === MODULE DEFINITION ===
+
+ angular.module('ovTopo')
+ .factory('TopoFilterService',
+ ['$log', 'FnService',
+ 'FlashService',
+ 'TopoPanelService',
+ 'TopoTrafficService',
+
+ function (_$log_, _fs_, _flash_, _tps_, _tts_) {
+ $log = _$log_;
+ fs = _fs_;
+ flash = _flash_;
+ tps = _tps_;
+ tts = _tts_;
+
+ function initFilter(_api_) {
+ api = _api_;
+ }
+
+ return {
+ initFilter: initFilter,
+
+ clickAction: clickAction,
+ selected: selected,
+ inLayer: inLayer
+ };
+ }]);
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoForce.js b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoForce.js
new file mode 100644
index 00000000..dbe8f9f5
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoForce.js
@@ -0,0 +1,1134 @@
+/*
+ * 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 Force Module.
+ Visualization of the topology in an SVG layer, using a D3 Force Layout.
+ */
+
+(function () {
+ 'use strict';
+
+ // injected refs
+ var $log, $timeout, fs, sus, ts, flash, wss, tov,
+ tis, tms, td3, tss, tts, tos, fltr, tls, uplink, svg;
+
+ // configuration
+ var linkConfig = {
+ light: {
+ baseColor: '#666',
+ inColor: '#66f',
+ outColor: '#f00'
+ },
+ dark: {
+ baseColor: '#aaa',
+ inColor: '#66f',
+ outColor: '#f66'
+ },
+ inWidth: 12,
+ outWidth: 10
+ };
+
+ // internal state
+ var settings, // merged default settings and options
+ force, // force layout object
+ drag, // drag behavior handler
+ network = {
+ nodes: [],
+ links: [],
+ linksByDevice: {},
+ lookup: {},
+ revLinkToKey: {}
+ },
+ lu, // shorthand for lookup
+ rlk, // shorthand for revLinktoKey
+ showHosts = false, // whether hosts are displayed
+ showOffline = true, // whether offline devices are displayed
+ nodeLock = false, // whether nodes can be dragged or not (locked)
+ fTimer, // timer for delayed force layout
+ fNodesTimer, // timer for delayed nodes update
+ fLinksTimer, // timer for delayed links update
+ dim, // the dimensions of the force layout [w,h]
+ linkNums = []; // array of link number labels
+
+ // SVG elements;
+ var linkG, linkLabelG, numLinkLblsG, portLabelG, nodeG;
+
+ // D3 selections;
+ var link, linkLabel, node;
+
+ // default settings for force layout
+ var defaultSettings = {
+ gravity: 0.4,
+ friction: 0.7,
+ charge: {
+ // note: key is node.class
+ device: -8000,
+ host: -5000,
+ _def_: -12000
+ },
+ linkDistance: {
+ // note: key is link.type
+ direct: 100,
+ optical: 120,
+ hostLink: 3,
+ _def_: 50
+ },
+ linkStrength: {
+ // note: key is link.type
+ // range: {0.0 ... 1.0}
+ //direct: 1.0,
+ //optical: 1.0,
+ //hostLink: 1.0,
+ _def_: 1.0
+ }
+ };
+
+
+ // ==========================
+ // === EVENT HANDLERS
+
+ function addDevice(data) {
+ var id = data.id,
+ d;
+
+ uplink.showNoDevs(false);
+
+ // although this is an add device event, if we already have the
+ // device, treat it as an update instead..
+ if (lu[id]) {
+ updateDevice(data);
+ return;
+ }
+
+ d = tms.createDeviceNode(data);
+ network.nodes.push(d);
+ lu[id] = d;
+ updateNodes();
+ fStart();
+ }
+
+ function updateDevice(data) {
+ var id = data.id,
+ d = lu[id],
+ wasOnline;
+
+ if (d) {
+ wasOnline = d.online;
+ angular.extend(d, data);
+ if (tms.positionNode(d, true)) {
+ sendUpdateMeta(d);
+ }
+ updateNodes();
+ if (wasOnline !== d.online) {
+ tms.findAttachedLinks(d.id).forEach(restyleLinkElement);
+ updateOfflineVisibility(d);
+ }
+ }
+ }
+
+ function removeDevice(data) {
+ var id = data.id,
+ d = lu[id];
+ if (d) {
+ removeDeviceElement(d);
+ }
+ }
+
+ function addHost(data) {
+ var id = data.id,
+ d, lnk;
+
+ // although this is an add host event, if we already have the
+ // host, treat it as an update instead..
+ if (lu[id]) {
+ updateHost(data);
+ return;
+ }
+
+ d = tms.createHostNode(data);
+ network.nodes.push(d);
+ lu[id] = d;
+ updateNodes();
+
+ lnk = tms.createHostLink(data);
+ if (lnk) {
+ d.linkData = lnk; // cache ref on its host
+ network.links.push(lnk);
+ lu[d.ingress] = lnk;
+ lu[d.egress] = lnk;
+ updateLinks();
+ }
+ fStart();
+ }
+
+ function updateHost(data) {
+ var id = data.id,
+ d = lu[id];
+ if (d) {
+ angular.extend(d, data);
+ if (tms.positionNode(d, true)) {
+ sendUpdateMeta(d);
+ }
+ updateNodes();
+ }
+ }
+
+ function removeHost(data) {
+ var id = data.id,
+ d = lu[id];
+ if (d) {
+ removeHostElement(d, true);
+ }
+ }
+
+ function addLink(data) {
+ var result = tms.findLink(data, 'add'),
+ bad = result.badLogic,
+ d = result.ldata;
+
+ if (bad) {
+ //logicError(bad + ': ' + link.id);
+ return;
+ }
+
+ if (d) {
+ // we already have a backing store link for src/dst nodes
+ addLinkUpdate(d, data);
+ return;
+ }
+
+ // no backing store link yet
+ d = tms.createLink(data);
+ if (d) {
+ network.links.push(d);
+ aggregateLink(d, data);
+ lu[d.key] = d;
+ updateLinks();
+ fStart();
+ }
+ }
+
+ function updateLink(data) {
+ var result = tms.findLink(data, 'update'),
+ bad = result.badLogic;
+ if (bad) {
+ //logicError(bad + ': ' + link.id);
+ return;
+ }
+ result.updateWith(link);
+ }
+
+ function removeLink(data) {
+ var result = tms.findLink(data, 'remove');
+
+ if (!result.badLogic) {
+ result.removeRawLink();
+ }
+ }
+
+ // ========================
+
+ function nodeById(id) {
+ return lu[id];
+ }
+
+ function makeNodeKey(node1, node2) {
+ return node1 + '-' + node2;
+ }
+
+ function findNodePair(key, keyRev) {
+ if (network.linksByDevice[key]) {
+ return key;
+ } else if (network.linksByDevice[keyRev]) {
+ return keyRev;
+ } else {
+ return false;
+ }
+ }
+
+ function aggregateLink(ldata, link) {
+ var key = makeNodeKey(link.src, link.dst),
+ keyRev = makeNodeKey(link.dst, link.src),
+ found = findNodePair(key, keyRev);
+
+ if (found) {
+ network.linksByDevice[found].push(ldata);
+ ldata.devicePair = found;
+ } else {
+ network.linksByDevice[key] = [ ldata ];
+ ldata.devicePair = key;
+ }
+ }
+
+ function addLinkUpdate(ldata, link) {
+ // add link event, but we already have the reverse link installed
+ ldata.fromTarget = link;
+ rlk[link.id] = ldata.key;
+ // possible solution to el being undefined in restyleLinkElement:
+ //_updateLinks();
+ restyleLinkElement(ldata);
+ }
+
+
+ var widthRatio = 1.4,
+ linkScale = d3.scale.linear()
+ .domain([1, 12])
+ .range([widthRatio, 12 * widthRatio])
+ .clamp(true),
+ allLinkTypes = 'direct indirect optical tunnel';
+
+ function restyleLinkElement(ldata, immediate) {
+ // this fn's job is to look at raw links and decide what svg classes
+ // need to be applied to the line element in the DOM
+ var th = ts.theme(),
+ el = ldata.el,
+ type = ldata.type(),
+ lw = ldata.linkWidth(),
+ online = ldata.online(),
+ delay = immediate ? 0 : 1000;
+
+ // FIXME: understand why el is sometimes undefined on addLink events...
+ // Investigated:
+ // el is undefined when it's a reverse link that is being added.
+ // updateLinks (which sets ldata.el) isn't called before this is called.
+ // Calling _updateLinks in addLinkUpdate fixes it, but there might be
+ // a more efficient way to fix it.
+ if (el && !el.empty()) {
+ el.classed('link', true);
+ el.classed('inactive', !online);
+ el.classed(allLinkTypes, false);
+ if (type) {
+ el.classed(type, true);
+ }
+ el.transition()
+ .duration(delay)
+ .attr('stroke-width', linkScale(lw))
+ .attr('stroke', linkConfig[th].baseColor);
+ }
+ }
+
+ function removeLinkElement(d) {
+ var idx = fs.find(d.key, network.links, 'key'),
+ removed;
+ if (idx >=0) {
+ // remove from links array
+ removed = network.links.splice(idx, 1);
+ // remove from lookup cache
+ delete lu[removed[0].key];
+ updateLinks();
+ fResume();
+ }
+ }
+
+ function removeHostElement(d, upd) {
+ // first, remove associated hostLink...
+ removeLinkElement(d.linkData);
+
+ // remove hostLink bindings
+ delete lu[d.ingress];
+ delete lu[d.egress];
+
+ // remove from lookup cache
+ delete lu[d.id];
+ // remove from nodes array
+ var idx = fs.find(d.id, network.nodes);
+ network.nodes.splice(idx, 1);
+
+ // remove from SVG
+ // NOTE: upd is false if we were called from removeDeviceElement()
+ if (upd) {
+ updateNodes();
+ fResume();
+ }
+ }
+
+ function removeDeviceElement(d) {
+ var id = d.id,
+ idx;
+ // first, remove associated hosts and links..
+ tms.findAttachedHosts(id).forEach(removeHostElement);
+ tms.findAttachedLinks(id).forEach(removeLinkElement);
+
+ // remove from lookup cache
+ delete lu[id];
+ // remove from nodes array
+ idx = fs.find(id, network.nodes);
+ if (idx > -1) {
+ network.nodes.splice(idx, 1);
+ }
+
+ if (!network.nodes.length) {
+ uplink.showNoDevs(true);
+ }
+
+ // remove from SVG
+ updateNodes();
+ fResume();
+ }
+
+ function updateHostVisibility() {
+ sus.visible(nodeG.selectAll('.host'), showHosts);
+ sus.visible(linkG.selectAll('.hostLink'), showHosts);
+ sus.visible(linkLabelG.selectAll('.hostLinkLabel'), showHosts);
+ }
+
+ function updateOfflineVisibility(dev) {
+ function updDev(d, show) {
+ var b;
+ sus.visible(d.el, show);
+
+ tms.findAttachedLinks(d.id).forEach(function (link) {
+ b = show && ((link.type() !== 'hostLink') || showHosts);
+ sus.visible(link.el, b);
+ });
+ tms.findAttachedHosts(d.id).forEach(function (host) {
+ b = show && showHosts;
+ sus.visible(host.el, b);
+ });
+ }
+
+ if (dev) {
+ // updating a specific device that just toggled off/on-line
+ updDev(dev, dev.online || showOffline);
+ } else {
+ // updating all offline devices
+ tms.findDevices(true).forEach(function (d) {
+ updDev(d, showOffline);
+ });
+ }
+ }
+
+
+ function sendUpdateMeta(d, clearPos) {
+ var metaUi = {},
+ ll;
+
+ // if we are not clearing the position data (unpinning),
+ // attach the x, y, longitude, latitude...
+ if (!clearPos) {
+ ll = tms.lngLatFromCoord([d.x, d.y]);
+ metaUi = {x: d.x, y: d.y, lng: ll[0], lat: ll[1]};
+ }
+ d.metaUi = metaUi;
+ wss.sendEvent('updateMeta', {
+ id: d.id,
+ class: d.class,
+ memento: metaUi
+ });
+ }
+
+
+ function mkSvgClass(d) {
+ return d.fixed ? d.svgClass + ' fixed' : d.svgClass;
+ }
+
+ function vis(b) {
+ return b ? 'visible' : 'hidden';
+ }
+
+ function toggleHosts(x) {
+ var kev = (x === 'keyev'),
+ on = kev ? !showHosts : !!x;
+
+ showHosts = on;
+ updateHostVisibility();
+ flash.flash('Hosts ' + vis(on));
+ return on;
+ }
+
+ function toggleOffline(x) {
+ var kev = (x === 'keyev'),
+ on = kev ? !showOffline : !!x;
+
+ showOffline = on;
+ updateOfflineVisibility();
+ flash.flash('Offline devices ' + vis(on));
+ return on;
+ }
+
+ function cycleDeviceLabels() {
+ flash.flash(td3.incDevLabIndex());
+ tms.findDevices().forEach(function (d) {
+ td3.updateDeviceLabel(d);
+ });
+ }
+
+ function unpin() {
+ var hov = tss.hovered();
+ if (hov) {
+ sendUpdateMeta(hov, true);
+ hov.fixed = false;
+ hov.el.classed('fixed', false);
+ fResume();
+ }
+ }
+
+ function showMastership(masterId) {
+ if (!masterId) {
+ restoreLayerState();
+ } else {
+ showMastershipFor(masterId);
+ }
+ }
+
+ function restoreLayerState() {
+ // NOTE: this level of indirection required, for when we have
+ // the layer filter functionality re-implemented
+ suppressLayers(false);
+ }
+
+ function showMastershipFor(id) {
+ suppressLayers(true);
+ node.each(function (n) {
+ if (n.master === id) {
+ n.el.classed('suppressedmax', false);
+ }
+ });
+ }
+
+ function supAmt(less) {
+ return less ? "suppressed" : "suppressedmax";
+ }
+
+ function suppressLayers(b, less) {
+ var cls = supAmt(less);
+ node.classed(cls, b);
+ link.classed(cls, b);
+ }
+
+ function unsuppressNode(id, less) {
+ var cls = supAmt(less);
+ node.each(function (n) {
+ if (n.id === id) {
+ n.el.classed(cls, false);
+ }
+ });
+ }
+
+ function unsuppressLink(key, less) {
+ var cls = supAmt(less);
+ link.each(function (n) {
+ if (n.key === key) {
+ n.el.classed(cls, false);
+ }
+ });
+ }
+
+ function showBadLinks() {
+ var badLinks = tms.findBadLinks();
+ flash.flash('Bad Links: ' + badLinks.length);
+ $log.debug('Bad Link List (' + badLinks.length + '):');
+ badLinks.forEach(function (d) {
+ $log.debug('bad link: (' + d.bad + ') ' + d.key, d);
+ if (d.el) {
+ d.el.attr('stroke-width', linkScale(2.8))
+ .attr('stroke', 'red');
+ }
+ });
+ // back to normal after 2 seconds...
+ $timeout(updateLinks, 2000);
+ }
+
+ // ==========================================
+
+ function updateNodes() {
+ if (fNodesTimer) {
+ $timeout.cancel(fNodesTimer);
+ }
+ fNodesTimer = $timeout(_updateNodes, 150);
+ }
+
+ // IMPLEMENTATION NOTE: _updateNodes() should NOT stop, start, or resume
+ // the force layout; that needs to be determined and implemented elsewhere
+ function _updateNodes() {
+ // select all the nodes in the layout:
+ node = nodeG.selectAll('.node')
+ .data(network.nodes, function (d) { return d.id; });
+
+ // operate on existing nodes:
+ node.filter('.device').each(td3.deviceExisting);
+ node.filter('.host').each(td3.hostExisting);
+
+ // operate on entering nodes:
+ var entering = node.enter()
+ .append('g')
+ .attr({
+ id: function (d) { return sus.safeId(d.id); },
+ class: mkSvgClass,
+ transform: function (d) {
+ // Need to guard against NaN here ??
+ return sus.translate(d.x, d.y);
+ },
+ opacity: 0
+ })
+ .call(drag)
+ .on('mouseover', tss.nodeMouseOver)
+ .on('mouseout', tss.nodeMouseOut)
+ .transition()
+ .attr('opacity', 1);
+
+ // augment entering nodes:
+ entering.filter('.device').each(td3.deviceEnter);
+ entering.filter('.host').each(td3.hostEnter);
+
+ // operate on both existing and new nodes:
+ td3.updateDeviceColors();
+
+ // operate on exiting nodes:
+ // Note that the node is removed after 2 seconds.
+ // Sub element animations should be shorter than 2 seconds.
+ var exiting = node.exit()
+ .transition()
+ .duration(2000)
+ .style('opacity', 0)
+ .remove();
+
+ // exiting node specifics:
+ exiting.filter('.host').each(td3.hostExit);
+ exiting.filter('.device').each(td3.deviceExit);
+ }
+
+ // ==========================
+
+ function getDefaultPos(link) {
+ return {
+ x1: link.source.x,
+ y1: link.source.y,
+ x2: link.target.x,
+ y2: link.target.y
+ };
+ }
+
+ // returns amount of adjustment along the normal for given link
+ function amt(numLinks, linkIdx) {
+ var gap = 6;
+ return (linkIdx - ((numLinks - 1) / 2)) * gap;
+ }
+
+ function calcMovement(d, amt, flipped) {
+ var pos = getDefaultPos(d),
+ mult = flipped ? -amt : amt,
+ dx = pos.x2 - pos.x1,
+ dy = pos.y2 - pos.y1,
+ length = Math.sqrt((dx * dx) + (dy * dy));
+
+ return {
+ x1: pos.x1 + (mult * dy / length),
+ y1: pos.y1 + (mult * -dx / length),
+ x2: pos.x2 + (mult * dy / length),
+ y2: pos.y2 + (mult * -dx / length)
+ };
+ }
+
+ function calcPosition() {
+ var lines = this,
+ linkSrcId;
+ linkNums = [];
+ lines.each(function (d) {
+ if (d.type() === 'hostLink') {
+ d.position = getDefaultPos(d);
+ }
+ });
+
+ function normalizeLinkSrc(link) {
+ // ensure source device is consistent across set of links
+ // temporary measure until link modeling is refactored
+ if (!linkSrcId) {
+ linkSrcId = link.source.id;
+ return false;
+ }
+
+ return link.source.id !== linkSrcId;
+ }
+
+ angular.forEach(network.linksByDevice, function (linkArr, key) {
+ var numLinks = linkArr.length,
+ link;
+
+ if (numLinks === 1) {
+ link = linkArr[0];
+ link.position = getDefaultPos(link);
+ link.position.multiLink = false;
+ } else if (numLinks >= 5) {
+ // this code is inefficient, in the future the way links
+ // are modeled will be changed
+ angular.forEach(linkArr, function (link) {
+ link.position = getDefaultPos(link);
+ link.position.multiLink = true;
+ });
+ linkNums.push({
+ id: key,
+ num: numLinks,
+ linkCoords: linkArr[0].position
+ });
+ } else {
+ linkSrcId = null;
+ angular.forEach(linkArr, function (link, index) {
+ var offsetAmt = amt(numLinks, index),
+ needToFlip = normalizeLinkSrc(link);
+ link.position = calcMovement(link, offsetAmt, needToFlip);
+ link.position.multiLink = false;
+ });
+ }
+ });
+ }
+
+ function updateLinks() {
+ if (fLinksTimer) {
+ $timeout.cancel(fLinksTimer);
+ }
+ fLinksTimer = $timeout(_updateLinks, 150);
+ }
+
+ // IMPLEMENTATION NOTE: _updateLinks() should NOT stop, start, or resume
+ // the force layout; that needs to be determined and implemented elsewhere
+ function _updateLinks() {
+ var th = ts.theme();
+
+ link = linkG.selectAll('.link')
+ .data(network.links, function (d) { return d.key; });
+
+ // operate on existing links:
+ link.each(function (d) {
+ // this is supposed to be an existing link, but we have observed
+ // occasions (where links are deleted and added rapidly?) where
+ // the DOM element has not been defined. So protect against that...
+ if (d.el) {
+ restyleLinkElement(d, true);
+ }
+ });
+
+ // operate on entering links:
+ var entering = link.enter()
+ .append('line')
+ .call(calcPosition)
+ .attr({
+ x1: function (d) { return d.position.x1; },
+ y1: function (d) { return d.position.y1; },
+ x2: function (d) { return d.position.x2; },
+ y2: function (d) { return d.position.y2; },
+ stroke: linkConfig[th].inColor,
+ 'stroke-width': linkConfig.inWidth
+ });
+
+ // augment links
+ entering.each(td3.linkEntering);
+
+ // operate on both existing and new links:
+ //link.each(...)
+
+ // add labels for how many links are in a thick line
+ td3.applyNumLinkLabels(linkNums, numLinkLblsG);
+
+ // apply or remove labels
+ td3.applyLinkLabels();
+
+ // operate on exiting links:
+ link.exit()
+ .attr('stroke-dasharray', '3 3')
+ .attr('stroke', linkConfig[th].outColor)
+ .style('opacity', 0.5)
+ .transition()
+ .duration(1500)
+ .attr({
+ 'stroke-dasharray': '3 12',
+ 'stroke-width': linkConfig.outWidth
+ })
+ .style('opacity', 0.0)
+ .remove();
+ }
+
+
+ // ==========================
+ // force layout tick function
+
+ function fResume() {
+ if (!tos.isOblique()) {
+ force.resume();
+ }
+ }
+
+ function fStart() {
+ if (!tos.isOblique()) {
+ if (fTimer) {
+ $timeout.cancel(fTimer);
+ }
+ fTimer = $timeout(function () {
+ $log.debug("Starting force-layout");
+ force.start();
+ }, 200);
+ }
+ }
+
+ var tickStuff = {
+ nodeAttr: {
+ transform: function (d) {
+ var dx = isNaN(d.x) ? 0 : d.x,
+ dy = isNaN(d.y) ? 0 : d.y;
+ return sus.translate(dx, dy);
+ }
+ },
+ linkAttr: {
+ x1: function (d) { return d.position.x1; },
+ y1: function (d) { return d.position.y1; },
+ x2: function (d) { return d.position.x2; },
+ y2: function (d) { return d.position.y2; }
+ },
+ linkLabelAttr: {
+ transform: function (d) {
+ var lnk = tms.findLinkById(d.key);
+ if (lnk) {
+ return td3.transformLabel(lnk.position);
+ }
+ }
+ }
+ };
+
+ function tick() {
+ // guard against null (which can happen when our view pages out)...
+ if (node && node.size()) {
+ node.attr(tickStuff.nodeAttr);
+ }
+ if (link && link.size()) {
+ link.call(calcPosition)
+ .attr(tickStuff.linkAttr);
+ td3.applyNumLinkLabels(linkNums, numLinkLblsG);
+ }
+ if (linkLabel && linkLabel.size()) {
+ linkLabel.attr(tickStuff.linkLabelAttr);
+ }
+ }
+
+
+ // ==========================
+ // === MOUSE GESTURE HANDLERS
+
+ function zoomingOrPanning(ev) {
+ return ev.metaKey || ev.altKey;
+ }
+
+ function atDragEnd(d) {
+ // once we've finished moving, pin the node in position
+ d.fixed = true;
+ d3.select(this).classed('fixed', true);
+ sendUpdateMeta(d);
+ tss.clickConsumed(true);
+ }
+
+ // predicate that indicates when dragging is active
+ function dragEnabled() {
+ var ev = d3.event.sourceEvent;
+ // nodeLock means we aren't allowing nodes to be dragged...
+ return !nodeLock && !zoomingOrPanning(ev);
+ }
+
+ // predicate that indicates when clicking is active
+ function clickEnabled() {
+ return true;
+ }
+
+ // =============================================
+ // function entry points for overlay module
+
+ // TODO: find an automatic way of tracking via the "showHighlights" events
+ var allTrafficClasses = 'primary secondary optical animated ' +
+ 'port-traffic-Kbps port-traffic-Mbps port-traffic-Gbps ' +
+ 'port-traffic-Gbps-choked';
+
+ function clearLinkTrafficStyle() {
+ link.style('stroke-width', null)
+ .classed(allTrafficClasses, false);
+ }
+
+ function removeLinkLabels() {
+ network.links.forEach(function (d) {
+ d.label = '';
+ });
+ }
+
+ function updateLinkLabelModel() {
+ // create the backing data for showing labels..
+ var data = [];
+ link.each(function (d) {
+ if (d.label) {
+ data.push({
+ id: 'lab-' + d.key,
+ key: d.key,
+ label: d.label,
+ ldata: d
+ });
+ }
+ });
+
+ linkLabel = linkLabelG.selectAll('.linkLabel')
+ .data(data, function (d) { return d.id; });
+ }
+
+ // ==========================
+ // Module definition
+
+ function mkModelApi(uplink) {
+ return {
+ projection: uplink.projection,
+ network: network,
+ restyleLinkElement: restyleLinkElement,
+ removeLinkElement: removeLinkElement
+ };
+ }
+
+ function mkD3Api() {
+ return {
+ node: function () { return node; },
+ link: function () { return link; },
+ linkLabel: function () { return linkLabel; },
+ instVisible: function () { return tis.isVisible(); },
+ posNode: tms.positionNode,
+ showHosts: function () { return showHosts; },
+ restyleLinkElement: restyleLinkElement,
+ updateLinkLabelModel: updateLinkLabelModel,
+ linkConfig: function () { return linkConfig; }
+ };
+ }
+
+ function mkSelectApi() {
+ return {
+ node: function () { return node; },
+ zoomingOrPanning: zoomingOrPanning,
+ updateDeviceColors: td3.updateDeviceColors,
+ deselectLink: tls.deselectLink
+ };
+ }
+
+ function mkTrafficApi() {
+ return {
+ hovered: tss.hovered,
+ somethingSelected: tss.somethingSelected,
+ selectOrder: tss.selectOrder
+ };
+ }
+
+ function mkOverlayApi() {
+ return {
+ clearLinkTrafficStyle: clearLinkTrafficStyle,
+ removeLinkLabels: removeLinkLabels,
+ findLinkById: tms.findLinkById,
+ findNodeById: nodeById,
+ updateLinks: updateLinks,
+ updateNodes: updateNodes,
+ supLayers: suppressLayers,
+ unsupNode: unsuppressNode,
+ unsupLink: unsuppressLink
+ };
+ }
+
+ function mkObliqueApi(uplink, fltr) {
+ return {
+ force: function() { return force; },
+ zoomLayer: uplink.zoomLayer,
+ nodeGBBox: function() { return nodeG.node().getBBox(); },
+ node: function () { return node; },
+ link: function () { return link; },
+ linkLabel: function () { return linkLabel; },
+ nodes: function () { return network.nodes; },
+ tickStuff: tickStuff,
+ nodeLock: function (b) {
+ var old = nodeLock;
+ nodeLock = b;
+ return old;
+ },
+ opacifyMap: uplink.opacifyMap,
+ inLayer: fltr.inLayer,
+ calcLinkPos: calcPosition,
+ applyNumLinkLabels: function () {
+ td3.applyNumLinkLabels(linkNums, numLinkLblsG);
+ }
+ };
+ }
+
+ function mkFilterApi() {
+ return {
+ node: function () { return node; },
+ link: function () { return link; }
+ };
+ }
+
+ function mkLinkApi(svg, uplink) {
+ return {
+ svg: svg,
+ zoomer: uplink.zoomer(),
+ network: network,
+ portLabelG: function () { return portLabelG; },
+ showHosts: function () { return showHosts; }
+ };
+ }
+
+ angular.module('ovTopo')
+ .factory('TopoForceService',
+ ['$log', '$timeout', 'FnService', 'SvgUtilService',
+ 'ThemeService', 'FlashService', 'WebSocketService',
+ 'TopoOverlayService', 'TopoInstService', 'TopoModelService',
+ 'TopoD3Service', 'TopoSelectService', 'TopoTrafficService',
+ 'TopoObliqueService', 'TopoFilterService', 'TopoLinkService',
+
+ function (_$log_, _$timeout_, _fs_, _sus_, _ts_, _flash_, _wss_, _tov_,
+ _tis_, _tms_, _td3_, _tss_, _tts_, _tos_, _fltr_, _tls_) {
+ $log = _$log_;
+ $timeout = _$timeout_;
+ fs = _fs_;
+ sus = _sus_;
+ ts = _ts_;
+ flash = _flash_;
+ wss = _wss_;
+ tov = _tov_;
+ tis = _tis_;
+ tms = _tms_;
+ td3 = _td3_;
+ tss = _tss_;
+ tts = _tts_;
+ tos = _tos_;
+ fltr = _fltr_;
+ tls = _tls_;
+
+ var themeListener = ts.addListener(function () {
+ updateLinks();
+ updateNodes();
+ });
+
+ // forceG is the SVG group to display the force layout in
+ // uplink is the api from the main topo source file
+ // dim is the initial dimensions of the SVG as [w,h]
+ // opts are, well, optional :)
+ function initForce(_svg_, forceG, _uplink_, _dim_, opts) {
+ uplink = _uplink_;
+ dim = _dim_;
+ svg = _svg_;
+
+ lu = network.lookup;
+ rlk = network.revLinkToKey;
+
+ $log.debug('initForce().. dim = ' + dim);
+
+ tov.setApi(mkOverlayApi(), tss);
+ tms.initModel(mkModelApi(uplink), dim);
+ td3.initD3(mkD3Api());
+ tss.initSelect(mkSelectApi());
+ tts.initTraffic(mkTrafficApi());
+ tos.initOblique(mkObliqueApi(uplink, fltr));
+ fltr.initFilter(mkFilterApi());
+ tls.initLink(mkLinkApi(svg, uplink), td3);
+
+ settings = angular.extend({}, defaultSettings, opts);
+
+ linkG = forceG.append('g').attr('id', 'topo-links');
+ linkLabelG = forceG.append('g').attr('id', 'topo-linkLabels');
+ numLinkLblsG = forceG.append('g').attr('id', 'topo-numLinkLabels');
+ nodeG = forceG.append('g').attr('id', 'topo-nodes');
+ portLabelG = forceG.append('g').attr('id', 'topo-portLabels');
+
+ link = linkG.selectAll('.link');
+ linkLabel = linkLabelG.selectAll('.linkLabel');
+ node = nodeG.selectAll('.node');
+
+ force = d3.layout.force()
+ .size(dim)
+ .nodes(network.nodes)
+ .links(network.links)
+ .gravity(settings.gravity)
+ .friction(settings.friction)
+ .charge(settings.charge._def_)
+ .linkDistance(settings.linkDistance._def_)
+ .linkStrength(settings.linkStrength._def_)
+ .on('tick', tick);
+
+ drag = sus.createDragBehavior(force,
+ tss.selectObject, atDragEnd, dragEnabled, clickEnabled);
+ }
+
+ function newDim(_dim_) {
+ dim = _dim_;
+ force.size(dim);
+ tms.newDim(dim);
+ }
+
+ function destroyForce() {
+ force.stop();
+
+ tls.destroyLink();
+ tos.destroyOblique();
+ tts.destroyTraffic();
+ tss.destroySelect();
+ td3.destroyD3();
+ tms.destroyModel();
+ // note: no need to destroy overlay service
+ ts.removeListener(themeListener);
+ themeListener = null;
+
+ // clean up the DOM
+ svg.selectAll('g').remove();
+ svg.selectAll('defs').remove();
+
+ // clean up internal state
+ network.nodes = [];
+ network.links = [];
+ network.linksByDevice = {};
+ network.lookup = {};
+ network.revLinkToKey = {};
+
+ linkNums = [];
+
+ linkG = linkLabelG = numLinkLblsG = nodeG = portLabelG = null;
+ link = linkLabel = node = null;
+ force = drag = null;
+
+ // clean up $timeout promises
+ if (fTimer) {
+ $timeout.cancel(fTimer);
+ }
+ if (fNodesTimer) {
+ $timeout.cancel(fNodesTimer);
+ }
+ if (fLinksTimer) {
+ $timeout.cancel(fLinksTimer);
+ }
+ }
+
+ return {
+ initForce: initForce,
+ newDim: newDim,
+ destroyForce: destroyForce,
+
+ updateDeviceColors: td3.updateDeviceColors,
+ toggleHosts: toggleHosts,
+ togglePorts: tls.togglePorts,
+ toggleOffline: toggleOffline,
+ cycleDeviceLabels: cycleDeviceLabels,
+ unpin: unpin,
+ showMastership: showMastership,
+ showBadLinks: showBadLinks,
+
+ addDevice: addDevice,
+ updateDevice: updateDevice,
+ removeDevice: removeDevice,
+ addHost: addHost,
+ updateHost: updateHost,
+ removeHost: removeHost,
+ addLink: addLink,
+ updateLink: updateLink,
+ removeLink: removeLink
+ };
+ }]);
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoInst.js b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoInst.js
new file mode 100644
index 00000000..7e929977
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoInst.js
@@ -0,0 +1,373 @@
+/*
+ * 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 Instances Module.
+ Defines modeling of ONOS instances.
+ */
+
+(function () {
+ 'use strict';
+
+ // injected refs
+ var $log, ps, sus, gs, ts, fs, flash;
+
+ // api from topo
+ var api;
+ /*
+ showMastership( id )
+ */
+
+ // configuration
+ var instCfg = {
+ rectPad: 8,
+ nodeOx: 9,
+ nodeOy: 9,
+ nodeDim: 40,
+ birdOx: 19,
+ birdOy: 21,
+ birdDim: 21,
+ uiDy: 45,
+ titleDy: 30,
+ textYOff: 20,
+ textYSpc: 15
+ },
+ showLogicErrors = true,
+ idIns = 'topo-p-instance',
+ instOpts = {
+ edge: 'left',
+ width: 20
+ };
+
+ // internal state
+ var onosInstances,
+ onosOrder,
+ oiShowMaster,
+ oiBox,
+ themeListener;
+
+
+ // ==========================
+
+ function addInstance(data) {
+ var id = data.id;
+
+ if (onosInstances[id]) {
+ updateInstance(data);
+ return;
+ }
+ onosInstances[id] = data;
+ onosOrder.push(data);
+ updateInstances();
+ }
+
+ function updateInstance(data) {
+ var id = data.id,
+ d = onosInstances[id];
+ if (d) {
+ angular.extend(d, data);
+ updateInstances();
+ } else {
+ logicError('updateInstance: lookup fail: ID = "' + id + '"');
+ }
+ }
+
+ function removeInstance(data) {
+ var id = data.id,
+ d = onosInstances[id];
+ if (d) {
+ var idx = fs.find(id, onosOrder);
+ if (idx >= 0) {
+ onosOrder.splice(idx, 1);
+ }
+ delete onosInstances[id];
+ updateInstances();
+ } else {
+ logicError('removeInstance lookup fail. ID = "' + id + '"');
+ }
+ }
+
+ // ==========================
+
+ function computeDim(self) {
+ var css = window.getComputedStyle(self);
+ return {
+ w: sus.stripPx(css.width),
+ h: sus.stripPx(css.height)
+ };
+ }
+
+ function clickInst(d) {
+ var el = d3.select(this),
+ aff = el.classed('affinity');
+ if (!aff) {
+ setAffinity(el, d);
+ } else {
+ cancelAffinity();
+ }
+ }
+
+ function setAffinity(el, d) {
+ d3.selectAll('.onosInst')
+ .classed('mastership', true)
+ .classed('affinity', false);
+ el.classed('affinity', true);
+
+ // suppress all elements except nodes whose master is this instance
+ api.showMastership(d.id);
+ oiShowMaster = true;
+ }
+
+ function cancelAffinity() {
+ d3.selectAll('.onosInst')
+ .classed('mastership affinity', false);
+
+ api.showMastership(null);
+ oiShowMaster = false;
+ }
+
+ function instRectAttr(dim) {
+ var pad = instCfg.rectPad;
+ return {
+ x: pad,
+ y: pad,
+ width: dim.w - pad*2,
+ height: dim.h - pad*2,
+ rx: 6
+ };
+ }
+
+ function viewBox(dim) {
+ return '0 0 ' + dim.w + ' ' + dim.h;
+ }
+
+ function attachUiBadge(svg) {
+ gs.addGlyph(svg, 'uiAttached', 30, true, [12, instCfg.uiDy])
+ .classed('badgeIcon uiBadge', true);
+ }
+
+ function instColor(id, online) {
+ return sus.cat7().getColor(id, !online, ts.theme());
+ }
+
+ // ==============================
+
+ function updateInstances() {
+ var onoses = oiBox.el().selectAll('.onosInst')
+ .data(onosOrder, function (d) { return d.id; }),
+ instDim = {w:0,h:0},
+ c = instCfg;
+
+ function nSw(n) {
+ return '# Switches: ' + n;
+ }
+
+ // operate on existing onos instances if necessary
+ onoses.each(function (d) {
+ var el = d3.select(this),
+ svg = el.select('svg');
+ instDim = computeDim(this);
+
+ // update online state
+ el.classed('online', d.online);
+
+ // update ui-attached state
+ svg.select('use.uiBadge').remove();
+ if (d.uiAttached) {
+ attachUiBadge(svg);
+ }
+
+ function updAttr(id, value) {
+ svg.select('text.instLabel.'+id).text(value);
+ }
+
+ updAttr('ip', d.ip);
+ updAttr('ns', nSw(d.switches));
+ });
+
+
+ // operate on new onos instances
+ var entering = onoses.enter()
+ .append('div')
+ .attr('class', 'onosInst')
+ .classed('online', function (d) { return d.online; })
+ .on('click', clickInst);
+
+ entering.each(function (d) {
+ var el = d3.select(this),
+ rectAttr,
+ svg;
+ instDim = computeDim(this);
+ rectAttr = instRectAttr(instDim);
+
+ svg = el.append('svg').attr({
+ width: instDim.w,
+ height: instDim.h,
+ viewBox: viewBox(instDim)
+ });
+
+ svg.append('rect').attr(rectAttr);
+
+ gs.addGlyph(svg, 'bird', 28, true, [14, 14])
+ .classed('badgeIcon', true);
+
+ if (d.uiAttached) {
+ attachUiBadge(svg);
+ }
+
+ var left = c.nodeOx + c.nodeDim,
+ len = rectAttr.width - left,
+ hlen = len / 2,
+ midline = hlen + left;
+
+ // title
+ svg.append('text')
+ .attr({
+ class: 'instTitle',
+ x: midline,
+ y: c.titleDy
+ })
+ .text(d.id);
+
+ // a couple of attributes
+ var ty = c.titleDy + c.textYOff;
+
+ function addAttr(id, label) {
+ svg.append('text').attr({
+ class: 'instLabel ' + id,
+ x: midline,
+ y: ty
+ }).text(label);
+ ty += c.textYSpc;
+ }
+
+ addAttr('ip', d.ip);
+ addAttr('ns', nSw(d.switches));
+ });
+
+ // operate on existing + new onoses here
+ // set the affinity colors...
+ onoses.each(function (d) {
+ var el = d3.select(this),
+ rect = el.select('svg').select('rect'),
+ col = instColor(d.id, d.online);
+ rect.style('fill', col);
+ });
+
+ // adjust the panel size appropriately...
+ oiBox.width(instDim.w * onosOrder.length);
+ oiBox.height(instDim.h);
+
+ // remove any outgoing instances
+ onoses.exit().remove();
+ }
+
+
+ // ==========================
+
+ function logicError(msg) {
+ if (showLogicErrors) {
+ $log.warn('TopoInstService: ' + msg);
+ }
+ }
+
+ function initInst(_api_) {
+ api = _api_;
+ oiBox = ps.createPanel(idIns, instOpts);
+ oiBox.show();
+
+ onosInstances = {};
+ onosOrder = [];
+ oiShowMaster = false;
+
+ // we want to update the instances, each time the theme changes
+ themeListener = ts.addListener(updateInstances);
+ }
+
+ function destroyInst() {
+ ts.removeListener(themeListener);
+ themeListener = null;
+
+ ps.destroyPanel(idIns);
+ oiBox = null;
+
+ onosInstances = {};
+ onosOrder = [];
+ oiShowMaster = false;
+ }
+
+ function showInsts() {
+ oiBox.show();
+ }
+
+ function hideInsts() {
+ oiBox.hide();
+ }
+
+ function toggleInsts(x) {
+ var kev = (x === 'keyev'),
+ on,
+ verb;
+
+ if (kev) {
+ on = oiBox.toggle();
+ } else {
+ on = !!x;
+ if (on) {
+ showInsts();
+ } else {
+ hideInsts();
+ }
+ }
+ verb = on ? 'Show' : 'Hide';
+ flash.flash(verb + ' instances panel');
+ return on;
+ }
+
+ // ==========================
+
+ angular.module('ovTopo')
+ .factory('TopoInstService',
+ ['$log', 'PanelService', 'SvgUtilService', 'GlyphService',
+ 'ThemeService', 'FnService', 'FlashService',
+
+ function (_$log_, _ps_, _sus_, _gs_, _ts_, _fs_, _flash_) {
+ $log = _$log_;
+ ps = _ps_;
+ sus = _sus_;
+ gs = _gs_;
+ ts = _ts_;
+ fs = _fs_;
+ flash = _flash_;
+
+ return {
+ initInst: initInst,
+ destroyInst: destroyInst,
+
+ addInstance: addInstance,
+ updateInstance: updateInstance,
+ removeInstance: removeInstance,
+
+ cancelAffinity: cancelAffinity,
+
+ isVisible: function () { return oiBox.isVisible(); },
+ show: showInsts,
+ hide: hideInsts,
+ toggle: toggleInsts,
+ showMaster: function () { return oiShowMaster; }
+ };
+ }]);
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoLink.js b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoLink.js
new file mode 100644
index 00000000..38f3a6c3
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoLink.js
@@ -0,0 +1,338 @@
+/*
+ * 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 Link Module.
+ Functions for highlighting/selecting links
+ */
+
+(function () {
+ 'use strict';
+
+ // injected refs
+ var $log, fs, sus, ts, flash, tss, tps;
+
+ // internal state
+ var api,
+ td3,
+ network,
+ showPorts = true, // enable port highlighting by default
+ enhancedLink = null, // the link over which the mouse is hovering
+ selectedLink = null; // the link which is currently selected
+
+ // SVG elements;
+ var svg;
+
+
+ // ======== ALGORITHM TO FIND LINK CLOSEST TO MOUSE ========
+
+ function getLogicalMousePosition(container) {
+ var m = d3.mouse(container),
+ sc = api.zoomer.scale(),
+ tr = api.zoomer.translate(),
+ mx = (m[0] - tr[0]) / sc,
+ my = (m[1] - tr[1]) / sc;
+ return {x: mx, y: my};
+ }
+
+
+ function sq(x) { return x * x; }
+
+ function mdist(p, m) {
+ return Math.sqrt(sq(p.x - m.x) + sq(p.y - m.y));
+ }
+
+ function prox(dist) {
+ return dist / api.zoomer.scale();
+ }
+
+ function computeNearestNode(mouse) {
+ var proximity = prox(30),
+ nearest = null,
+ minDist;
+
+ if (network.nodes.length) {
+ minDist = proximity * 2;
+
+ network.nodes.forEach(function (d) {
+ var dist;
+
+ if (!api.showHosts() && d.class === 'host') {
+ return; // skip hidden hosts
+ }
+
+ dist = mdist({x: d.x, y: d.y}, mouse);
+ if (dist < minDist && dist < proximity) {
+ minDist = dist;
+ nearest = d;
+ }
+ });
+ }
+ return nearest;
+ }
+
+
+ function computeNearestLink(mouse) {
+ var proximity = prox(30),
+ nearest = null,
+ minDist;
+
+ function pdrop(line, mouse) {
+ var x1 = line.x1,
+ y1 = line.y1,
+ x2 = line.x2,
+ y2 = line.y2,
+ x3 = mouse.x,
+ y3 = mouse.y,
+ k = ((y2-y1) * (x3-x1) - (x2-x1) * (y3-y1)) /
+ (sq(y2-y1) + sq(x2-x1)),
+ x4 = x3 - k * (y2-y1),
+ y4 = y3 + k * (x2-x1);
+ return {x:x4, y:y4};
+ }
+
+ function lineHit(line, p, m) {
+ if (p.x < line.x1 && p.x < line.x2) return false;
+ if (p.x > line.x1 && p.x > line.x2) return false;
+ if (p.y < line.y1 && p.y < line.y2) return false;
+ if (p.y > line.y1 && p.y > line.y2) return false;
+ // line intersects, but are we close enough?
+ return mdist(p, m) <= proximity;
+ }
+
+ if (network.links.length) {
+ minDist = proximity * 2;
+
+ network.links.forEach(function (d) {
+ if (!api.showHosts() && d.type() === 'hostLink') {
+ return; // skip hidden host links
+ }
+
+ var line = d.position,
+ point = pdrop(line, mouse),
+ hit = lineHit(line, point, mouse),
+ dist;
+
+ if (hit) {
+ dist = mdist(point, mouse);
+ if (dist < minDist) {
+ minDist = dist;
+ nearest = d;
+ }
+ }
+ });
+ }
+ return nearest;
+ }
+
+ function enhanceLink(ldata) {
+ // if the new link is same as old link, do nothing
+ if (enhancedLink && ldata && enhancedLink.key === ldata.key) return;
+
+ // first, unenhance the currently enhanced link
+ if (enhancedLink) {
+ unenhance(enhancedLink);
+ }
+ enhancedLink = ldata;
+ if (enhancedLink) {
+ enhance(enhancedLink);
+ }
+ }
+
+ function unenhance(d) {
+ // guard against link element not set
+ if (d.el) {
+ d.el.classed('enhanced', false);
+ }
+ api.portLabelG().selectAll('.portLabel').remove();
+ }
+
+ function enhance(d) {
+ var data = [],
+ point;
+
+ // guard against link element not set
+ if (!d.el) return;
+
+ d.el.classed('enhanced', true);
+
+ // Define port label data objects.
+ // NOTE: src port is absent in the case of host-links.
+
+ point = locatePortLabel(d);
+ angular.extend(point, {
+ id: 'topo-port-tgt',
+ num: d.tgtPort
+ });
+ data.push(point);
+
+ if (d.srcPort) {
+ point = locatePortLabel(d, 1);
+ angular.extend(point, {
+ id: 'topo-port-src',
+ num: d.srcPort
+ });
+ data.push(point);
+ }
+
+ td3.applyPortLabels(data, api.portLabelG());
+ }
+
+ function locatePortLabel(link, src) {
+ var offset = 32,
+ pos = link.position,
+ nearX = src ? pos.x1 : pos.x2,
+ nearY = src ? pos.y1 : pos.y2,
+ farX = src ? pos.x2 : pos.x1,
+ farY = src ? pos.y2 : pos.y1;
+
+ function dist(x, y) { return Math.sqrt(x*x + y*y); }
+
+ var dx = farX - nearX,
+ dy = farY - nearY,
+ k = offset / dist(dx, dy);
+
+ return {x: k * dx + nearX, y: k * dy + nearY};
+ }
+
+ function selectLink(ldata) {
+ // if the new link is same as old link, do nothing
+ if (selectedLink && ldata && selectedLink.key === ldata.key) return;
+
+ // make sure no nodes are selected
+ tss.deselectAll();
+
+ // first, unenhance the currently enhanced link
+ if (selectedLink) {
+ unselLink(selectedLink);
+ }
+ selectedLink = ldata;
+ if (selectedLink) {
+ selLink(selectedLink);
+ }
+ }
+
+ function unselLink(d) {
+ // guard against link element not set
+ if (d.el) {
+ d.el.classed('selected', false);
+ }
+ }
+
+ function selLink(d) {
+ // guard against link element not set
+ if (!d.el) return;
+
+ d.el.classed('selected', true);
+
+ tps.displayLink(d);
+ tps.displaySomething();
+ }
+
+ // ====== MOUSE EVENT HANDLERS ======
+
+ function mouseMoveHandler() {
+ var mp = getLogicalMousePosition(this),
+ link = computeNearestLink(mp);
+ enhanceLink(link);
+ }
+
+ function mouseClickHandler() {
+ var mp, link, node;
+
+ if (!tss.clickConsumed()) {
+ mp = getLogicalMousePosition(this);
+ node = computeNearestNode(mp);
+ if (node) {
+ $log.debug('found nearest node:', node.labels[1]);
+ tss.selectObject(node);
+ } else {
+ link = computeNearestLink(mp);
+ selectLink(link);
+ }
+ }
+ }
+
+
+ // ======================
+
+ function togglePorts(x) {
+ var kev = (x === 'keyev'),
+ on = kev ? !showPorts : !!x,
+ what = on ? 'Enable' : 'Disable',
+ handler = on ? mouseMoveHandler : null;
+
+ showPorts = on;
+
+ if (!on) {
+ enhanceLink(null);
+ }
+ svg.on('mousemove', handler);
+ flash.flash(what + ' port highlighting');
+ return on;
+ }
+
+ function deselectLink() {
+ if (selectedLink) {
+ unselLink(selectedLink);
+ selectedLink = null;
+ return true;
+ }
+ return false;
+ }
+
+ // ==========================
+ // Module definition
+
+ angular.module('ovTopo')
+ .factory('TopoLinkService',
+ ['$log', 'FnService', 'SvgUtilService', 'ThemeService', 'FlashService',
+ 'TopoSelectService', 'TopoPanelService',
+
+ function (_$log_, _fs_, _sus_, _ts_, _flash_, _tss_, _tps_) {
+ $log = _$log_;
+ fs = _fs_;
+ sus = _sus_;
+ ts = _ts_;
+ flash = _flash_;
+ tss = _tss_;
+ tps = _tps_;
+
+ function initLink(_api_, _td3_) {
+ api = _api_;
+ td3 = _td3_;
+ svg = api.svg;
+ network = api.network;
+ if (showPorts && !fs.isMobile()) {
+ svg.on('mousemove', mouseMoveHandler);
+ }
+ svg.on('click', mouseClickHandler);
+ }
+
+ function destroyLink() {
+ // unconditionally remove any event handlers
+ svg.on('mousemove', null);
+ svg.on('click', null);
+ }
+
+ return {
+ initLink: initLink,
+ destroyLink: destroyLink,
+ togglePorts: togglePorts,
+ deselectLink: deselectLink
+ };
+ }]);
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoModel.js b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoModel.js
new file mode 100644
index 00000000..fb98fc2b
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoModel.js
@@ -0,0 +1,439 @@
+/*
+ * 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 Model Module.
+ Auxiliary functions for the model of the topology; that is, our internal
+ representations of devices, hosts, links, etc.
+ */
+
+(function () {
+ 'use strict';
+
+ // injected refs
+ var $log, fs, rnd;
+
+ // api to topoForce
+ var api;
+ /*
+ projection()
+ network {...}
+ restyleLinkElement( ldata )
+ removeLinkElement( ldata )
+ */
+
+ // shorthand
+ var lu, rlk, nodes, links, linksByDevice;
+
+ var dim; // dimensions of layout [w,h]
+
+ // configuration 'constants'
+ var defaultLinkType = 'direct',
+ nearDist = 15;
+
+
+ function coordFromLngLat(loc) {
+ var p = api.projection();
+ // suspected cause of ONOS-2109
+ return p ? p([loc.lng, loc.lat]) : [0, 0];
+ }
+
+ function lngLatFromCoord(coord) {
+ var p = api.projection();
+ return p ? p.invert(coord) : [0, 0];
+ }
+
+ function positionNode(node, forUpdate) {
+ var meta = node.metaUi,
+ x = meta && meta.x,
+ y = meta && meta.y,
+ xy;
+
+ // If we have [x,y] already, use that...
+ if (x && y) {
+ node.fixed = true;
+ node.px = node.x = x;
+ node.py = node.y = y;
+ return;
+ }
+
+ var location = node.location,
+ coord;
+
+ if (location && location.type === 'latlng') {
+ coord = coordFromLngLat(location);
+ node.fixed = true;
+ node.px = node.x = coord[0];
+ node.py = node.y = coord[1];
+ return true;
+ }
+
+ // if this is a node update (not a node add).. skip randomizer
+ if (forUpdate) {
+ return;
+ }
+
+ // Note: Placing incoming unpinned nodes at exactly the same point
+ // (center of the view) causes them to explode outwards when
+ // the force layout kicks in. So, we spread them out a bit
+ // initially, to provide a more serene layout convergence.
+ // Additionally, if the node is a host, we place it near
+ // the device it is connected to.
+
+ function rand() {
+ return {
+ x: rnd.randDim(dim[0]),
+ y: rnd.randDim(dim[1])
+ };
+ }
+
+ function near(node) {
+ return {
+ x: node.x + nearDist + rnd.spread(nearDist),
+ y: node.y + nearDist + rnd.spread(nearDist)
+ };
+ }
+
+ function getDevice(cp) {
+ var d = lu[cp.device];
+ return d || rand();
+ }
+
+ xy = (node.class === 'host') ? near(getDevice(node.cp)) : rand();
+ angular.extend(node, xy);
+ }
+
+ function mkSvgCls(dh, t, on) {
+ var ndh = 'node ' + dh,
+ ndht = t ? ndh + ' ' + t : ndh;
+ return on ? ndht + ' online' : ndht;
+ }
+
+ function createDeviceNode(device) {
+ var node = device;
+
+ // Augment as needed...
+ node.class = 'device';
+ node.svgClass = mkSvgCls('device', device.type, device.online);
+ positionNode(node);
+ return node;
+ }
+
+ function createHostNode(host) {
+ var node = host;
+
+ // Augment as needed...
+ node.class = 'host';
+ if (!node.type) {
+ node.type = 'endstation';
+ }
+ node.svgClass = mkSvgCls('host', node.type);
+ positionNode(node);
+ return node;
+ }
+
+ function createHostLink(host) {
+ var src = host.id,
+ dst = host.cp.device,
+ id = host.ingress,
+ lnk = linkEndPoints(src, dst);
+
+ if (!lnk) {
+ return null;
+ }
+
+ // Synthesize link ...
+ angular.extend(lnk, {
+ key: id,
+ class: 'link',
+ // NOTE: srcPort left undefined (host end of the link)
+ tgtPort: host.cp.port,
+
+ type: function () { return 'hostLink'; },
+ online: function () {
+ // hostlink target is edge switch
+ return lnk.target.online;
+ },
+ linkWidth: function () { return 1; }
+ });
+ return lnk;
+ }
+
+ function createLink(link) {
+ var lnk = linkEndPoints(link.src, link.dst);
+
+ if (!lnk) {
+ return null;
+ }
+
+ angular.extend(lnk, {
+ key: link.id,
+ class: 'link',
+ fromSource: link,
+ srcPort: link.srcPort,
+ tgtPort: link.dstPort,
+ position: {
+ x1: 0,
+ y1: 0,
+ x2: 0,
+ y2: 0
+ },
+
+ // functions to aggregate dual link state
+ type: function () {
+ var s = lnk.fromSource,
+ t = lnk.fromTarget;
+ return (s && s.type) || (t && t.type) || defaultLinkType;
+ },
+ online: function () {
+ var s = lnk.fromSource,
+ t = lnk.fromTarget,
+ both = lnk.source.online && lnk.target.online;
+ return both && ((s && s.online) || (t && t.online));
+ },
+ linkWidth: function () {
+ var s = lnk.fromSource,
+ t = lnk.fromTarget,
+ ws = (s && s.linkWidth) || 0,
+ wt = (t && t.linkWidth) || 0;
+ return lnk.position.multiLink ? 5 : Math.max(ws, wt);
+ }
+ });
+ return lnk;
+ }
+
+
+ function linkEndPoints(srcId, dstId) {
+ var srcNode = lu[srcId],
+ dstNode = lu[dstId],
+ sMiss = !srcNode ? missMsg('src', srcId) : '',
+ dMiss = !dstNode ? missMsg('dst', dstId) : '';
+
+ if (sMiss || dMiss) {
+ $log.error('Node(s) not on map for link:' + sMiss + dMiss);
+ //logicError('Node(s) not on map for link:\n' + sMiss + dMiss);
+ return null;
+ }
+ return {
+ source: srcNode,
+ target: dstNode
+ };
+ }
+
+ function missMsg(what, id) {
+ return '\n[' + what + '] "' + id + '" missing';
+ }
+
+
+ function makeNodeKey(d, what) {
+ var port = what + 'Port';
+ return d[what] + '/' + d[port];
+ }
+
+ function makeLinkKey(d, flipped) {
+ var one = flipped ? makeNodeKey(d, 'dst') : makeNodeKey(d, 'src'),
+ two = flipped ? makeNodeKey(d, 'src') : makeNodeKey(d, 'dst');
+ return one + '-' + two;
+ }
+
+ function findLinkById(id) {
+ // check to see if this is a reverse lookup, else default to given id
+ var key = rlk[id] || id;
+ return key && lu[key];
+ }
+
+ function findLink(linkData, op) {
+ var key = makeLinkKey(linkData),
+ keyrev = makeLinkKey(linkData, 1),
+ link = lu[key],
+ linkRev = lu[keyrev],
+ result = {},
+ ldata = link || linkRev,
+ rawLink;
+
+ if (op === 'add') {
+ if (link) {
+ // trying to add a link that we already know about
+ result.ldata = link;
+ result.badLogic = 'addLink: link already added';
+
+ } else if (linkRev) {
+ // we found the reverse of the link to be added
+ result.ldata = linkRev;
+ if (linkRev.fromTarget) {
+ result.badLogic = 'addLink: link already added';
+ }
+ }
+ } else if (op === 'update') {
+ if (!ldata) {
+ result.badLogic = 'updateLink: link not found';
+ } else {
+ rawLink = link ? ldata.fromSource : ldata.fromTarget;
+ result.updateWith = function (data) {
+ angular.extend(rawLink, data);
+ api.restyleLinkElement(ldata);
+ }
+ }
+ } else if (op === 'remove') {
+ if (!ldata) {
+ result.badLogic = 'removeLink: link not found';
+ } else {
+ rawLink = link ? ldata.fromSource : ldata.fromTarget;
+
+ if (!rawLink) {
+ result.badLogic = 'removeLink: link not found';
+
+ } else {
+ result.removeRawLink = function () {
+ // remove link out of aggregate linksByDevice list
+ var linksForDevPair = linksByDevice[ldata.devicePair],
+ rmvIdx = fs.find(ldata.key, linksForDevPair, 'key');
+ if (rmvIdx >= 0) {
+ linksForDevPair.splice(rmvIdx, 1);
+ }
+ ldata.position.multilink = linksForDevPair.length >= 5;
+
+ if (link) {
+ // remove fromSource
+ ldata.fromSource = null;
+ if (ldata.fromTarget) {
+ // promote target into source position
+ ldata.fromSource = ldata.fromTarget;
+ ldata.fromTarget = null;
+ ldata.key = keyrev;
+ delete lu[key];
+ lu[keyrev] = ldata;
+ delete rlk[keyrev];
+ }
+ } else {
+ // remove fromTarget
+ ldata.fromTarget = null;
+ delete rlk[keyrev];
+ }
+ if (ldata.fromSource) {
+ api.restyleLinkElement(ldata);
+ } else {
+ api.removeLinkElement(ldata);
+ }
+ }
+ }
+ }
+ }
+ return result;
+ }
+
+ function findDevices(offlineOnly) {
+ var a = [];
+ nodes.forEach(function (d) {
+ if (d.class === 'device' && !(offlineOnly && d.online)) {
+ a.push(d);
+ }
+ });
+ return a;
+ }
+
+ function findAttachedHosts(devId) {
+ var hosts = [];
+ nodes.forEach(function (d) {
+ if (d.class === 'host' && d.cp.device === devId) {
+ hosts.push(d);
+ }
+ });
+ return hosts;
+ }
+
+ function findAttachedLinks(devId) {
+ var lnks = [];
+ links.forEach(function (d) {
+ if (d.source.id === devId || d.target.id === devId) {
+ lnks.push(d);
+ }
+ });
+ return lnks;
+ }
+
+ // returns one-way links or where the internal link types differ
+ function findBadLinks() {
+ var lnks = [],
+ src, tgt;
+ links.forEach(function (d) {
+ // NOTE: skip edge links, which are synthesized
+ if (d.type() !== 'hostLink') {
+ delete d.bad;
+ src = d.fromSource;
+ tgt = d.fromTarget;
+ if (src && !tgt) {
+ d.bad = 'missing link';
+ } else if (src.type !== tgt.type) {
+ d.bad = 'type mismatch';
+ }
+ if (d.bad) {
+ lnks.push(d);
+ }
+ }
+ });
+ return lnks;
+ }
+
+ // ==========================
+ // Module definition
+
+ angular.module('ovTopo')
+ .factory('TopoModelService',
+ ['$log', 'FnService', 'RandomService',
+
+ function (_$log_, _fs_, _rnd_) {
+ $log = _$log_;
+ fs = _fs_;
+ rnd = _rnd_;
+
+ function initModel(_api_, _dim_) {
+ api = _api_;
+ dim = _dim_;
+ lu = api.network.lookup;
+ rlk = api.network.revLinkToKey;
+ nodes = api.network.nodes;
+ links = api.network.links;
+ linksByDevice = api.network.linksByDevice;
+ }
+
+ function newDim(_dim_) {
+ dim = _dim_;
+ }
+
+ function destroyModel() { }
+
+ return {
+ initModel: initModel,
+ newDim: newDim,
+ destroyModel: destroyModel,
+
+ positionNode: positionNode,
+ createDeviceNode: createDeviceNode,
+ createHostNode: createHostNode,
+ createHostLink: createHostLink,
+ createLink: createLink,
+ coordFromLngLat: coordFromLngLat,
+ lngLatFromCoord: lngLatFromCoord,
+ findLink: findLink,
+ findLinkById: findLinkById,
+ findDevices: findDevices,
+ findAttachedHosts: findAttachedHosts,
+ findAttachedLinks: findAttachedLinks,
+ findBadLinks: findBadLinks
+ }
+ }]);
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoOblique.js b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoOblique.js
new file mode 100644
index 00000000..e84b1173
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoOblique.js
@@ -0,0 +1,257 @@
+/*
+ * 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 Oblique View Module.
+ Provides functionality to view the topology as two planes (packet & optical)
+ from an oblique (side-on) perspective.
+ */
+
+(function () {
+ 'use strict';
+
+ // injected refs
+ var $log, fs, sus, flash;
+
+ // api to topoForce
+ var api;
+ /*
+ force() // get ref to force layout object
+ zoomLayer() // get ref to zoom layer
+ nodeGBBox() // get bounding box of node group layer
+ node() // get ref to D3 selection of nodes
+ link() // get ref to D3 selection of links
+ nodes() // get ref to network nodes array
+ tickStuff // ref to tick functions
+ nodeLock(b) // test-and-set nodeLock state
+ opacifyMap(b) // show or hide map layer
+ inLayer(d, layer) // return true if d in layer {'pkt'|'opt'}
+ calcLinkPos() // recomputes link pos based on node data
+ */
+
+ // configuration
+ var xsky = -.7, // x skew y factor
+ xsk = -35, // x skew angle
+ ysc = .5, // y scale
+ pad = 50,
+ time = 1500,
+ fill = {
+ pkt: 'rgba(130,130,170,0.3)', // blue-ish
+ opt: 'rgba(170,130,170,0.3)' // magenta-ish
+ };
+
+ // internal state
+ var oblique = false,
+ xffn = null,
+ plane = {},
+ oldNodeLock;
+
+
+ function planeId(tag) {
+ return 'topo-obview-' + tag + 'Plane';
+ }
+
+ function ytfn(h, dir) {
+ return h * ysc * dir * 1.1;
+ }
+
+ function obXform(h, dir) {
+ var yt = ytfn(h, dir);
+ return sus.scale(1, ysc) + sus.translate(0, yt) + sus.skewX(xsk);
+ }
+
+ function noXform() {
+ return sus.skewX(0) + sus.translate(0,0) + sus.scale(1,1);
+ }
+
+ function padBox(box, p) {
+ box.x -= p;
+ box.y -= p;
+ box.width += p*2;
+ box.height += p*2;
+ }
+
+ function toObliqueView() {
+ var box = api.nodeGBBox(),
+ ox, oy;
+
+ padBox(box, pad);
+
+ ox = box.x + box.width / 2;
+ oy = box.y + box.height / 2;
+
+ // remember node lock state, then lock the nodes down
+ oldNodeLock = api.nodeLock(true);
+ api.opacifyMap(false);
+
+ insertPlanes(ox, oy);
+
+ xffn = function (xy, dir) {
+ var yt = ytfn(box.height, dir),
+ ax = xy.x - ox,
+ ay = xy.y - oy,
+ x = ax + ay * xsky,
+ y = (ay + yt) * ysc;
+ return {x: ox + x, y: oy + y};
+ };
+
+ showPlane('pkt', box, -1);
+ showPlane('opt', box, 1);
+ obTransitionNodes();
+ }
+
+ function toNormalView() {
+ xffn = null;
+
+ hidePlane('pkt');
+ hidePlane('opt');
+ obTransitionNodes();
+
+ removePlanes();
+
+ // restore node lock state
+ api.nodeLock(oldNodeLock);
+ api.opacifyMap(true);
+ }
+
+ function obTransitionNodes() {
+ // return the direction for the node
+ // -1 for pkt layer, 1 for optical layer
+ function dir(d) {
+ return api.inLayer(d, 'pkt') ? -1 : 1;
+ }
+
+ if (xffn) {
+ api.nodes().forEach(function (d) {
+ var oldxy = {x: d.x, y: d.y},
+ coords = xffn(oldxy, dir(d));
+ d.oldxy = oldxy;
+ d.px = d.x = coords.x;
+ d.py = d.y = coords.y;
+ });
+ } else {
+ api.nodes().forEach(function (d) {
+ var old = d.oldxy || {x: d.x, y: d.y};
+ d.px = d.x = old.x;
+ d.py = d.y = old.y;
+ delete d.oldxy;
+ });
+ }
+
+ api.node().transition()
+ .duration(time)
+ .attr(api.tickStuff.nodeAttr);
+ api.link().transition()
+ .duration(time)
+ .call(api.calcLinkPos)
+ .attr(api.tickStuff.linkAttr)
+ .call(api.applyNumLinkLabels);
+ api.linkLabel().transition()
+ .duration(time)
+ .attr(api.tickStuff.linkLabelAttr);
+ }
+
+ function showPlane(tag, box, dir) {
+ // set box origin at center..
+ box.x = -box.width/2;
+ box.y = -box.height/2;
+
+ plane[tag].select('rect')
+ .attr(box)
+ .attr('opacity', 0)
+ .transition()
+ .duration(time)
+ .attr('opacity', 1)
+ .attr('transform', obXform(box.height, dir));
+ }
+
+ function hidePlane(tag) {
+ plane[tag].select('rect')
+ .transition()
+ .duration(time)
+ .attr('opacity', 0)
+ .attr('transform', noXform());
+ }
+
+ function insertPlanes(ox, oy) {
+ function ins(tag) {
+ var id = planeId(tag),
+ g = api.zoomLayer().insert('g', '#topo-G')
+ .attr('id', id)
+ .attr('transform', sus.translate(ox,oy));
+ g.append('rect')
+ .attr('fill', fill[tag])
+ .attr('opacity', 0);
+ plane[tag] = g;
+ }
+ ins('opt');
+ ins('pkt');
+ }
+
+ function removePlanes() {
+ function rem(tag) {
+ var id = planeId(tag);
+ api.zoomLayer().select('#'+id)
+ .transition()
+ .duration(time + 50)
+ .remove();
+ delete plane[tag];
+ }
+ rem('opt');
+ rem('pkt');
+ }
+
+
+// === -----------------------------------------------------
+// === MODULE DEFINITION ===
+
+angular.module('ovTopo')
+ .factory('TopoObliqueService',
+ ['$log', 'FnService', 'SvgUtilService', 'FlashService',
+
+ function (_$log_, _fs_, _sus_, _flash_) {
+ $log = _$log_;
+ fs = _fs_;
+ sus = _sus_;
+ flash = _flash_;
+
+ function initOblique(_api_) {
+ api = _api_;
+ }
+
+ function destroyOblique() { }
+
+ function toggleOblique() {
+ oblique = !oblique;
+ if (oblique) {
+ api.force().stop();
+ flash.flash('Oblique view');
+ toObliqueView();
+ } else {
+ flash.flash('Normal view');
+ toNormalView();
+ }
+ }
+
+ return {
+ initOblique: initOblique,
+ destroyOblique: destroyOblique,
+
+ isOblique: function () { return oblique; },
+ toggleOblique: toggleOblique
+ };
+ }]);
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoOverlay.js b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoOverlay.js
new file mode 100644
index 00000000..7eb45ba4
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoOverlay.js
@@ -0,0 +1,418 @@
+/*
+ * 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 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, fs, gs, wss, ns, tss, tps, api;
+
+ // internal state
+ var overlays = {},
+ current = null;
+
+ 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);
+ }
+
+ // TODO: remove this redundant code.......
+ // NOTE: unregister needs to be called if an app is ever
+ // deactivated/uninstalled via the applications view
+/*
+ function unregister(overlay) {
+ var u = 'unregister',
+ over = fs.isO(overlay),
+ id = over ? over.overlayId : '';
+
+ if (!id) {
+ return error(u, 'not a recognized overlay');
+ }
+ if (!overlays[id]) {
+ return warn(u, 'not registered: "' + id + "'")
+ }
+ delete overlays[id];
+ $log.debug(tos + 'unregistered overlay: ' + id);
+ }
+*/
+
+
+ // returns the list of overlay identifiers
+ function list() {
+ return d3.map(overlays).keys();
+ }
+
+ // add a radio button for each registered overlay
+ function augmentRbset(rset, switchFn) {
+ angular.forEach(overlays, function (ov) {
+ rset.push({
+ gid: ov._glyphId,
+ tooltip: (ov.tooltip || '(no tooltip)'),
+ cb: function () {
+ tbSelection(ov.overlayId, switchFn);
+ }
+ });
+ });
+ }
+
+ // 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 (!same) {
+ 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'
+ }
+ };
+
+ // 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();
+ }
+
+ // === -----------------------------------------------------
+ // Event (from server) Handlers
+
+ function setApi(_api_, _tss_) {
+ api = _api_;
+ tss = _tss_;
+ }
+
+ 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] )
+ */
+
+ // TODO: clear node highlighting
+ 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);
+ if (hdata && !hdata.el.empty()) {
+ if (!host.subdue) {
+ api.unsupNode(hdata.id, less);
+ }
+ // TODO: further highlighting?
+ }
+ });
+
+ data.devices.forEach(function (device) {
+ var ddata = api.findNodeById(device.id);
+ if (ddata && !ddata.el.empty()) {
+ if (!device.subdue) {
+ api.unsupNode(ddata.id, less);
+ }
+ // TODO: further highlighting?
+ }
+ });
+
+ data.links.forEach(function (link) {
+ var ldata = api.findLinkById(link.id),
+ lab = link.label,
+ units, portcls, magnitude;
+
+ if (ldata && !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);
+ }
+ }
+ });
+
+ api.updateNodes();
+ api.updateLinks();
+ }
+
+ // ========================================================================
+
+ angular.module('ovTopo')
+ .factory('TopoOverlayService',
+ ['$log', 'FnService', 'GlyphService', 'WebSocketService', 'NavService',
+ 'TopoPanelService',
+
+ function (_$log_, _fs_, _gs_, _wss_, _ns_, _tps_) {
+ $log = _$log_;
+ fs = _fs_;
+ gs = _gs_;
+ wss = _wss_;
+ ns = _ns_;
+ tps = _tps_;
+
+ return {
+ register: register,
+ //unregister: unregister,
+ setApi: setApi,
+ list: list,
+ augmentRbset: augmentRbset,
+ mkGlyphId: mkGlyphId,
+ tbSelection: tbSelection,
+ installButtons: installButtons,
+ addDetailButton: addDetailButton,
+ hooks: {
+ escape: escapeHook,
+ emptySelect: emptySelectHook,
+ singleSelect: singleSelectHook,
+ multiSelect: multiSelectHook,
+ mouseOver: mouseOverHook,
+ mouseOut: mouseOutHook
+ },
+
+ showHighlights: showHighlights
+ }
+ }]);
+
+}()); \ No newline at end of file
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoPanel.js b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoPanel.js
new file mode 100644
index 00000000..7db17f0d
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoPanel.js
@@ -0,0 +1,539 @@
+/*
+ * 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 Panel Module.
+ Defines functions for manipulating the summary, detail, and instance panels.
+ */
+
+(function () {
+ 'use strict';
+
+ // injected refs
+ var $log, $window, $rootScope, fs, ps, gs, flash, wss, bns, mast, ns;
+
+ // constants
+ var pCls = 'topo-p',
+ idSum = 'topo-p-summary',
+ idDet = 'topo-p-detail',
+ panelOpts = {
+ width: 260
+ },
+ sumMax = 240,
+ padTop = 20,
+ devPath = 'device';
+
+ // internal state
+ var useDetails = true, // should we show details if we have 'em?
+ haveDetails = false, // do we have details that we could show?
+ sumFromTop, // summary panel distance from top of screen
+ unbindWatch;
+
+ // panels
+ var summary, detail;
+
+ // === -----------------------------------------------------
+ // Panel API
+ function createTopoPanel(id, opts) {
+ var p = ps.createPanel(id, opts),
+ pid = id,
+ header, body, footer;
+ p.classed(pCls, true);
+
+ function panel() {
+ return p;
+ }
+
+ function hAppend(x) {
+ return header.append(x);
+ }
+
+ function bAppend(x) {
+ return body.append(x);
+ }
+
+ function fAppend(x) {
+ return footer.append(x);
+ }
+
+ function setup() {
+ p.empty();
+
+ p.append('div').classed('header', true);
+ p.append('div').classed('body', true);
+ p.append('div').classed('footer', true);
+
+ header = p.el().select('.header');
+ body = p.el().select('.body');
+ footer = p.el().select('.footer');
+ }
+
+ function destroy() {
+ ps.destroyPanel(pid);
+ }
+
+ // fromTop is how many pixels from the top of the page the panel is
+ // max is the max height of the panel in pixels
+ // only adjusts if the body content would be 10px or larger
+ function adjustHeight(fromTop, max) {
+ var totalPHeight, avSpace,
+ overflow = 0,
+ pdg = 30;
+
+ if (!fromTop) {
+ $log.warn('adjustHeight: height from top of page not given');
+ return null;
+ } else if (!body || !p) {
+ $log.warn('adjustHeight: panel contents are not defined');
+ return null;
+ }
+
+ p.el().style('height', null);
+ body.style('height', null);
+
+ totalPHeight = fromTop + p.height();
+ avSpace = fs.windowSize(pdg).height;
+
+ if (totalPHeight >= avSpace) {
+ overflow = totalPHeight - avSpace;
+ }
+
+ function _adjustBody(height) {
+ if (height < 10) {
+ return false;
+ } else {
+ body.style('height', height + 'px');
+ }
+ return true;
+ }
+
+ if (!_adjustBody(fs.noPxStyle(body, 'height') - overflow)) {
+ return;
+ }
+
+ if (max && p.height() > max) {
+ _adjustBody(fs.noPxStyle(body, 'height') - (p.height() - max));
+ }
+ }
+
+ return {
+ panel: panel,
+ setup: setup,
+ destroy: destroy,
+ appendHeader: hAppend,
+ appendBody: bAppend,
+ appendFooter: fAppend,
+ adjustHeight: adjustHeight
+ };
+ }
+
+ // === -----------------------------------------------------
+ // Utility functions
+
+ function addSep(tbody) {
+ tbody.append('tr').append('td').attr('colspan', 2).append('hr');
+ }
+
+ function addBtnFooter() {
+ detail.appendFooter('hr');
+ detail.appendFooter('div').classed('actionBtns', true);
+ }
+
+ function addProp(tbody, label, value) {
+ var tr = tbody.append('tr'),
+ lab;
+ if (typeof label === 'string') {
+ lab = label.replace(/_/g, ' ');
+ } else {
+ lab = label;
+ }
+
+ function addCell(cls, txt) {
+ tr.append('td').attr('class', cls).html(txt);
+ }
+ addCell('label', lab + ' :');
+ addCell('value', value);
+ }
+
+ function listProps(tbody, data) {
+ data.propOrder.forEach(function (p) {
+ if (p === '-') {
+ addSep(tbody);
+ } else {
+ addProp(tbody, p, data.props[p]);
+ }
+ });
+ }
+
+ function watchWindow() {
+ unbindWatch = $rootScope.$watchCollection(
+ function () {
+ return {
+ h: $window.innerHeight,
+ w: $window.innerWidth
+ };
+ }, function () {
+ summary.adjustHeight(sumFromTop, sumMax);
+ detail.adjustHeight(detail.ypos.current);
+ }
+ );
+ }
+
+ // === -----------------------------------------------------
+ // Functions for populating the summary panel
+
+ function populateSummary(data) {
+ summary.setup();
+
+ var svg = summary.appendHeader('div')
+ .classed('icon', true)
+ .append('svg'),
+ title = summary.appendHeader('h2'),
+ table = summary.appendBody('table'),
+ tbody = table.append('tbody'),
+ glyphId = data.type || 'node';
+
+ gs.addGlyph(svg, glyphId, 40);
+
+ if (glyphId === 'node') {
+ gs.addGlyph(svg, 'bird', 24, true, [8,12]);
+ }
+
+ title.text(data.title);
+ listProps(tbody, data);
+ }
+
+ // === -----------------------------------------------------
+ // Functions for populating the detail panel
+
+ var isDevice = {
+ switch: 1,
+ roadm: 1
+ };
+
+ function displaySingle(data) {
+ detail.setup();
+
+ var svg = detail.appendHeader('div')
+ .classed('icon clickable', true)
+ .append('svg'),
+ title = detail.appendHeader('h2')
+ .classed('clickable', true),
+ table = detail.appendBody('table'),
+ tbody = table.append('tbody'),
+ navFn;
+
+ gs.addGlyph(svg, (data.type || 'unknown'), 40);
+ title.text(data.title);
+
+ // only add navigation when displaying a device
+ if (isDevice[data.type]) {
+ navFn = function () {
+ ns.navTo(devPath, { devId: data.id });
+ };
+
+ svg.on('click', navFn);
+ title.on('click', navFn);
+ }
+
+ listProps(tbody, data);
+ addBtnFooter();
+ }
+
+ function displayMulti(ids) {
+ detail.setup();
+
+ var title = detail.appendHeader('h3'),
+ table = detail.appendBody('table'),
+ tbody = table.append('tbody');
+
+ title.text('Selected Nodes');
+ ids.forEach(function (d, i) {
+ addProp(tbody, i+1, d);
+ });
+ addBtnFooter();
+ }
+
+ function addAction(o) {
+ var btnDiv = d3.select('#' + idDet)
+ .select('.actionBtns')
+ .append('div')
+ .classed('actionBtn', true);
+ bns.button(btnDiv, idDet + '-' + o.id, o.gid, o.cb, o.tt);
+ }
+
+ var friendlyIndex = {
+ device: 1,
+ host: 0
+ };
+
+ function friendly(d) {
+ var i = friendlyIndex[d.class] || 0;
+ return (d.labels && d.labels[i]) || '';
+ }
+
+ function linkSummary(d) {
+ var o = d && d.online ? 'online' : 'offline';
+ return d ? d.type + ' / ' + o : '-';
+ }
+
+ // provided to change presentation of internal type name
+ var linkTypePres = {
+ hostLink: 'edge link'
+ };
+
+ function linkType(d) {
+ return linkTypePres[d.type()] || d.type();
+ }
+
+ var coreOrder = [
+ 'Type', '-',
+ 'A_type', 'A_id', 'A_label', 'A_port', '-',
+ 'B_type', 'B_id', 'B_label', 'B_port', '-'
+ ],
+ edgeOrder = [
+ 'Type', '-',
+ 'A_type', 'A_id', 'A_label', '-',
+ 'B_type', 'B_id', 'B_label', 'B_port'
+ ];
+
+ function displayLink(data) {
+ detail.setup();
+
+ var svg = detail.appendHeader('div')
+ .classed('icon', true)
+ .append('svg'),
+ title = detail.appendHeader('h2'),
+ table = detail.appendBody('table'),
+ tbody = table.append('tbody'),
+ edgeLink = data.type() === 'hostLink',
+ order = edgeLink ? edgeOrder : coreOrder;
+
+ gs.addGlyph(svg, 'ports', 40);
+ title.text('Link');
+
+
+ listProps(tbody, {
+ propOrder: order,
+ props: {
+ Type: linkType(data),
+
+ A_type: data.source.class,
+ A_id: data.source.id,
+ A_label: friendly(data.source),
+ A_port: data.srcPort,
+
+ B_type: data.target.class,
+ B_id: data.target.id,
+ B_label: friendly(data.target),
+ B_port: data.tgtPort
+ }
+ });
+
+ if (!edgeLink) {
+ addProp(tbody, 'A &rarr; B', linkSummary(data.fromSource));
+ addProp(tbody, 'B &rarr; A', linkSummary(data.fromTarget));
+ }
+ }
+
+ function displayNothing() {
+ haveDetails = false;
+ hideDetailPanel();
+ }
+
+ function displaySomething() {
+ haveDetails = true;
+ if (useDetails) {
+ showDetailPanel();
+ }
+ }
+
+ // === -----------------------------------------------------
+ // Event Handlers
+
+ function showSummary(data) {
+ populateSummary(data);
+ showSummaryPanel();
+ }
+
+ function toggleSummary(x) {
+ var kev = (x === 'keyev'),
+ on = kev ? !summary.panel().isVisible() : !!x,
+ verb = on ? 'Show' : 'Hide';
+
+ if (on) {
+ // ask server to start sending summary data.
+ wss.sendEvent('requestSummary');
+ // note: the summary panel will appear, once data arrives
+ } else {
+ hideSummaryPanel();
+ }
+ flash.flash(verb + ' summary panel');
+ return on;
+ }
+
+ // === -----------------------------------------------------
+ // === LOGIC For showing/hiding summary and detail panels...
+
+ function showSummaryPanel() {
+ function _show() {
+ summary.panel().show();
+ summary.adjustHeight(sumFromTop, sumMax);
+ }
+ if (detail.panel().isVisible()) {
+ detail.down(_show);
+ } else {
+ _show();
+ }
+ }
+
+ function hideSummaryPanel() {
+ // instruct server to stop sending summary data
+ wss.sendEvent("cancelSummary");
+ summary.panel().hide(detail.up);
+ }
+
+ function showDetailPanel() {
+ if (summary.panel().isVisible()) {
+ detail.down(detail.panel().show);
+ } else {
+ detail.up(detail.panel().show);
+ }
+ }
+
+ function hideDetailPanel() {
+ detail.panel().hide();
+ }
+
+ // ==========================
+
+ function augmentDetailPanel() {
+ var d = detail,
+ downPos = sumFromTop + sumMax + 20;
+ d.ypos = { up: sumFromTop, down: downPos, current: downPos};
+
+ d._move = function (y, cb) {
+ var yp = d.ypos,
+ endCb;
+
+ if (fs.isF(cb)) {
+ endCb = function () {
+ cb();
+ d.adjustHeight(d.ypos.current);
+ }
+ } else {
+ endCb = function () {
+ d.adjustHeight(d.ypos.current);
+ }
+ }
+ if (yp.current !== y) {
+ yp.current = y;
+ d.panel().el().transition().duration(300)
+ .each('end', endCb)
+ .style('top', yp.current + 'px');
+ } else {
+ endCb();
+ }
+ };
+
+ d.down = function (cb) { d._move(d.ypos.down, cb); };
+ d.up = function (cb) { d._move(d.ypos.up, cb); };
+ }
+
+ function toggleUseDetailsFlag(x) {
+ var kev = (x === 'keyev'),
+ verb;
+
+ useDetails = kev ? !useDetails : !!x;
+ verb = useDetails ? 'Enable' : 'Disable';
+
+ if (useDetails) {
+ if (haveDetails) {
+ showDetailPanel();
+ }
+ } else {
+ hideDetailPanel();
+ }
+ flash.flash(verb + ' details panel');
+ return useDetails;
+ }
+
+ // ==========================
+
+ function initPanels() {
+ sumFromTop = mast.mastHeight() + padTop;
+ summary = createTopoPanel(idSum, panelOpts);
+ detail = createTopoPanel(idDet, panelOpts);
+
+ augmentDetailPanel();
+ watchWindow();
+ }
+
+ function destroyPanels() {
+ summary.destroy();
+ summary = null;
+
+ detail.destroy();
+ detail = null;
+ haveDetails = false;
+ unbindWatch();
+ }
+
+ // ==========================
+
+ angular.module('ovTopo')
+ .factory('TopoPanelService',
+ ['$log', '$window', '$rootScope', 'FnService', 'PanelService', 'GlyphService',
+ 'FlashService', 'WebSocketService', 'ButtonService', 'MastService',
+ 'NavService',
+
+ function (_$log_, _$window_, _$rootScope_,
+ _fs_, _ps_, _gs_, _flash_, _wss_, _bns_, _mast_, _ns_) {
+ $log = _$log_;
+ $window = _$window_;
+ $rootScope = _$rootScope_;
+ fs = _fs_;
+ ps = _ps_;
+ gs = _gs_;
+ flash = _flash_;
+ wss = _wss_;
+ bns = _bns_;
+ mast = _mast_;
+ ns = _ns_;
+
+ return {
+ initPanels: initPanels,
+ destroyPanels: destroyPanels,
+ createTopoPanel: createTopoPanel,
+
+ showSummary: showSummary,
+ toggleSummary: toggleSummary,
+
+ toggleUseDetailsFlag: toggleUseDetailsFlag,
+ displaySingle: displaySingle,
+ displayMulti: displayMulti,
+ displayLink: displayLink,
+ displayNothing: displayNothing,
+ displaySomething: displaySomething,
+ addAction: addAction,
+
+ hideSummaryPanel: hideSummaryPanel,
+
+ detailVisible: function () { return detail.panel().isVisible(); },
+ summaryVisible: function () { return summary.panel().isVisible(); }
+ };
+ }]);
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoSelect.js b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoSelect.js
new file mode 100644
index 00000000..483c4baa
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoSelect.js
@@ -0,0 +1,286 @@
+/*
+ * 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, wss, tov, tps, tts, ns;
+
+ // api to topoForce
+ var api;
+ /*
+ node() // get ref to D3 selection of nodes
+ zoomingOrPanning( ev )
+ updateDeviceColors( [dev] )
+ deselectLink()
+ */
+
+ // 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
+ consumeClick = false; // used to coordinate with SVG click handler
+
+ // ==========================
+
+ 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) {
+ if (hovered != m) {
+ hovered = m;
+ tov.hooks.mouseOver({
+ id: m.id,
+ class: m.class,
+ type: m.type
+ });
+ }
+ }
+ }
+
+ function nodeMouseOut(m) {
+ if (!m.dragStarted) {
+ if (hovered) {
+ hovered = null;
+ tov.hooks.mouseOut();
+ }
+ }
+ }
+
+ // ==========================
+
+ function selectObject(obj) {
+ var el = this,
+ nodeEv = el && el.tagName === 'g',
+ ev = d3.event.sourceEvent || {},
+ n;
+
+ if (api.zoomingOrPanning(ev)) {
+ return;
+ }
+
+ if (nodeEv) {
+ n = d3.select(el);
+ } else {
+ api.node().each(function (d) {
+ if (d == obj) {
+ n = d3.select(el = this);
+ }
+ });
+ }
+ if (!n) return;
+
+ if (nodeEv) {
+ consumeClick = true;
+ }
+ api.deselectLink();
+
+ if (ev.shiftKey && n.classed('selected')) {
+ deselectObject(obj.id);
+ updateDetail();
+ return;
+ }
+
+ if (!ev.shiftKey) {
+ deselectAll(true);
+ }
+
+ selections[obj.id] = { obj: obj, el: el };
+ selectOrder.push(obj.id);
+
+ n.classed('selected', true);
+ api.updateDeviceColors(obj);
+ updateDetail();
+ }
+
+ 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);
+ }
+ }
+
+ function deselectAll(skipUpdate) {
+ var something = (selectOrder.length > 0);
+
+ // deselect all nodes in the network...
+ api.node().classed('selected', false);
+ selections = {};
+ selectOrder = [];
+ api.updateDeviceColors();
+ if (!skipUpdate) {
+ updateDetail();
+ }
+
+ // return true if something was selected
+ return something;
+ }
+
+ // === -----------------------------------------------------
+
+ function requestDetails(data) {
+ wss.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() {
+ tov.hooks.emptySelect();
+ tps.displayNothing();
+ }
+
+ function singleSelect() {
+ var data = getSel(0).obj;
+ requestDetails(data);
+ // NOTE: detail panel is shown as a response to receiving
+ // a 'showDetails' event from the server. See 'showDetails'
+ // callback function below...
+ }
+
+ function multiSelect() {
+ // display the selected nodes in the detail panel
+ tps.displayMulti(selectOrder);
+ addHostSelectionActions();
+ tov.hooks.multiSelect(selectOrder);
+ tps.displaySomething();
+ }
+
+ function addHostSelectionActions() {
+ if (allSelectionsClass('host')) {
+ if (nSel() === 2) {
+ tps.addAction({
+ id: 'host-flow-btn',
+ gid: 'endstation',
+ cb: tts.addHostIntent,
+ tt: 'Create Host-to-Host Flow'
+ });
+ } else if (nSel() >= 2) {
+ tps.addAction({
+ id: 'mult-src-flow-btn',
+ gid: 'flows',
+ cb: tts.addMultiSourceIntent,
+ tt: 'Create Multi-Source Flow'
+ });
+ }
+ }
+ }
+
+
+ // === -----------------------------------------------------
+ // Event Handlers
+
+ // display the data for the single selected node
+ function showDetails(data) {
+ var buttons = fs.isA(data.buttons) || [];
+ tps.displaySingle(data);
+ tov.installButtons(buttons, data, data.props['URI']);
+ tov.hooks.singleSelect(data);
+ tps.displaySomething();
+ }
+
+ // returns true if one or more nodes are selected.
+ function somethingSelected() {
+ return nSel();
+ }
+
+ function clickConsumed(x) {
+ var cc = consumeClick;
+ consumeClick = !!x;
+ return cc;
+ }
+
+ // === -----------------------------------------------------
+ // === MODULE DEFINITION ===
+
+ angular.module('ovTopo')
+ .factory('TopoSelectService',
+ ['$log', 'FnService', 'WebSocketService', 'TopoOverlayService',
+ 'TopoPanelService', 'TopoTrafficService', 'NavService',
+
+ function (_$log_, _fs_, _wss_, _tov_, _tps_, _tts_, _ns_) {
+ $log = _$log_;
+ fs = _fs_;
+ wss = _wss_;
+ tov = _tov_;
+ tps = _tps_;
+ tts = _tts_;
+ ns = _ns_;
+
+ function initSelect(_api_) {
+ api = _api_;
+ }
+
+ function destroySelect() { }
+
+ return {
+ initSelect: initSelect,
+ destroySelect: destroySelect,
+
+ showDetails: showDetails,
+
+ nodeMouseOver: nodeMouseOver,
+ nodeMouseOut: nodeMouseOut,
+ selectObject: selectObject,
+ deselectObject: deselectObject,
+ deselectAll: deselectAll,
+ updateDetail: updateDetail,
+
+ hovered: function () { return hovered; },
+ selectOrder: function () { return selectOrder; },
+ somethingSelected: somethingSelected,
+
+ clickConsumed: clickConsumed
+ };
+ }]);
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoSprite.js b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoSprite.js
new file mode 100644
index 00000000..1c957412
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoSprite.js
@@ -0,0 +1,262 @@
+/*
+ * 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 Sprite Module.
+ Defines behavior for loading sprites into the sprite layer.
+ */
+
+(function () {
+ 'use strict';
+
+ // injected refs
+ var $log, $http, fs, gs, sus, wss;
+
+ // constants
+ var tssid = 'TopoSpriteService: ',
+ fontsize = 20; // default font size 20pt.
+
+ // internal state
+ var spriteLayer, defsElement;
+
+
+ function registerPathsAsGlyphs(paths) {
+ var custom = {},
+ ids = [];
+
+ function mkd(d) {
+ return fs.isA(d) ? d.join('') : d;
+ }
+
+ paths.forEach(function (path) {
+ var tag = 'spr_' + path.tag;
+
+ if (path.glyph) {
+ // assumption is that we are using a built-in glyph
+ return;
+ }
+
+ custom['_' + tag] = path.viewbox || '0 0 1000 1000';
+ custom[tag] = mkd(path.d);
+ ids.push(tag);
+ });
+
+ gs.registerGlyphs(custom);
+ gs.loadDefs(defsElement, ids, true);
+ }
+
+ function applyStrokeStyle(s, use) {
+ var style;
+ if (s) {
+ style = {};
+ angular.forEach(s, function (value, key) {
+ style['stroke-' + key] = value;
+ });
+ use.style(style);
+ }
+ }
+
+ function applyFillClass(f, use) {
+ use.classed('fill-' + f, true);
+ }
+
+ function doSprite(spr, def, pathmeta) {
+ var pmeta = pathmeta[def.path],
+ c = spr.class || 'gray1',
+ p = spr.pos || [0,0],
+ lab = spr.label,
+ dim = def.dim || [40,40],
+ w = dim[0],
+ h = dim[1],
+ dy = def.labelyoff || 1,
+ sc = def.scale,
+ xfm = sus.translate(p),
+ g, attr, use;
+
+ if (sc) {
+ xfm += sus.scale(sc, sc);
+ }
+
+ g = spriteLayer.append('g')
+ .classed(c, true)
+ .attr('transform', xfm);
+
+ attr = {
+ width: w,
+ height: h,
+ 'xlink:href': '#' + pmeta.u
+ };
+
+ use = g.append('use').attr(attr);
+ applyStrokeStyle(pmeta.s, use);
+ applyFillClass(def.fill, use);
+
+
+ // add subpaths if they have been defined
+ if (fs.isA(def.subpaths)) {
+ def.subpaths.forEach(function (v) {
+ pmeta = pathmeta[v.path];
+ attr = {
+ width: w,
+ height: h,
+ 'xlink:href': '#' + pmeta.u,
+ transform: sus.translate(v.pos)
+ };
+ use = g.append('use').attr(attr);
+ applyStrokeStyle(pmeta.s, use);
+ applyFillClass(def.subpathfill, use);
+ });
+ }
+
+ if (lab) {
+ g.append('text')
+ .text(lab)
+ .attr({ x: w / 2, y: h * dy });
+ }
+ }
+
+ function doLabel(label) {
+ var c = label.class || 'gray1',
+ p = label.pos || [0,0],
+ sz = label.size || 1.0,
+ g = spriteLayer.append('g')
+ .classed(c, true)
+ .attr('transform', sus.translate(p))
+ .append('text')
+ .text(label.text)
+ .style('font-size', (fontsize * sz)+'pt');
+ }
+
+
+ // ==========================
+ // event handlers
+
+ // Handles response from 'spriteListRequest' which lists all the
+ // registered sprite definitions on the server.
+ // (see onos-upload-sprites)
+ function inList(payload) {
+ $log.debug(tssid + 'Registered sprite definitions:', payload.names);
+ // Some day, we will make this list available to the user in
+ // a dropdown selection box...
+ }
+
+ // Handles response from 'spriteDataRequest' which provides the
+ // data for the requested sprite definition.
+ function inData(payload) {
+ var data = payload.data,
+ name, desc, pfx, sprites, labels, alpha,
+ paths, defn, load,
+ pathmeta = {},
+ defs = {},
+ warn = [];
+
+ if (!data) {
+ $log.warn(tssid + 'No sprite data loaded.');
+ return;
+ }
+ name = data.defn_name;
+ desc = data.defn_desc;
+ paths = data.paths;
+ defn = data.defn;
+ load = data.load;
+ pfx = tssid + '[' + name + ']: ';
+
+ $log.debug("Loading sprites...[" + name + "]", desc);
+
+ function no(what) {
+ warn.push(pfx + 'No ' + what + ' property defined');
+ }
+
+ if (!paths) no('paths');
+ if (!defn) no('defn');
+ if (!load) no('load');
+
+ if (warn.length) {
+ $log.error(warn.join('\n'));
+ return;
+ }
+
+ // any custom paths need to be added to the glyph DB, and imported
+ registerPathsAsGlyphs(paths);
+
+ paths.forEach(function (p) {
+ pathmeta[p.tag] = {
+ s: p.stroke,
+ u: p.glyph || 'spr_' + p.tag
+ };
+ });
+
+ defn.forEach(function (d) {
+ defs[d.id] = d;
+ });
+
+ // sprites, labels and alpha are each optional components of the load
+ sprites = load.sprites;
+ labels = load.labels;
+ alpha = load.alpha;
+
+ if (alpha) {
+ spriteLayer.style('opacity', alpha);
+ }
+
+ if (sprites) {
+ sprites.forEach(function (spr) {
+ var def = defs[spr.id];
+ doSprite(spr, def, pathmeta);
+ });
+ }
+
+ if (labels) {
+ labels.forEach(doLabel);
+ }
+ }
+
+
+ function loadSprites(layer, defsElem, defname) {
+ var name = defname || 'sprites';
+ spriteLayer = layer;
+ defsElement = defsElem;
+
+ $log.info(tssid + 'Requesting sprite definition ['+name+']...');
+
+ wss.sendEvent('spriteListRequest');
+ wss.sendEvent('spriteDataRequest', {name: name});
+ }
+
+ // === -----------------------------------------------------
+ // === MODULE DEFINITION ===
+
+ angular.module('ovTopo')
+ .factory('TopoSpriteService',
+ ['$log', '$http', 'FnService', 'GlyphService',
+ 'SvgUtilService', 'WebSocketService',
+
+ function (_$log_, _$http_, _fs_, _gs_, _sus_, _wss_) {
+ $log = _$log_;
+ $http = _$http_;
+ fs = _fs_;
+ gs = _gs_;
+ sus = _sus_;
+ wss = _wss_;
+
+ return {
+ loadSprites: loadSprites,
+ spriteListResponse: inList,
+ spriteDataResponse: inData
+ };
+ }]);
+
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoToolbar.js b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoToolbar.js
new file mode 100644
index 00000000..84de261b
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoToolbar.js
@@ -0,0 +1,284 @@
+/*
+ * 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 Toolbar Module.
+ Functions for creating and interacting with the toolbar.
+ */
+
+(function () {
+ 'use strict';
+
+ // injected references
+ var $log, fs, tbs, ps, tov, api;
+
+ // API:
+ // getActionEntry
+ // setUpKeys
+
+ // internal state
+ var toolbar, keyData, cachedState, thirdRow;
+
+ // constants
+ var name = 'topo-tbar',
+ cooktag = 'topo_prefs',
+ soa = 'switchOverlayActions: ',
+ selOver = 'Select overlay here &#x21e7;';
+
+
+ // key to button mapping data
+ var k2b = {
+ O: { id: 'summary-tog', gid: 'summary', isel: true},
+ I: { id: 'instance-tog', gid: 'uiAttached', isel: true },
+ D: { id: 'details-tog', gid: 'details', isel: true },
+ H: { id: 'hosts-tog', gid: 'endstation', isel: false },
+ M: { id: 'offline-tog', gid: 'switch', isel: true },
+ P: { id: 'ports-tog', gid: 'ports', isel: true },
+ B: { id: 'bkgrnd-tog', gid: 'map', isel: false },
+ S: { id: 'sprite-tog', gid: 'cloud', isel: false },
+
+ //X: { id: 'nodelock-tog', gid: 'lock', isel: false },
+ Z: { id: 'oblique-tog', gid: 'oblique', isel: false },
+ N: { id: 'filters-btn', gid: 'filters' },
+ L: { id: 'cycleLabels-btn', gid: 'cycleLabels' },
+ R: { id: 'resetZoom-btn', gid: 'resetZoom' },
+
+ E: { id: 'eqMaster-btn', gid: 'eqMaster' }
+ };
+
+ var prohibited = [
+ 'T', 'backSlash', 'slash',
+ 'X' // needed until we re-instate X above.
+ ];
+ prohibited = prohibited.concat(d3.map(k2b).keys());
+
+
+ // initial toggle state: default settings and tag to key mapping
+ var defaultPrefsState = {
+ summary: 1,
+ insts: 1,
+ detail: 1,
+ hosts: 0,
+ offdev: 1,
+ porthl: 1,
+ bg: 0,
+ spr: 0,
+ toolbar: 0
+ },
+ prefsMap = {
+ summary: 'O',
+ insts: 'I',
+ detail: 'D',
+ hosts: 'H',
+ offdev: 'M',
+ porthl: 'P',
+ bg: 'B',
+ spr: 'S'
+ // NOTE: toolbar state is handled separately
+ };
+
+ function init(_api_) {
+ api = _api_;
+
+ // retrieve initial toggle button settings from user prefs
+ setInitToggleState();
+ }
+
+ function topoDefPrefs() {
+ return angular.extend({}, defaultPrefsState);
+ }
+
+ function setInitToggleState() {
+ cachedState = ps.asNumbers(ps.getPrefs(cooktag));
+ $log.debug('TOOLBAR---- read prefs state:', cachedState);
+
+ if (!cachedState) {
+ cachedState = topoDefPrefs();
+ ps.setPrefs(cooktag, cachedState);
+ $log.debug('TOOLBAR---- Set default prefs state:', cachedState);
+ }
+
+ angular.forEach(prefsMap, function (v, k) {
+ var cfg = k2b[v];
+ cfg && (cfg.isel = !!cachedState[k]);
+ });
+ }
+
+ function initKeyData() {
+ // TODO: use angular forEach instead of d3.map
+ keyData = d3.map(k2b);
+ keyData.forEach(function(key, value) {
+ var data = api.getActionEntry(key);
+ value.cb = data[0]; // on-click callback
+ value.tt = data[1] + ' (' + key + ')'; // tooltip
+ });
+ }
+
+ function addButton(key) {
+ var v = keyData.get(key);
+ v.btn = toolbar.addButton(v.id, v.gid, v.cb, v.tt);
+ }
+
+ function addToggle(key, suppressIfMobile) {
+ var v = keyData.get(key);
+ if (suppressIfMobile && fs.isMobile()) { return; }
+ v.tog = toolbar.addToggle(v.id, v.gid, v.isel, v.cb, v.tt);
+ }
+
+ function addFirstRow() {
+ addToggle('I');
+ addToggle('O');
+ addToggle('D');
+ toolbar.addSeparator();
+
+ addToggle('H');
+ addToggle('M');
+ addToggle('P', true);
+ addToggle('B');
+ addToggle('S', true);
+ }
+
+ function addSecondRow() {
+ //addToggle('X');
+ addToggle('Z');
+ addButton('N');
+ addButton('L');
+ addButton('R');
+ toolbar.addSeparator();
+ addButton('E');
+ }
+
+ function addOverlays() {
+ toolbar.addSeparator();
+
+ // generate radio button set for overlays; start with 'none'
+ var rset = [{
+ gid: 'topo',
+ tooltip: 'No Overlay',
+ cb: function () {
+ tov.tbSelection(null, switchOverlayActions);
+ }
+ }];
+ tov.augmentRbset(rset, switchOverlayActions);
+ toolbar.addRadioSet('topo-overlays', rset);
+ }
+
+ // invoked by overlay service to switch out old buttons and switch in new
+ function switchOverlayActions(oid, keyBindings) {
+ var prohibits = [],
+ kb = fs.isO(keyBindings) || {},
+ order = fs.isA(kb._keyOrder) || [];
+
+ if (keyBindings && !keyBindings._keyOrder) {
+ $log.warn(soa + 'no _keyOrder property defined');
+ } else {
+ // sanity removal of reserved property names
+ ['esc', '_keyListener', '_helpFormat'].forEach(function (k) {
+ fs.removeFromArray(k, order);
+ });
+ }
+
+ thirdRow.clear();
+
+ if (!order.length) {
+ thirdRow.setText(selOver);
+ thirdRow.classed('right', true);
+ api.setUpKeys(); // clear previous overlay key bindings
+
+ } else {
+ thirdRow.classed('right', false);
+ angular.forEach(order, function (key) {
+ var value, bid, gid, tt;
+
+ if (prohibited.indexOf(key) > -1) {
+ prohibits.push(key);
+
+ } else {
+ value = keyBindings[key];
+ bid = oid + '-' + key;
+ gid = tov.mkGlyphId(oid, value.gid);
+ tt = value.tt + ' (' + key + ')';
+ thirdRow.addButton(bid, gid, value.cb, tt);
+ }
+ });
+ api.setUpKeys(keyBindings); // add overlay key bindings
+ }
+
+ if (prohibits.length) {
+ $log.warn(soa + 'Prohibited key bindings ignored:', prohibits);
+ }
+ }
+
+ function createToolbar() {
+ initKeyData();
+ toolbar = tbs.createToolbar(name);
+ addFirstRow();
+ toolbar.addRow();
+ addSecondRow();
+ addOverlays();
+ thirdRow = toolbar.addRow();
+ thirdRow.setText(selOver);
+ thirdRow.classed('right', true);
+
+ if (cachedState.toolbar) {
+ toolbar.show();
+ } else {
+ toolbar.hide();
+ }
+ }
+
+ function destroyToolbar() {
+ tbs.destroyToolbar(name);
+ }
+
+ // allows us to ensure the button states track key strokes
+ function keyListener(key) {
+ var v = keyData.get(key);
+
+ if (v) {
+ // we have a valid button mapping
+ if (v.tog) {
+ // it's a toggle button
+ v.tog.toggleNoCb();
+ }
+ }
+ }
+
+ function toggleToolbar() {
+ toolbar.toggle();
+ }
+
+ angular.module('ovTopo')
+ .factory('TopoToolbarService',
+ ['$log', 'FnService', 'ToolbarService', 'PrefsService',
+ 'TopoOverlayService',
+
+ function (_$log_, _fs_, _tbs_, _ps_, _tov_) {
+ $log = _$log_;
+ fs = _fs_;
+ tbs = _tbs_;
+ ps = _ps_;
+ tov = _tov_;
+
+ return {
+ init: init,
+ createToolbar: createToolbar,
+ destroyToolbar: destroyToolbar,
+ keyListener: keyListener,
+ toggleToolbar: toggleToolbar
+ };
+ }]);
+}()); \ No newline at end of file
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoTraffic.js b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoTraffic.js
new file mode 100644
index 00000000..ca379360
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoTraffic.js
@@ -0,0 +1,220 @@
+/*
+ * 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 Traffic Module.
+ Defines behavior for viewing different traffic modes.
+ */
+
+(function () {
+ 'use strict';
+
+ // injected refs
+ var $log, fs, flash, wss, api;
+
+ /*
+ API to topoForce
+ hovered()
+ somethingSelected()
+ selectOrder()
+ */
+
+ // internal state
+ var trafficMode = null,
+ hoverMode = null;
+
+
+ // === -----------------------------------------------------
+ // Helper functions
+
+ // invoked in response to change in selection and/or mouseover/out:
+ function requestTrafficForMode(mouse) {
+ if (trafficMode === 'flows') {
+ requestDeviceLinkFlows();
+ } else if (trafficMode === 'intents') {
+ if (!mouse || hoverMode === 'intents') {
+ requestRelatedIntents();
+ }
+ } else {
+ // do nothing
+ }
+ }
+
+ function requestDeviceLinkFlows() {
+ // generates payload based on current hover-state
+ var hov = api.hovered();
+
+ function hoverValid() {
+ return hoverMode === 'flows' &&
+ hov && (hov.class === 'device');
+ }
+
+ if (api.somethingSelected()) {
+ wss.sendEvent('requestDeviceLinkFlows', {
+ ids: api.selectOrder(),
+ hover: hoverValid() ? hov.id : ''
+ });
+ }
+ }
+
+ function requestRelatedIntents() {
+ // generates payload based on current hover-state
+ var hov = api.hovered();
+
+ function hoverValid() {
+ return hoverMode === 'intents' &&
+ hov && (hov.class === 'host' || hov.class === 'device');
+ }
+
+ if (api.somethingSelected()) {
+ wss.sendEvent('requestRelatedIntents', {
+ ids: api.selectOrder(),
+ hover: hoverValid() ? hov.id : ''
+ });
+ }
+ }
+
+
+ // === -------------------------------------------------------------
+ // Traffic requests invoked from keystrokes or toolbar buttons...
+
+ function cancelTraffic(forced) {
+ if (!trafficMode || (!forced && trafficMode === 'allFlowPort')) {
+ return false;
+ }
+
+ trafficMode = hoverMode = null;
+ wss.sendEvent('cancelTraffic');
+ flash.flash('Traffic monitoring canceled');
+ return true;
+ }
+
+ function showAllFlowTraffic() {
+ trafficMode = 'allFlowPort';
+ hoverMode = null;
+ wss.sendEvent('requestAllFlowTraffic');
+ flash.flash('All Flow Traffic');
+ }
+
+ function showAllPortTraffic() {
+ trafficMode = 'allFlowPort';
+ hoverMode = null;
+ wss.sendEvent('requestAllPortTraffic');
+ flash.flash('All Port Traffic');
+ }
+
+ function showDeviceLinkFlows () {
+ trafficMode = hoverMode = 'flows';
+ requestDeviceLinkFlows();
+ flash.flash('Device Flows');
+ }
+
+ function showRelatedIntents () {
+ trafficMode = hoverMode = 'intents';
+ requestRelatedIntents();
+ flash.flash('Related Paths');
+ }
+
+ function showPrevIntent() {
+ if (trafficMode === 'intents') {
+ hoverMode = null;
+ wss.sendEvent('requestPrevRelatedIntent');
+ flash.flash('Previous related intent');
+ }
+ }
+
+ function showNextIntent() {
+ if (trafficMode === 'intents') {
+ hoverMode = null;
+ wss.sendEvent('requestNextRelatedIntent');
+ flash.flash('Next related intent');
+ }
+ }
+
+ function showSelectedIntentTraffic() {
+ if (trafficMode === 'intents') {
+ hoverMode = null;
+ wss.sendEvent('requestSelectedIntentTraffic');
+ flash.flash('Traffic on Selected Path');
+ }
+ }
+
+
+ // === ------------------------------------------------------
+ // action buttons on detail panel (multiple selection)
+
+ function addHostIntent () {
+ var so = api.selectOrder();
+ wss.sendEvent('addHostIntent', {
+ one: so[0],
+ two: so[1],
+ ids: so
+ });
+ trafficMode = 'intents';
+ hoverMode = null;
+ flash.flash('Host-to-Host flow added');
+ }
+
+ function addMultiSourceIntent () {
+ var so = api.selectOrder();
+ wss.sendEvent('addMultiSourceIntent', {
+ src: so.slice(0, so.length - 1),
+ dst: so[so.length - 1],
+ ids: so
+ });
+ trafficMode = 'intents';
+ hoverMode = null;
+ flash.flash('Multi-Source flow added');
+ }
+
+
+ // === -----------------------------------------------------
+ // === MODULE DEFINITION ===
+
+ angular.module('ovTopo')
+ .factory('TopoTrafficService',
+ ['$log', 'FnService', 'FlashService', 'WebSocketService',
+
+ function (_$log_, _fs_, _flash_, _wss_) {
+ $log = _$log_;
+ fs = _fs_;
+ flash = _flash_;
+ wss = _wss_;
+
+ return {
+ initTraffic: function (_api_) { api = _api_; },
+ destroyTraffic: function () { },
+
+ // invoked from toolbar overlay buttons or keystrokes
+ cancelTraffic: cancelTraffic,
+ showAllFlowTraffic: showAllFlowTraffic,
+ showAllPortTraffic: showAllPortTraffic,
+ showDeviceLinkFlows: showDeviceLinkFlows,
+ showRelatedIntents: showRelatedIntents,
+ showPrevIntent: showPrevIntent,
+ showNextIntent: showNextIntent,
+ showSelectedIntentTraffic: showSelectedIntentTraffic,
+
+ // invoked from mouseover/mouseout and selection change
+ requestTrafficForMode: requestTrafficForMode,
+
+ // TODO: these should move to new UI demo app
+ // invoked from buttons on detail (multi-select) panel
+ addHostIntent: addHostIntent,
+ addMultiSourceIntent: addMultiSourceIntent
+ };
+ }]);
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoTrafficNew.js b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoTrafficNew.js
new file mode 100644
index 00000000..cb4bc49a
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/topo/topoTrafficNew.js
@@ -0,0 +1,159 @@
+/*
+ * 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 Traffic Overlay Module.
+ Defines behavior for viewing different traffic modes.
+ Installed as a Topology Overlay.
+ */
+(function () {
+ 'use strict';
+
+ // injected refs
+ var $log, tov, tts;
+
+ // NOTE: no internal state here -- see TopoTrafficService for that
+
+ // NOTE: providing button disabling requires too big a refactoring of
+ // the button factory etc. Will have to be done another time.
+
+
+ // traffic overlay definition
+ var overlay = {
+ overlayId: 'traffic',
+ glyphId: 'allTraffic',
+ tooltip: 'Traffic Overlay',
+
+ // NOTE: Traffic glyphs already installed as part of the base ONOS set.
+
+ activate: function () {
+ $log.debug("Traffic overlay ACTIVATED");
+ },
+
+ deactivate: function () {
+ tts.cancelTraffic(true);
+ $log.debug("Traffic overlay DEACTIVATED");
+ },
+
+ // detail panel button definitions
+ // (keys match button identifiers, also defined in TrafficOverlay.java)
+ buttons: {
+ showDeviceFlows: {
+ gid: 'flows',
+ tt: 'Show Device Flows',
+ cb: function (data) { tts.showDeviceLinkFlows(); }
+ },
+
+ showRelatedTraffic: {
+ gid: 'relatedIntents',
+ tt: 'Show Related Traffic',
+ cb: function (data) { tts.showRelatedIntents(); }
+ }
+ },
+
+ // key bindings for traffic overlay toolbar buttons
+ // NOTE: fully qual. button ID is derived from overlay-id and key-name
+ keyBindings: {
+ 0: {
+ cb: function () { tts.cancelTraffic(true); },
+ tt: 'Cancel traffic monitoring',
+ gid: 'xMark'
+ },
+
+ A: {
+ cb: function () { tts.showAllFlowTraffic(); },
+ tt: 'Monitor all traffic using flow stats',
+ gid: 'allTraffic'
+ },
+ Q: {
+ cb: function () { tts.showAllPortTraffic(); },
+ tt: 'Monitor all traffic using port stats',
+ gid: 'allTraffic'
+ },
+ F: {
+ cb: function () { tts.showDeviceLinkFlows(); },
+ tt: 'Show device link flows',
+ gid: 'flows'
+ },
+ V: {
+ cb: function () { tts.showRelatedIntents(); },
+ tt: 'Show all related intents',
+ gid: 'relatedIntents'
+ },
+ leftArrow: {
+ cb: function () { tts.showPrevIntent(); },
+ tt: 'Show previous related intent',
+ gid: 'prevIntent'
+ },
+ rightArrow: {
+ cb: function () { tts.showNextIntent(); },
+ tt: 'Show next related intent',
+ gid: 'nextIntent'
+ },
+ W: {
+ cb: function () { tts.showSelectedIntentTraffic(); },
+ tt: 'Monitor traffic of selected intent',
+ gid: 'intentTraffic'
+ },
+
+ _keyOrder: [
+ '0', 'A', 'Q', 'F', 'V', 'leftArrow', 'rightArrow', 'W'
+ ]
+ },
+
+ hooks: {
+ // hook for handling escape key
+ escape: function () {
+ // Must return true to consume ESC, false otherwise.
+ return tts.cancelTraffic(true);
+ },
+
+ // hooks for when the selection changes...
+ empty: function () {
+ tts.cancelTraffic();
+ },
+ single: function (data) {
+ tts.requestTrafficForMode();
+ },
+ multi: function (selectOrder) {
+ tts.requestTrafficForMode();
+ tov.addDetailButton('showRelatedTraffic');
+ },
+
+ // mouse hooks
+ mouseover: function (m) {
+ // m has id, class, and type properties
+ tts.requestTrafficForMode(true);
+ },
+ mouseout: function () {
+ tts.requestTrafficForMode(true);
+ }
+ }
+ };
+
+ // invoke code to register with the overlay service
+ angular.module('ovTopo')
+ .run(['$log', 'TopoOverlayService', 'TopoTrafficService',
+
+ function (_$log_, _tov_, _tts_) {
+ $log = _$log_;
+ tov = _tov_;
+ tts = _tts_;
+ tov.register(overlay);
+ }]);
+
+}());
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/tunnel/tunnel.css b/framework/src/onos/web/gui/src/main/webapp/app/view/tunnel/tunnel.css
new file mode 100644
index 00000000..da7d06da
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/tunnel/tunnel.css
@@ -0,0 +1,27 @@
+/*
+ * 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 -- Link View -- CSS file
+ */
+
+#ov-tunnel h2 {
+ display: inline-block;
+}
+
+#ov-tunnel div.ctrl-btns {
+ width: 45px;
+} \ No newline at end of file
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/tunnel/tunnel.html b/framework/src/onos/web/gui/src/main/webapp/app/view/tunnel/tunnel.html
new file mode 100644
index 00000000..4909cc0e
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/tunnel/tunnel.html
@@ -0,0 +1,72 @@
+<!--
+ ~ 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.
+ -->
+
+<!-- Tunnel partial HTML -->
+<div id="ov-tunnel">
+ <div class="tabular-header">
+ <h2>Tunnels ({{tableData.length}} total)</h2>
+ <div class="ctrl-btns">
+ <div class="refresh" ng-class="{active: autoRefresh}"
+ icon icon-size="36" icon-id="refresh"
+ tooltip tt-msg="autoRefreshTip"
+ ng-click="toggleRefresh()"></div>
+ </div>
+ </div>
+
+ <div class="summary-list" onos-table-resize>
+ <div ng-show="loading" class="loading-wheel"
+ icon icon-id="loading" icon-size="75"></div>
+
+ <div class="table-header" onos-sortable-header>
+ <table>
+ <tr>
+ <td colId="id" sortable>ID </td>
+ <td colId="name" sortable>Name</td>
+ <td colId="one" sortable>Port 1 </td>
+ <td colId="two" sortable>Port 2 </td>
+ <td colId="type" sortable>Type </td>
+ <td colId="group_id" sortable>Group Id</td>
+ <td colId="bandwidth" sortable>Bandwidth</td>
+ <td colId="path" sortable>Path</td>
+ </tr>
+ </table>
+ </div>
+
+ <div class="table-body">
+ <table onos-flash-changes id-prop="one">
+ <tr ng-if="!tableData.length" class="no-data">
+ <td colspan="6">
+ No tunnels found
+ </td>
+ </tr>
+
+ <tr ng-repeat="tunnel in tableData track by $index"
+ ng-repeat-complete row-id="{{tunnel.id}}">
+ <td>{{tunnel.id}}</td>
+ <td>{{tunnel.name}}</td>
+ <td>{{tunnel.one}}</td>
+ <td>{{tunnel.two}}</td>
+ <td>{{tunnel.type}}</td>
+ <td>{{tunnel.group_id}}</td>
+ <td>{{tunnel.bandwidth}}</td>
+ <td>{{tunnel.path}}</td>
+ </tr>
+ </table>
+ </div>
+
+ </div>
+
+</div>
diff --git a/framework/src/onos/web/gui/src/main/webapp/app/view/tunnel/tunnel.js b/framework/src/onos/web/gui/src/main/webapp/app/view/tunnel/tunnel.js
new file mode 100644
index 00000000..5cde0457
--- /dev/null
+++ b/framework/src/onos/web/gui/src/main/webapp/app/view/tunnel/tunnel.js
@@ -0,0 +1,44 @@
+/*
+ * 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 -- Host View Module
+ */
+
+(function () {
+ 'use strict';
+
+ angular.module('ovTunnel', [])
+ .controller('OvTunnelCtrl',
+ ['$log', '$scope', '$sce', 'FnService', 'TableBuilderService',
+
+ function ($log, $scope, $sce, fs, tbs) {
+ tbs.buildTable({
+ scope: $scope,
+ tag: 'tunnel'
+ });
+
+ $scope.$watch('tableData', function () {
+ if (!fs.isEmptyObject($scope.tableData)) {
+ $scope.tableData.forEach(function (tunnel) {
+ //tunnel.direction = $sce.trustAsHtml(tunnel.direction);
+ });
+ }
+ });
+
+ $log.log('OvTunnelCtrl has been created');
+ }]);
+}());