diff options
Diffstat (limited to 'framework/src/onos/web/gui/src/main/webapp/app/fw')
46 files changed, 6331 insertions, 0 deletions
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', []); + +}()); |