DISPATCH-531 Initial version of openstack horizon plugin
Project: http://git-wip-us.apache.org/repos/asf/qpid-dispatch/repo Commit: http://git-wip-us.apache.org/repos/asf/qpid-dispatch/commit/0c58c381 Tree: http://git-wip-us.apache.org/repos/asf/qpid-dispatch/tree/0c58c381 Diff: http://git-wip-us.apache.org/repos/asf/qpid-dispatch/diff/0c58c381 Branch: refs/heads/master Commit: 0c58c3814866cbe60f13ff69ad73c74a4d8692aa Parents: 9790303 Author: Ernest Allen <eal...@redhat.com> Authored: Wed Oct 19 08:48:34 2016 -0400 Committer: Ernest Allen <eal...@redhat.com> Committed: Wed Oct 19 08:48:34 2016 -0400 ---------------------------------------------------------------------- console/dispatch-dashboard/MANIFEST.in | 3 + console/dispatch-dashboard/README.rst | 41 + console/dispatch-dashboard/dispatch/__init__.py | 0 .../dispatch-dashboard/dispatch/dashboard.py | 23 + .../dispatch/overv/__init__.py | 0 .../dispatch-dashboard/dispatch/overv/panel.py | 20 + .../dispatch/overv/templates/overv/index.html | 13 + .../dispatch-dashboard/dispatch/overv/tests.py | 19 + .../dispatch-dashboard/dispatch/overv/urls.py | 20 + .../dispatch-dashboard/dispatch/overv/views.py | 22 + .../static/dashboard/dispatch/connect.json | 2 + .../dashboard/dispatch/dispatch.comService.js | 935 + .../dashboard/dispatch/dispatch.module.js | 256 + .../static/dashboard/dispatch/dispatch.scss | 2135 ++ .../dashboard/dispatch/jquery.dynatree.min.js | 4 + .../static/dashboard/dispatch/lib/d3.v3.min.js | 5 + .../static/dashboard/dispatch/lib/rhea-min.js | 4 + .../static/dashboard/dispatch/lib/slider.js | 233 + .../dashboard/dispatch/lib/tooltipsy.min.js | 20 + .../static/dashboard/dispatch/lib/ui-grid.js | 28540 +++++++++++++++++ .../dispatch/overv/overview.controller.js | 1428 + .../dashboard/dispatch/overv/overview.module.js | 178 + .../dashboard/dispatch/qdrChartService.js | 1109 + .../dispatch/topology/config-file-header.html | 17 + .../topology/download-dialog-template.html | 23 + .../dispatch/topology/node-config-template.html | 51 + .../dispatch/topology/topology.controller.js | 1703 + .../topology/topology.download-controller.js | 150 + .../topology/topology.form-controller.js | 73 + .../dispatch/topology/topology.module.js | 112 + .../topology/topology.node-controller.js | 294 + .../dispatch/templates/dispatch/base.html | 10 + .../dispatch/topology/__init__.py | 0 .../dispatch/topology/panel.py | 20 + .../topology/templates/topology/index.html | 35 + .../dispatch/topology/tests.py | 19 + .../dispatch/topology/urls.py | 20 + .../dispatch/topology/views.py | 22 + .../enabled/_4000_dispatch.py | 33 + .../enabled/_4030_dispatch_overv_panel.py | 9 + .../enabled/_4050_dispatch_topology_panel.py | 9 + console/dispatch-dashboard/setup.py | 42 + 42 files changed, 37652 insertions(+) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/0c58c381/console/dispatch-dashboard/MANIFEST.in ---------------------------------------------------------------------- diff --git a/console/dispatch-dashboard/MANIFEST.in b/console/dispatch-dashboard/MANIFEST.in new file mode 100644 index 0000000..1d1b591 --- /dev/null +++ b/console/dispatch-dashboard/MANIFEST.in @@ -0,0 +1,3 @@ +include setup.py + +recursive-include dispatch * http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/0c58c381/console/dispatch-dashboard/README.rst ---------------------------------------------------------------------- diff --git a/console/dispatch-dashboard/README.rst b/console/dispatch-dashboard/README.rst new file mode 100644 index 0000000..ac3b17c --- /dev/null +++ b/console/dispatch-dashboard/README.rst @@ -0,0 +1,41 @@ +========= +dispatch_dashboard +========= + +Qpid Dispatch Router Horizon plugin + +Manual Installation +------------------- + +Copy the contents of this directoty to /opt/stack/dispatch_plugin and setup the plugin:: + + cd /opt/stack/dispatch_plugin/ + python setup.py sdist + +If needed, create a virtual environment and install Horizon dependencies:: + + cd /opt/stack/horizon + python tools/install_venv.py + +If needed, set up your ``local_settings.py`` file:: + + cp openstack_dashboard/local/local_settings.py.example openstack_dashboard/local/local_settings.py + + +Install the dispatch dashboard in your horizon virtual environment:: + + ./tools/with_venv.sh pip install ../dispatch-plugin/dist/dispatch-0.0.1.tar.gz + +And enable it in Horizon:: + + cp ../dispatch-plugin/enabled/_4*.py openstack_dashboard/local/enabled + +If needed, compress the files:: + + ./tools/with-venv.sh python manage.py compress + +Run a server in the virtual environment:: + + ./tools/with-venv.sh python manage.py runserver 0.0.0.0:8080 + +The horizon dashboard will be available in your browser at http://localhost:8080/ http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/0c58c381/console/dispatch-dashboard/dispatch/__init__.py ---------------------------------------------------------------------- diff --git a/console/dispatch-dashboard/dispatch/__init__.py b/console/dispatch-dashboard/dispatch/__init__.py new file mode 100644 index 0000000..e69de29 http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/0c58c381/console/dispatch-dashboard/dispatch/dashboard.py ---------------------------------------------------------------------- diff --git a/console/dispatch-dashboard/dispatch/dashboard.py b/console/dispatch-dashboard/dispatch/dashboard.py new file mode 100644 index 0000000..9fad953 --- /dev/null +++ b/console/dispatch-dashboard/dispatch/dashboard.py @@ -0,0 +1,23 @@ +# 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. + +from django.utils.translation import ugettext_lazy as _ + +import horizon + + +class Dispatch(horizon.Dashboard): + name = _("Qpid Dispatch") + slug = "dispatch" + default_panel = 'overv' # slug of the dashboard's default panel. + +horizon.register(Dispatch) http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/0c58c381/console/dispatch-dashboard/dispatch/overv/__init__.py ---------------------------------------------------------------------- diff --git a/console/dispatch-dashboard/dispatch/overv/__init__.py b/console/dispatch-dashboard/dispatch/overv/__init__.py new file mode 100644 index 0000000..e69de29 http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/0c58c381/console/dispatch-dashboard/dispatch/overv/panel.py ---------------------------------------------------------------------- diff --git a/console/dispatch-dashboard/dispatch/overv/panel.py b/console/dispatch-dashboard/dispatch/overv/panel.py new file mode 100644 index 0000000..315c7e0 --- /dev/null +++ b/console/dispatch-dashboard/dispatch/overv/panel.py @@ -0,0 +1,20 @@ +# 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. + +from django.utils.translation import ugettext_lazy as _ + +import horizon + + +class Overv(horizon.Panel): + name = _("Overview") + slug = "overv" http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/0c58c381/console/dispatch-dashboard/dispatch/overv/templates/overv/index.html ---------------------------------------------------------------------- diff --git a/console/dispatch-dashboard/dispatch/overv/templates/overv/index.html b/console/dispatch-dashboard/dispatch/overv/templates/overv/index.html new file mode 100644 index 0000000..afe47bd --- /dev/null +++ b/console/dispatch-dashboard/dispatch/overv/templates/overv/index.html @@ -0,0 +1,13 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Overv" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Overview") %} +{% endblock page_header %} + +{% block main %} + <ng-include src="'dispatch/overview.html'"></ng-include> +{% endblock %} + + http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/0c58c381/console/dispatch-dashboard/dispatch/overv/tests.py ---------------------------------------------------------------------- diff --git a/console/dispatch-dashboard/dispatch/overv/tests.py b/console/dispatch-dashboard/dispatch/overv/tests.py new file mode 100644 index 0000000..47816a3 --- /dev/null +++ b/console/dispatch-dashboard/dispatch/overv/tests.py @@ -0,0 +1,19 @@ +# 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. + +from horizon.test import helpers as test + + +class OvervTests(test.TestCase): + # Unit tests for overv. + def test_me(self): + self.assertTrue(1 + 1 == 2) http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/0c58c381/console/dispatch-dashboard/dispatch/overv/urls.py ---------------------------------------------------------------------- diff --git a/console/dispatch-dashboard/dispatch/overv/urls.py b/console/dispatch-dashboard/dispatch/overv/urls.py new file mode 100644 index 0000000..6debf00 --- /dev/null +++ b/console/dispatch-dashboard/dispatch/overv/urls.py @@ -0,0 +1,20 @@ +# 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. + +from django.conf.urls import url + +from dispatch.overv import views + + +urlpatterns = [ + url(r'^$', views.IndexView.as_view(), name='index'), +] http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/0c58c381/console/dispatch-dashboard/dispatch/overv/views.py ---------------------------------------------------------------------- diff --git a/console/dispatch-dashboard/dispatch/overv/views.py b/console/dispatch-dashboard/dispatch/overv/views.py new file mode 100644 index 0000000..235a0d5 --- /dev/null +++ b/console/dispatch-dashboard/dispatch/overv/views.py @@ -0,0 +1,22 @@ +# 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. + +from horizon import views + + +class IndexView(views.APIView): + # A very simple class-based view... + template_name = 'dispatch/overv/index.html' + + def get_data(self, request, context, *args, **kwargs): + # Add data to the context here... + return context http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/0c58c381/console/dispatch-dashboard/dispatch/static/dashboard/dispatch/connect.json ---------------------------------------------------------------------- diff --git a/console/dispatch-dashboard/dispatch/static/dashboard/dispatch/connect.json b/console/dispatch-dashboard/dispatch/static/dashboard/dispatch/connect.json new file mode 100644 index 0000000..2be876d --- /dev/null +++ b/console/dispatch-dashboard/dispatch/static/dashboard/dispatch/connect.json @@ -0,0 +1,2 @@ +)]}', +{"address": "0.0.0.0", "port": 5673} http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/0c58c381/console/dispatch-dashboard/dispatch/static/dashboard/dispatch/dispatch.comService.js ---------------------------------------------------------------------- diff --git a/console/dispatch-dashboard/dispatch/static/dashboard/dispatch/dispatch.comService.js b/console/dispatch-dashboard/dispatch/static/dashboard/dispatch/dispatch.comService.js new file mode 100644 index 0000000..ace792a --- /dev/null +++ b/console/dispatch-dashboard/dispatch/static/dashboard/dispatch/dispatch.comService.js @@ -0,0 +1,935 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you 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. +*/ + +(function() { + console.dump = function(object) { + if (window.JSON && window.JSON.stringify) + QDR.log.info(JSON.stringify(object,undefined,2)); + else + console.log(object); + }; +})(); + +var QDR = (function(QDR) { + 'use strict'; + + // The QDR service handles the connection to + // the server in the background + angular + .module('horizon.dashboard.dispatch') + .factory('horizon.dashboard.dispatch.comService', QDRService); + + QDRService.$inject = [ + '$rootScope', + '$http', + '$timeout', + '$location', + 'horizon.dashboard.dispatch.basePath', + ]; + + function QDRService($rootScope, $http, $timeout, $location, basePath) { + var self = { + + rhea: require("rhea"), + + timeout: 10, + connectActions: [], + disconnectActions: [], + updatedActions: {}, + stop: undefined, // update interval handle + + addConnectAction: function(action) { + if (angular.isFunction(action)) { + self.connectActions.push(action); + } + }, + addDisconnectAction: function(action) { + if (angular.isFunction(action)) { + self.disconnectActions.push(action); + } + }, + addUpdatedAction: function(key, action) { + if (angular.isFunction(action)) { + self.updatedActions[key] = action; + } + }, + delUpdatedAction: function(key) { + if (key in self.updatedActions) + delete self.updatedActions[key]; + }, + + executeConnectActions: function() { + self.connectActions.forEach(function(action) { + //QDR.log.debug("executing connect action " + action); + try { + action.apply(); + } catch (e) { + // in case the page that registered the handler has been unloaded + } + }); + self.connectActions = []; + + }, + executeDisconnectActions: function() { + self.disconnectActions.forEach(function(action) { + try { + action.apply(); + } catch (e) { + // in case the page that registered the handler has been unloaded + } + }); + self.disconnectActions = []; + }, + executeUpdatedActions: function() { + for (var action in self.updatedActions) { + try { + self.updatedActions[action].apply(); + } catch (e) { + delete self.updatedActions[action] + } + } + }, + redirectWhenConnected: function (org) { + //$location.path(basePath + "/connect") + //$location.search('org', org); + window.location.replace("/connect/"); + }, + + notifyTopologyDone: function() { + //QDR.log.debug("got Toplogy done notice"); + + if (!angular.isDefined(self.schema)) + return; + else if (self.topology._gettingTopo) + return; + if (!self.gotTopology) { + QDR.log.debug("topology was just initialized"); + self.gotTopology = true; + self.executeConnectActions(); + $rootScope.$apply(); + } else { + //QDR.log.debug("topology model was just updated"); + self.executeUpdatedActions(); + } + + }, + /** + * @property options + * Holds a reference to the connection options when + * a connection is started + */ + options: undefined, + + /* + * @property message + * The proton message that is used to send commands + * and receive responses + */ + sender: undefined, + receiver: undefined, + sendable: false, + + schema: undefined, + + toAddress: undefined, + connected: false, + gotTopology: false, + errorText: undefined, + connectionError: undefined, + + isConnected: function() { + return self.connected; + }, + + correlator: { + _objects: {}, + _corremationID: 0, + + corr: function () { + var id = ++this._corremationID + ""; + this._objects[id] = {resolver: null} + return id; + }, + request: function() { + //QDR.log.debug("correlator:request"); + return this; + }, + then: function(id, resolver, error) { + //QDR.log.debug("registered then resolver for correlationID: " + id); + if (error) { + delete this._objects[id]; + return; + } + this._objects[id].resolver = resolver; + }, + // called by receiver's on('message') handler when a response arrives + resolve: function(context) { + var correlationID = context.message.properties.correlation_id; + this._objects[correlationID].resolver(context.message.body, context); + delete this._objects[correlationID]; + } + }, + + onSubscription: function() { + self.getSchema(); + }, + + startUpdating: function () { + self.stopUpdating(); + QDR.log.info("startUpdating called") + self.topology.get(); + self.stop = setInterval(function() { + self.topology.get(); + }, 2000); + }, + stopUpdating: function () { + if (angular.isDefined(self.stop)) { + QDR.log.info("stopUpdating called") + clearInterval(self.stop); + self.stop = undefined; + } + }, + + initProton: function() { + //self.loadConnectOptions() + }, + cleanUp: function() { + }, + error: function(line) { + if (line.num) { + QDR.log.debug("error - num: ", line.num, " message: ", line.message); + } else { + QDR.log.debug("error - message: ", line.message); + } + }, + disconnected: function(line) { + QDR.log.debug("Disconnected from QDR server"); + self.executeDisconnectActions(); + }, + + nameFromId: function (id) { + return id.split('/')[3]; + }, + + humanify: function (s) { + if (!s || s.length === 0) + return s; + var t = s.charAt(0).toUpperCase() + s.substr(1).replace(/[A-Z]/g, ' $&'); + return t.replace(".", " "); + }, + pretty: function(v) { + var formatComma = d3.format(","); + if (!isNaN(parseFloat(v)) && isFinite(v)) + return formatComma(v); + return v; + }, + + nodeNameList: function() { + var nl = []; + // if we are in the middel of updating the topology + // then use the last known node info + var ni = self.topology._nodeInfo; + if (self.topology._gettingTopo) + ni = self.topology._lastNodeInfo; + for (var id in ni) { + nl.push(self.nameFromId(id)); + } + return nl.sort(); + }, + + nodeIdList: function() { + var nl = []; + // if we are in the middel of updating the topology + // then use the last known node info + var ni = self.topology._nodeInfo; + if (self.topology._gettingTopo) + ni = self.topology._lastNodeInfo; + for (var id in ni) { + nl.push(id); + } + return nl.sort(); + }, + + nodeList: function () { + var nl = []; + var ni = self.topology._nodeInfo; + if (self.topology._gettingTopo) + ni = self.topology._lastNodeInfo; + for (var id in ni) { + nl.push({name: self.nameFromId(id), id: id}); + } + return nl; + }, + + // given an attribute name array, find the value at the same index in the values array + valFor: function (aAr, vAr, key) { + var idx = aAr.indexOf(key); + if ((idx > -1) && (idx < vAr.length)) { + return vAr[idx]; + } + return null; + }, + + isArtemis: function (d) { + return d.nodeType ==='on-demand' && !d.properties.product; + }, + + isQpid: function (d) { + return d.nodeType ==='on-demand' && (d.properties && d.properties.product === 'qpid-cpp'); + }, + + isAConsole: function (properties, connectionId, nodeType, key) { + return self.isConsole({properties: properties, connectionId: connectionId, nodeType: nodeType, key: key}) + }, + isConsole: function (d) { + // use connection properties if available + if (d && d['properties'] && d['properties']['console_identifier'] == 'Dispatch console') + return true; + return false; + }, + + flatten: function (attributes, result) { + var flat = {} + attributes.forEach( function (attr, i) { + if (result && result.length > i) + flat[attr] = result[i] + }) + return flat; + }, + isConsoleLink: function (link) { + // find the connection for this link + var conns = self.topology.nodeInfo()[link.nodeId]['.connection'] + var connIndex = conns.attributeNames.indexOf("identity") + var linkCons = conns.results.filter ( function (conn) { + return conn[connIndex] === link.connectionId; + }) + var conn = self.flatten(conns.attributeNames, linkCons[0]); + + return self.isConsole(conn) + }, + + quiesceLink: function (nodeId, name) { + function gotMethodResponse (nodeName, entity, response, context) { + var statusCode = context.message.application_properties.statusCode; + if (statusCode < 200 || statusCode >= 300) { + Core.notification('error', context.message.application_properties.statusDescription); + } + } + var attributes = {adminStatus: 'disabled', name: name}; + self.sendMethod(nodeId, "router.link", attributes, "UPDATE", undefined, gotMethodResponse) + }, + + connectionOptions: {address: '0.0.0.0', port: 5673}, + loadConnectOptions: function (callback) { + $http.get(basePath + 'connect.json'). + success(function(data, status, headers, config) { + //QDR.log.debug("got connect info from file") + //console.dump(data) + self.connectionOptions = data; + if (callback) + callback() + }). + error(function(data, status, headers, config) { + //QDR.log.debug("did not get connect info from file") + //console.dump(status) + if (callback) + callback() + }); + }, + + addr_text: function (addr) { + if (!addr) + return "-" + if (addr[0] == 'M') + return addr.substring(2) + else + return addr.substring(1) + }, + addr_class: function (addr) { + if (!addr) return "-" + if (addr[0] == 'M') return "mobile" + if (addr[0] == 'R') return "router" + if (addr[0] == 'A') return "area" + if (addr[0] == 'L') return "local" + if (addr[0] == 'C') return "link-incoming" + if (addr[0] == 'D') return "link-outgoing" + if (addr[0] == 'T') return "topo" + return "unknown: " + addr[0] + }, + identity_clean: function (identity) { + if (!identity) + return "-" + var pos = identity.indexOf('/') + if (pos >= 0) + return identity.substring(pos + 1) + return identity + }, + + /* + * send the management messages that build up the topology + * + * + */ + topology: { + _gettingTopo: false, + _nodeInfo: {}, + _lastNodeInfo: {}, + _expected: {}, + _timerHandle: null, + + nodeInfo: function () { + return this._gettingTopo ? this._lastNodeInfo : this._nodeInfo; + }, + + get: function () { + if (this._gettingTopo) + return; + if (!self.connected) { + QDR.log.debug("topology get failed because !self.connected") + return; + } + this._lastNodeInfo = angular.copy(this._nodeInfo); + this._gettingTopo = true; + + self.errorText = undefined; + this.cleanUp(this._nodeInfo); + this._nodeInfo = {}; + this._expected = {}; + + // get the list of nodes to query. + // once this completes, we will get the info for each node returned + self.getRemoteNodeInfo( function (response, context) { + //QDR.log.debug("got remote node list of "); + //console.dump(response); + if( Object.prototype.toString.call( response ) === '[object Array]' ) { + if (response.length === 0) { + // there is only one router, get its node id from the reeciiver + //"amqp:/_topo/0/Router.A/temp.aSO3+WGaoNUgGVx" + var address = context.receiver.remote.attach.source.address; + var addrParts = address.split('/') + addrParts.splice(addrParts.length-1, 1, '$management') + response = [addrParts.join('/')] + } + // we expect a response for each of these nodes + self.topology.wait(self.timeout); + for (var i=0; i<response.length; ++i) { + self.makeMgmtCalls(response[i]); + } + }; + }); + }, + + cleanUp: function (obj) { + //if (obj) + // delete obj; + }, + wait: function (timeout) { + this.timerHandle = setTimeout(this.timedOut, timeout * 1000); + }, + timedOut: function () { + // a node dropped out. this happens when the get-mgmt-nodex + // results contains more nodes than actually respond within + // the timeout. However, if the responses we get don't contain + // the missing node, assume we are done. + QDR.log.info("timed out waiting for management responses"); + // note: can't use 'this' in a timeout handler + self.topology.miniDump("state at timeout"); + // check if _nodeInfo is consistent + if (self.topology.isConsistent()) { + //TODO: notify controllers which node was dropped + // so they can keep an event log + self.topology.ondone(); + return; + } + self.topology.onerror(Error("Timed out waiting for management responses")); + }, + isConsistent: function () { + // see if the responses we have so far reference any nodes + // for which we don't have a response + var gotKeys = {}; + for (var id in this._nodeInfo) { + var onode = this._nodeInfo[id]; + var conn = onode['.connection']; + // get list of node names in the connection data + if (conn) { + var containerIndex = conn.attributeNames.indexOf('container'); + var connectionResults = conn.results; + if (containerIndex >= 0) + for (var j=0; j < connectionResults.length; ++j) { + // inter-router connection to a valid dispatch connection name + gotKeys[connectionResults[j][containerIndex]] = ""; // just add the key + } + } + } + // gotKeys now contains all the container names that we have received + // Are any of the keys that are still expected in the gotKeys list? + var keys = Object.keys(gotKeys); + for (var id in this._expected) { + var key = self.nameFromId(id); + if (key in keys) + return false; + } + return true; + }, + + addNodeInfo: function (id, entity, values) { + // save the results in the nodeInfo object + if (id) { + if (!(id in self.topology._nodeInfo)) { + self.topology._nodeInfo[id] = {}; + } + self.topology._nodeInfo[id][entity] = values; + } + + // remove the id / entity from _expected + if (id in self.topology._expected) { + var entities = self.topology._expected[id]; + var idx = entities.indexOf(entity); + if (idx > -1) { + entities.splice(idx, 1); + if (entities.length == 0) + delete self.topology._expected[id]; + } + } + // see if the expected obj is empty + if (Object.getOwnPropertyNames(self.topology._expected).length == 0) + self.topology.ondone(); + self.topology.cleanUp(values); + }, + expect: function (id, key) { + if (!key || !id) + return; + if (!(id in this._expected)) + this._expected[id] = []; + if (this._expected[id].indexOf(key) == -1) + this._expected[id].push(key); + }, + ondone: function () { + clearTimeout(this.timerHandle); + this._gettingTopo = false; + //this.miniDump(); + //this.dump(); + self.notifyTopologyDone(); + }, + dump: function (prefix) { + if (prefix) + QDR.log.info(prefix); + QDR.log.info("---"); + for (var key in this._nodeInfo) { + QDR.log.info(key); + console.dump(this._nodeInfo[key]); + QDR.log.info("---"); + } + QDR.log.debug("was still expecting:"); + console.dump(this._expected); + }, + miniDump: function (prefix) { + if (prefix) + QDR.log.info(prefix); + QDR.log.info("---"); + console.dump(Object.keys(this._nodeInfo)); + QDR.log.info("---"); + }, + onerror: function (err) { + this._gettingTopo = false; + QDR.log.debug("Err:" + err); + self.executeDisconnectActions(); + + } + + }, + + getRemoteNodeInfo: function (callback) { + //QDR.log.debug("getRemoteNodeInfo called"); + var ret; + // first get the list of remote node names + self.correlator.request( + ret = self.sendMgmtQuery('GET-MGMT-NODES') + ).then(ret.id, function(response, context) { + callback(response, context); + self.topology.cleanUp(response); + }, ret.error); + }, + + makeMgmtCalls: function (id) { + var keys = [".router", ".connection", ".container", ".router.node", ".listener", ".router.link"]; + $.each(keys, function (i, key) { + self.topology.expect(id, key); + self.getNodeInfo(id, key, [], self.topology.addNodeInfo); + }); + }, + + getNodeInfo: function (nodeName, entity, attrs, callback) { + //QDR.log.debug("getNodeInfo called with nodeName: " + nodeName + " and entity " + entity); + var ret; + self.correlator.request( + ret = self.sendQuery(nodeName, entity, attrs) + ).then(ret.id, function(response) { + callback(nodeName, entity, response); + //self.topology.addNodeInfo(nodeName, entity, response); + //self.topology.cleanUp(response); + }, ret.error); + }, + + getMultipleNodeInfo: function (nodeNames, entity, attrs, callback, selectedNodeId, aggregate) { + if (!angular.isDefined(aggregate)) + aggregate = true; + var responses = {}; + var gotNodesResult = function (nodeName, dotentity, response) { + responses[nodeName] = response; + if (Object.keys(responses).length == nodeNames.length) { + if (aggregate) + self.aggregateNodeInfo(nodeNames, entity, selectedNodeId, responses, callback); + else { + callback(nodeNames, entity, responses) + } + } + } + + nodeNames.forEach( function (id) { + self.getNodeInfo(id, '.'+entity, attrs, gotNodesResult); + }) + //TODO: implement a timeout in case not all requests complete + }, + + aggregateNodeInfo: function (nodeNames, entity, selectedNodeId, responses, callback) { + //QDR.log.debug("got all results for " + entity); + // aggregate the responses + var newResponse = {}; + var thisNode = responses[selectedNodeId]; + newResponse['attributeNames'] = thisNode.attributeNames; + newResponse['results'] = thisNode.results; + newResponse['aggregates'] = []; + for (var i=0; i<thisNode.results.length; ++i) { + var result = thisNode.results[i]; + var vals = []; + result.forEach( function (val) { + vals.push({sum: val, detail: []}) + }) + newResponse.aggregates.push(vals); + } + var nameIndex = thisNode.attributeNames.indexOf("name"); + var ent = self.schema.entityTypes[entity]; + var ids = Object.keys(responses); + ids.sort(); + ids.forEach( function (id) { + var response = responses[id]; + var results = response.results; + results.forEach( function (result) { + // find the matching result in the aggregates + var found = newResponse.aggregates.some( function (aggregate, j) { + if (aggregate[nameIndex].sum === result[nameIndex]) { + // result and aggregate are now the same record, add the graphable values + newResponse.attributeNames.forEach( function (key, i) { + if (ent.attributes[key] && ent.attributes[key].graph) { + if (id != selectedNodeId) + aggregate[i].sum += result[i]; + } + aggregate[i].detail.push({node: self.nameFromId(id)+':', val: result[i]}) + }) + return true; // stop looping + } + return false; // continute looking for the aggregate record + }) + if (!found) { + // this attribute was not found in the aggregates yet + // because it was not in the selectedNodeId's results + var vals = []; + result.forEach( function (val) { + vals.push({sum: val, detail: [{node: self.nameFromId(id), val: val}]}) + }) + newResponse.aggregates.push(vals) + } + }) + }) + callback(nodeNames, entity, newResponse); + }, + + + getSchema: function () { + //QDR.log.debug("getting schema"); + var ret; + self.correlator.request( + ret = self.sendMgmtQuery('GET-SCHEMA') + ).then(ret.id, function(response) { + //QDR.log.debug("Got schema response"); + // remove deprecated + for (var entityName in response.entityTypes) { + var entity = response.entityTypes[entityName] + if (entity.deprecated) { + // deprecated entity + delete response.entityTypes[entityName] + } else { + for (var attributeName in entity.attributes) { + var attribute = entity.attributes[attributeName] + if (attribute.deprecated) { + // deprecated attribute + delete response.entityTypes[entityName].attributes[attributeName] + } + } + } + } + self.schema = response; + self.topology.get(); + }, ret.error); + }, + + getNodeInfo: function (nodeName, entity, attrs, callback) { + //QDR.log.debug("getNodeInfo called with nodeName: " + nodeName + " and entity " + entity); + var ret; + self.correlator.request( + ret = self.sendQuery(nodeName, entity, attrs) + ).then(ret.id, function(response) { + callback(nodeName, entity, response); + //self.topology.addNodeInfo(nodeName, entity, response); + //self.topology.cleanUp(response); + }, ret.error); + }, + + sendMethod: function (nodeId, entity, attrs, operation, props, callback) { + var ret; + self.correlator.request( + ret = self._sendMethod(nodeId, entity, attrs, operation, props) + ).then(ret.id, function (response, context) { + callback(nodeId, entity, response, context); + }, ret.error); + }, + + _fullAddr: function (toAddr) { + var toAddrParts = toAddr.split('/'); + if (toAddrParts.shift() != "amqp:") { + self.topology.error(Error("unexpected format for router address: " + toAddr)); + return; + } + //var fullAddr = self.toAddress + "/" + toAddrParts.join('/'); + var fullAddr = toAddrParts.join('/'); + return fullAddr; + }, + + _sendMethod: function (toAddr, entity, attrs, operation, props) { + var fullAddr = self._fullAddr(toAddr); + var ret = {id: self.correlator.corr()}; + if (!self.sender || !self.sendable) { + ret.error = "no sender" + return ret; + } + try { + var application_properties = { + operation: operation + } + if (entity) { + var ent = self.schema.entityTypes[entity]; + var fullyQualifiedType = ent ? ent.fullyQualifiedType : entity; + application_properties.type = fullyQualifiedType || entity; + } + if (attrs.name) + application_properties.name = attrs.name; + if (props) { + jQuery.extend(application_properties, props); + } + var msg = { + body: attrs, + properties: { + to: fullAddr, + reply_to: self.receiver.remote.attach.source.address, + correlation_id: ret.id + }, + application_properties: application_properties + } + self.sender.send( msg ); + console.dump("------- method called -------") + console.dump (msg) + } + catch (e) { + error = "error sending: " + e; + QDR.log.error(error) + ret.error = error; + } + return ret; + }, + + sendQuery: function(toAddr, entity, attrs, operation) { + operation = operation || "QUERY" + var fullAddr = self._fullAddr(toAddr); + + var body; + if (attrs) + body = { + "attributeNames": attrs, + } + else + body = { + "attributeNames": [], + } + if (entity[0] === '.') + entity = entity.substr(1, entity.length-1) + var prefix = "org.apache.qpid.dispatch." + var configs = ["address", "autoLink", "linkRoute"] + if (configs.indexOf(entity) > -1) + prefix += "router.config." + return self._send(body, fullAddr, operation, prefix + entity); + }, + + sendMgmtQuery: function (operation) { + return self._send([], "/$management", operation); + }, + + _send: function (body, to, operation, entityType) { + var ret = {id: self.correlator.corr()}; + if (!self.sender || !self.sendable) { + ret.error = "no sender" + return ret; + } + try { + var application_properties = { + operation: operation, + type: "org.amqp.management", + name: "self" + }; + if (entityType) + application_properties.entityType = entityType; + + self.sender.send({ + body: body, + properties: { + to: to, + reply_to: self.receiver.remote.attach.source.address, + correlation_id: ret.id + }, + application_properties: application_properties + }) + } + catch (e) { + error = "error sending: " + e; + QDR.log.error(error) + ret.error = error; + } + return ret; + }, + + disconnect: function() { + self.connection.close(); + self.errorText = "Disconnected." + }, + + connect: function(overrideConnectOptions) { + QDR.log.debug("****** calling rhea.connect ********") + var options = self.connectionOptions; + if (overrideConnectOptions) + options = overrideConnectOptions; + self.topologyInitialized = false; + if (!self.connected) { + var okay = {connection: false, sender: false, receiver: false} + var port = options.port || 5673; + var baseAddress = options.address + ':' + port; + var ws = self.rhea.websocket_connect(WebSocket); + self.toAddress = "amqp://" + baseAddress; + self.connectionError = undefined; + + var stop = function (context) { + //self.stopUpdating(); + okay.sender = false; + okay.receiver = false; + okay.connected = false; + self.connected = false; + self.sender = null; + self.receiver = null; + self.sendable = false; + self.gotTopology = false; + } + var maybeStart = function () { + if (okay.connection && okay.sender && okay.receiver && self.sendable && !self.connected) { + QDR.log.info("okay to start") + self.connected = true; + self.connection = connection; + self.sender = sender; + self.receiver = receiver; + self.onSubscription(); + self.gotTopology = false; + } + } + var onDisconnect = function () { + //QDR.log.warn("Disconnected"); + self.connectionError = true; + stop(); + self.executeDisconnectActions(); + } + + var connection; + try { +QDR.log.debug("trying to connect to ws://" + baseAddress) + connection = self.rhea.connect({ + connection_details:ws('ws://' + baseAddress, ["binary", "base64", "AMQWSB10"]), + reconnect:true, + properties: {console_identifier: 'Dispatch console'} + }); + } + catch (e) { + QDR.log.debug("exception caught on connect") + self.errorText = "Connection failed" + onDisconnect(); + } + if (!self.connectionError) { + connection.on('connection_open', function (context) { + QDR.log.debug("connection_opened") + okay.connection = true; + okay.receiver = false; + okay.sender = false; + }) + connection.on('disconnected', function (context) { + QDR.log.debug("connection disconnected") + self.errorText = "Unable to connect" + onDisconnect(); + }) + connection.on('connection_close', function (context) { + QDR.log.debug("connection closed") + self.errorText = "Disconnected" + onDisconnect(); + }) + + var sender = connection.open_sender(); + sender.on('sender_open', function (context) { + QDR.log.debug("sender_opened") + okay.sender = true + maybeStart() + }) + sender.on('sendable', function (context) { + //QDR.log.debug("sendable") + self.sendable = true; + maybeStart(); + }) + + var receiver = connection.open_receiver({source: {dynamic: true}}); + receiver.on('receiver_open', function (context) { + QDR.log.debug("receiver_opened") + okay.receiver = true; + maybeStart() + }) + receiver.on("message", function (context) { + self.correlator.resolve(context); + }); + } + } + } + } + return self; + }; + + return QDR; +}(QDR || {})); http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/0c58c381/console/dispatch-dashboard/dispatch/static/dashboard/dispatch/dispatch.module.js ---------------------------------------------------------------------- diff --git a/console/dispatch-dashboard/dispatch/static/dashboard/dispatch/dispatch.module.js b/console/dispatch-dashboard/dispatch/static/dashboard/dispatch/dispatch.module.js new file mode 100644 index 0000000..48cc85f --- /dev/null +++ b/console/dispatch-dashboard/dispatch/static/dashboard/dispatch/dispatch.module.js @@ -0,0 +1,256 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you 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. +*/ +/** + * @module QDR + * @main QDR + * + * The main entry point for the QDR module + * + */ +var QDR = (function(QDR) { + + /** + * @property pluginName + * @type {string} + * + * The name of this plugin + */ + QDR.pluginName = "QDR"; + QDR.pluginRoot = ""; + QDR.isStandalone = true; + QDR.isHorizon = true; + QDR.offsetParent = ".col-xs-12"; + + /** + * @property log + * @type {Logging.Logger} + * + * This plugin's logger instance + */ + //HIO QDR.log = Logger.get(QDR.pluginName); + /** + * @property templatePath + * @type {string} + * + * The top level path to this plugin's partials + */ + QDR.srcBase = "plugin/"; + QDR.templatePath = QDR.srcBase + "html/"; + QDR.cssPath = QDR.srcBase + "css/"; + /** + * @property SETTINGS_KEY + * @type {string} + * + * The key used to fetch our settings from local storage + */ + QDR.SETTINGS_KEY = 'QDRSettings'; + QDR.LAST_LOCATION = "QDRLastLocation"; + + /** + * @property module + * @type {object} + * + * This plugin's angularjs module instance + */ + QDR.module = angular.module('horizon.dashboard.dispatch', + [ + 'ui.grid', + 'ui.grid.resizeColumns', + 'ui.grid.selection', + 'ui.bootstrap', + 'ui.slider', + 'horizon.dashboard.dispatch.overv', + 'horizon.dashboard.dispatch.topology' + ]) + + Core = { + notification: function (severity, msg) { + $.notify(msg, severity); + } + } + + QDR.module.config(['$provide', '$windowProvider', + function ($provide, $windowProvider) { + var path = $windowProvider.$get().STATIC_URL + 'dashboard/dispatch/'; + $provide.constant('horizon.dashboard.dispatch.basePath', path); + } + ]); + + QDR.module.config(['$compileProvider', + function($compileProvider) { + $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|ftp|mailto|file|blob):/); + } + ]); + + QDR.module.filter('to_trusted', ['$sce', function($sce){ + return function(text) { + return $sce.trustAsHtml(text); + }; + }]); + + QDR.module.filter('humanify', ['horizon.dashboard.dispatch.comService', function (QDRService) { + return function (input) { + return QDRService.humanify(input); + }; + }]); + + QDR.module.filter('Pascalcase', function () { + return function (str) { + if (!str) + return ""; + return str.replace(/(\w)(\w*)/g, + function(g0,g1,g2){return g1.toUpperCase() + g2.toLowerCase();}); + } + }) + + QDR.module.filter('safePlural', function () { + return function (str) { + var es = ['x', 'ch', 'ss', 'sh'] + for (var i=0; i<es.length; ++i) { + if (str.endsWith(es[i])) + return str + 'es' + } + if (str.endsWith('y')) + return str.substr(0, str.length-2) + 'ies' + if (str.endsWith('s')) + return str; + return str + 's' + } + }) + + QDR.logger = function ($log) { + var log = $log; + + this.debug = function (msg) { msg = "QDR: " + msg; log.debug(msg)}; + this.error = function (msg) {msg = "QDR: " + msg; log.error(msg)} + this.info = function (msg) {msg = "QDR: " + msg; log.info(msg)} + this.warn = function (msg) {msg = "QDR: " + msg; log.warn(msg)} + + return this; + } + // one-time initialization happens in the run function + // of our module + QDR.module.run( + ["$rootScope", + '$route', + '$timeout', + "$location", + "$log", + "horizon.dashboard.dispatch.comService", + "horizon.dashboard.dispatch.chartService", + function ( + $rootScope, + $route, + $timeout, + $location, + $log, + QDRService, + QDRChartService) { + QDR.log = new QDR.logger($log); + QDR.log.info("*************creating Dispatch Console************"); + + var curPath = $location.path() + var org = curPath.substr(1) + if (org && org.length > 0 && org !== "connect") { + // $location.search('org', org) + } else { + // $location.search('org', null) + } + + QDRService.initProton(); + var settings = angular.fromJson(localStorage[QDR.SETTINGS_KEY]); + QDRService.addConnectAction(function() { + QDRChartService.init(); // initialize charting service after we are connected + }); + + if (settings && settings.autostart) { + QDRService.addDisconnectAction( function () { + $timeout(function () { + var lastLocation = localStorage[QDR.LAST_LOCATION] || "/overview"; + org = lastLocation.substr(1) + //$location.path("/connect"); + //$location.search('org', org) +debugger; + window.location.replace("/dispatch/connect/"); + }) + }) + QDRService.addConnectAction(function() { + var searchObject = $location.search(); + // the redirect will be handled by QDRService when connected + if (searchObject.org) { + return; + } + // there was no org= parameter, so redirect to last known location + $timeout(function () { + var lastLocation = localStorage[QDR.LAST_LOCATION] || "/overview"; + //$location.path(lastLocation); + }) + }); + QDRService.connect(settings); + } else { +QDR.log.debug("QDR.module run called with location of " + $location.path()); + $timeout(function () { + //$location.path('/connect') + //$location.search('org', org) +//debugger; +// window.location.replace("/dispatch/connect/"); + }) + } + + $rootScope.$on('$routeChangeSuccess', function() { + var path = $location.path(); + if (path !== "/connect") { + localStorage[QDR.LAST_LOCATION] = path; + } + }); + }]); + + QDR.module.controller ("QDR.Core", function ($scope, $rootScope) { + $scope.alerts = []; + $scope.closeAlert = function(index) { + $scope.alerts.splice(index, 1); + }; + $scope.$on('newAlert', function(event, data) { + $scope.alerts.push(data); + $scope.$apply(); + }); + $scope.$on("clearAlerts", function () { + $scope.alerts = []; + $scope.$apply(); + }) + + }) + + return QDR; +}(QDR || {})); + +var Folder = (function () { + function Folder(title) { + this.title = title; + this.children = []; + this.folder = true; + } + return Folder; +})(); +var Leaf = (function () { + function Leaf(title) { + this.title = title; + } + return Leaf; +})(); --------------------------------------------------------------------- To unsubscribe, e-mail: commits-unsubscr...@qpid.apache.org For additional commands, e-mail: commits-h...@qpid.apache.org