Repository: qpid-dispatch Updated Branches: refs/heads/config-read-write 06f5f1201 -> 2f5ec0d89
DISPATCH-834 Now with ability to deploy with sudo password passed in Project: http://git-wip-us.apache.org/repos/asf/qpid-dispatch/repo Commit: http://git-wip-us.apache.org/repos/asf/qpid-dispatch/commit/2f5ec0d8 Tree: http://git-wip-us.apache.org/repos/asf/qpid-dispatch/tree/2f5ec0d8 Diff: http://git-wip-us.apache.org/repos/asf/qpid-dispatch/diff/2f5ec0d8 Branch: refs/heads/config-read-write Commit: 2f5ec0d8948d35c09deb4bcc7a02cfeb0a8341c4 Parents: 06f5f12 Author: Ernest Allen <eal...@redhat.com> Authored: Fri Oct 13 14:52:35 2017 -0400 Committer: Ernest Allen <eal...@redhat.com> Committed: Fri Oct 13 14:52:35 2017 -0400 ---------------------------------------------------------------------- console/config/config.py | 34 +- console/config/css/dispatch.css | 1 - console/config/css/mock.css | 29 +- .../config/deployments/install_dispatch.yaml | 25 +- console/config/deployments/run_dispatch.yaml | 18 + console/config/html/qdrTopology.html | 54 +- console/config/js/qdrNewNode.js | 16 - console/config/js/qdrService.js | 49 +- console/config/js/qdrTopology.js | 672 ++++++++++++------- 9 files changed, 572 insertions(+), 326 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/2f5ec0d8/console/config/config.py ---------------------------------------------------------------------- diff --git a/console/config/config.py b/console/config/config.py index d17bc34..bae1240 100755 --- a/console/config/config.py +++ b/console/config/config.py @@ -32,13 +32,8 @@ import cStringIO import yaml import threading import subprocess - -import pdb from distutils.spawn import find_executable -def id_generator(size=6, chars=string.ascii_uppercase + string.digits): - return ''.join(random.choice(chars) for _ in range(size)) - get_class = lambda x: globals()[x] sectionKeys = {"log": "module", "sslProfile": "name", "connector": "port", "listener": "port"} @@ -101,6 +96,7 @@ class Manager(object): self.verbose = verbose self.topo_base = "topologies/" self.deploy_base = "deployments/" + self.deploy_file = self.deploy_base + "deploy.txt" self.state = None def operation(self, op, request): @@ -135,6 +131,8 @@ class Manager(object): def DEPLOY(self, request): nodes = request["nodes"] topology = request["topology"] + inventory_file = self.deploy_base + "inventory.yml" + ansible_become_pass = "ansible_become_pass" self.PUBLISH(request, deploy=True) @@ -145,7 +143,6 @@ class Manager(object): } hosts = inventory['deploy_routers']['hosts'] - #pdb.set_trace() for node in nodes: if node['cls'] == 'router': host = node['host'] @@ -154,37 +151,44 @@ class Manager(object): # if any of the nodes for this host has a console, set create_console for this host to true hosts[host]['create_console'] = (hosts[host]['create_console'] or self.has_console(node)) hosts[host]['nodes'].append(node['name']) + # pass in the password for eash host if provided + if request.get(ansible_become_pass + "_" + host): + hosts[host][ansible_become_pass] = request.get(ansible_become_pass + "_" + host) # local hosts need to be marked as such if host in ('0.0.0.0', 'localhost', '127.0.0.1'): hosts[host]['ansible_connection'] = 'local' - with open(self.deploy_base + 'inventory.yml', 'w') as n: + with open(inventory_file, 'w') as n: yaml.safe_dump(inventory, n, default_flow_style=False) - # start ansible-playbook in separate thread and callback when done + # start ansible-playbook in separate thread so we don't have to wait and can still get a callback when done def popenCallback(callback, args): def popen(callback, args): # send all output to deploy.txt so we can send it to the console in DEPLOY_STATUS - with open('deploy.txt', 'w') as fout: + with open(self.deploy_file, 'w') as fout: proc = subprocess.Popen(args, stdout=fout, stderr=fout) proc.wait() - callback() + callback(proc.returncode) return thread = threading.Thread(target=popen, args=(callback, args)) thread.start() - def ansible_done(): + def ansible_done(returncode): + os.remove(inventory_file) if self.verbose: - print "-------------- DEPLOYMENT DONE ----------------" - self.state = "DONE" + print "-------------- DEPLOYMENT DONE with return code", returncode, "------------" + if returncode: + self.state = returncode + else: + self.state = "DONE" self.state = "DEPLOYING" - popenCallback(ansible_done, ['ansible-playbook', self.deploy_base + 'install_dispatch.yaml', '-i', self.deploy_base + 'inventory.yml']) + popenCallback(ansible_done, ['ansible-playbook', self.deploy_base + 'install_dispatch.yaml', '-i', inventory_file]) return "deployment started" def DEPLOY_STATUS(self, request): - with open('deploy.txt', 'r') as fin: + with open(self.deploy_file, 'r') as fin: content = fin.readlines() # remove leading blank line http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/2f5ec0d8/console/config/css/dispatch.css ---------------------------------------------------------------------- diff --git a/console/config/css/dispatch.css b/console/config/css/dispatch.css index f018be1..d5b71d3 100644 --- a/console/config/css/dispatch.css +++ b/console/config/css/dispatch.css @@ -94,7 +94,6 @@ circle.node.reflexive { circle.node.selected { stroke: #6F6 !important; stroke-width: 2px; - fill: #e0e0ff !important; } circle.node.highlighted { stroke: #6F6; http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/2f5ec0d8/console/config/css/mock.css ---------------------------------------------------------------------- diff --git a/console/config/css/mock.css b/console/config/css/mock.css index 6ae1156..3faa3f5 100644 --- a/console/config/css/mock.css +++ b/console/config/css/mock.css @@ -135,4 +135,31 @@ pre.tail { max-height: 15em; min-height: 15em; overflow-y: scroll; -} \ No newline at end of file +} + +circle.node { + opacity: 0.5; +} +circle.node.inter-router.highlighted { + opacity: 1; +} +circle.node { + stroke-width: 0; +} + +.host_color, .host_name { + display: inline-block; +} + +.host_color { + width: 1em; + height: 1em; + border: 0; +} +.host_name { + font-weight: bold; +} + +.host_pass { + float: right; +} http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/2f5ec0d8/console/config/deployments/install_dispatch.yaml ---------------------------------------------------------------------- diff --git a/console/config/deployments/install_dispatch.yaml b/console/config/deployments/install_dispatch.yaml index 0ff4107..4c51057 100644 --- a/console/config/deployments/install_dispatch.yaml +++ b/console/config/deployments/install_dispatch.yaml @@ -18,13 +18,18 @@ file: path: /usr/local/share/qpid-dispatch state: directory - - name: install console - synchronize: - src: ../../stand-alone + + - name: archive the console + local_action: + module: archive + dest: standalone.zip + path: "{{playbook_dir}}/../../stand-alone" + format: zip + + - unarchive: + src: standalone.zip dest: /usr/local/share/qpid-dispatch - archive: no - recursive: yes - delete: yes + when: create_console - name: create directories @@ -44,9 +49,11 @@ with_items: '{{ nodes }}' - name: stop running routers - action: shell pkill -f qdrouterd + shell: "ps ax | grep qpid-dispatch/{{item}}.conf | grep -v 'grep' | awk -F ' ' '{print $1}' | xargs kill -9" + #action: shell pkill -f qdrouterd ignore_errors: True + with_items: '{{ nodes }}' - - name: start routers - shell: "sleep 1 ; qdrouterd --config /usr/local/etc/qpid-dispatch/{{ item }}.conf -d ; sleep 1" + - include_tasks: run_dispatch.yaml with_items: "{{ nodes }}" + http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/2f5ec0d8/console/config/deployments/run_dispatch.yaml ---------------------------------------------------------------------- diff --git a/console/config/deployments/run_dispatch.yaml b/console/config/deployments/run_dispatch.yaml new file mode 100644 index 0000000..6257ef0 --- /dev/null +++ b/console/config/deployments/run_dispatch.yaml @@ -0,0 +1,18 @@ + - name: 'start router for {{item}}.conf' + shell: "qdrouterd --config /usr/local/etc/qpid-dispatch/{{ item }}.conf -d" + ignore_errors: True + register: qdrouterd + + - name: 'force stop of {{item}}.conf' + shell: "ps ax | grep qpid-dispatch/{{item}}.conf | grep -v 'grep' | awk -F ' ' '{print $1}' | xargs kill -9" + ignore_errors: True + when: qdrouterd.rc == 1 + + - name: 'verify router for {{item}}.conf' + shell: "ps aux | grep qpid-dispatch/{{item}}.conf | grep -v 'grep'" + register: psresult + ignore_errors: True + + - name: 'retry {{item}}.conf' + shell: "qdrouterd --config /usr/local/etc/qpid-dispatch/{{ item }}.conf -d" + when: psresult.rc == 1 http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/2f5ec0d8/console/config/html/qdrTopology.html ---------------------------------------------------------------------- diff --git a/console/config/html/qdrTopology.html b/console/config/html/qdrTopology.html index 7b6f794..0788d4b 100644 --- a/console/config/html/qdrTopology.html +++ b/console/config/html/qdrTopology.html @@ -23,11 +23,13 @@ under the License. <button class="btn btn-primary" type="button" ng-click="Publish()" title="Save this topology">Publish</button> <button class="btn btn-primary" type="button" ng-click="Clear()" title="Remove all routers">Clear</button> <button class="btn btn-primary" type="button" ng-click="Deploy()" ng-disabled="!canDeploy()" ng-if="ansible" title="Deploy this topology">Deploy</button> + <button class="btn btn-primary pull-right" type="button" ng-click="doHelp()" title="Show help">?</button> <button class="btn btn-primary pull-right" type="button" ng-click="doSettings()" title="Show global settings">Settings</button> <button class="btn btn-primary pull-right" type="button" ng-click="showNewDlg()" title="Enter a new topology name">New Topology</button> <div class="selected-node pull-right"> <button class="btn btn-primary" type="button" ng-click="addAnotherNode(true)"><b class="plus caret"></b> Add new router</button> - <button id="action_button" class="btn btn-primary" type="button" ng-disabled="!selected_node" ng-click="showActions($event)" title="Show actions on selected router">Actions <b class="down caret"></b></button> + <!-- <button id="action_button" class="btn btn-primary" type="button" ng-disabled="!selected_node" ng-click="showActions($event)" title="Show actions on selected router">Actions <b class="down caret"></b></button> --> + <button id="multiple_action_button" class="btn btn-primary" type="button" ng-disabled="!anySelectedNodes()" ng-click="showMultiActions($event)" title="Show actions on selected routers">Actions for selected routers<b class="down caret"></b></button> </div> </div> <div id="topology"><!-- d3 toplogy here --></div> @@ -48,10 +50,10 @@ under the License. <div id="action_menu" class="contextMenu"> <ul> <li ng-click="editSection(selected_node, 'router')">Edit router info</li> - <li ng-click="setRouterHost(selected_node)">Set router host</li> + <li ng-click="setRouterHost()">Set router host</li> <li class="context-separator"></li> - <li ng-click="editSection(selected_node, 'log', 'new')">Add new log section</li> + <li ng-click="editSection(false, 'log', 'new')">Add new log section</li> <li ng-repeat="log in getSectionList(selected_node, 'log')" ng-click="editSection(selected_node, 'log', log)">Edit/Delete {{log}} log section</li> <li class="context-separator"></li> @@ -71,13 +73,24 @@ under the License. <!-- <li ng-repeat="listener in getSectionList(selected_node, 'listener')" ng-click="editSection(selected_node, 'listener', listener)">Edit/Delete listener on port {{listener}} </li> --> <li class="context-separator"></li> - <li ng-click="delNode(selected_node)">Delete this node</li> + <li ng-click="deleteNode(false)">Delete this node</li> <li ng-click="showConfig(selected_node)">Show generated config</li> </ul> </div> + <div id="multiple_action_menu" class="contextMenu"> + <ul> + <li ng-click="setRouterHost(true)">Set host for selected routers</li> + + <li class="context-separator"></li> + <li ng-click="editSection(true, 'log', 'new')">Add log section for selected routers</li> + + <li class="context-separator"></li> + <li ng-click="deleteNode(true)">Delete selected nodes</li> + </ul> + </div> <div id="client_context_menu" class="contextMenu"> <ul> - <li ng-click="delNode(selected_node)">Delete</li> + <li ng-click="deleteNode(false)">Delete</li> <li class="context-separator"></li> <li ng-click="editThisSection(selected_node)">Edit</li> </ul> @@ -227,14 +240,33 @@ under the License. <div class="modal-header"> <h3 class="modal-title">{{state}}</h3> </div> - <div class="modal-body"> - <pre id="deploy_status" class="tail">{{status}}</pre> - <div ng-hide="polling" id="message">Deployed. - <div ng-show="hasConsole()">Browse to <a ng-href="{{address}}" target="_blank">here</a> to manage the router.</div> + <div ng-hide="deploy_state == 'deploying'"> + <div class="modal-body"> + <div ng-repeat="host in hosts"> + <div class="host_color circle node" ng-class="host.color"></div> + <div class="host_name">{{host.name}}</div> + <button class="btn btn-basic host_pass" ng-click="show_pass(host)" ng-hide="showing_pass == host.name">Sudo Password</button> + <div class="host_pass" ng-show="showing_pass == host.name"><input ng-keyup="$event.keyCode == 13 ? show_pass() : null" focus-when="showing_pass == host.name" ng-model="host.pass" type="password" /> <button class="btn btn-basic" ng-click="show_pass()">OK</button></div> + <ul class="host_nodes"> + <li ng-repeat="node in host.nodes">{{node}}</li> + </ul> + </div> + </div> + <div class="modal-footer"> + <button class="btn btn-primary" type="button" ng-click="deploy()">Deploy</button> + <button ng-class="close_button_class" class="btn" type="button" ng-click="cancel()">Cancel</button> </div> </div> - <div class="modal-footer"> - <button class="btn btn-warning" type="button" ng-click="cancel()">Close</button> + <div ng-show="deploy_state == 'deploying'"> + <div class="modal-body"> + <pre id="deploy_output" class="tail">{{status}}</pre> + <div ng-show="done()" id="message">Deployed. + <div ng-show="hasConsole()">Browse to <a ng-href="{{address}}" target="_blank">here</a> to manage the router.</div> + </div> + </div> + <div class="modal-footer"> + <button ng-class="close_button_class" class="btn" type="button" ng-click="cancel()">Close</button> + </div> </div> </form> </script> http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/2f5ec0d8/console/config/js/qdrNewNode.js ---------------------------------------------------------------------- diff --git a/console/config/js/qdrNewNode.js b/console/config/js/qdrNewNode.js index e000727..c66b455 100644 --- a/console/config/js/qdrNewNode.js +++ b/console/config/js/qdrNewNode.js @@ -223,22 +223,6 @@ var QDR = (function(QDR) { } }) } - // add checkbox to apply this log module/enable to all routers - $scope.applyLog = {isChecked: false} - ediv.attributes.push( { - sort: 'last', - name: 'apply', - humanName: 'Apply to all routers', - description: 'Apply this to all routers', - type: 'checkbox', - rawtype: 'boolean', - input: 'checkbox', - selected: undefined, - 'default': false, - value: $scope.applyLog.isChecked, - required: false, - unique: false - }) } // sort ediv.attributes on name var allNames = ediv.attributes.map( function (attr) { http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/2f5ec0d8/console/config/js/qdrService.js ---------------------------------------------------------------------- diff --git a/console/config/js/qdrService.js b/console/config/js/qdrService.js index 149f94e..fc8f9eb 100644 --- a/console/config/js/qdrService.js +++ b/console/config/js/qdrService.js @@ -146,52 +146,6 @@ var QDR = (function(QDR) { }; })(); -function ngGridFlexibleHeightPlugin (opts) { - var self = this; - self.grid = null; - self.scope = null; - self.init = function (scope, grid, services) { - self.domUtilityService = services.DomUtilityService; - self.grid = grid; - self.scope = scope; - var recalcHeightForData = function () { setTimeout(innerRecalcForData, 1); }; - var innerRecalcForData = function () { - var gridId = self.grid.gridId; - var footerPanelSel = '.' + gridId + ' .ngFooterPanel'; - if (!self.grid.$topPanel || !self.grid.$canvas) - return; - var extraHeight = self.grid.$topPanel.height() + $(footerPanelSel).height(); - var naturalHeight = self.grid.$canvas.height() + 1; - if (opts != null) { - if (opts.minHeight != null && (naturalHeight + extraHeight) < opts.minHeight) { - naturalHeight = opts.minHeight - extraHeight - 2; - } - if (opts.maxHeight != null && (naturalHeight + extraHeight) > opts.maxHeight) { - naturalHeight = opts.maxHeight; - } - } - - var newViewportHeight = naturalHeight + 3; - if (!self.scope.baseViewportHeight || self.scope.baseViewportHeight !== newViewportHeight) { - self.grid.$viewport.css('height', newViewportHeight + 'px'); - self.grid.$root.css('height', (newViewportHeight + extraHeight) + 'px'); - self.scope.baseViewportHeight = newViewportHeight; - self.domUtilityService.RebuildGrid(self.scope, self.grid); - } - }; - self.scope.catHashKeys = function () { - var hash = '', - idx; - for (idx in self.scope.renderedRows) { - hash += self.scope.renderedRows[idx].$$hashKey; - } - return hash; - }; - self.scope.$watch('catHashKeys()', innerRecalcForData); - self.scope.$watch(self.grid.config.data, recalcHeightForData); - }; -} - if (!String.prototype.startsWith) { String.prototype.startsWith = function (searchString, position) { return this.substr(position || 0, searchString.length) === searchString @@ -324,4 +278,5 @@ if (!Array.prototype.findIndex) { return -1; } }); -} \ No newline at end of file +} + http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/2f5ec0d8/console/config/js/qdrTopology.js ---------------------------------------------------------------------- diff --git a/console/config/js/qdrTopology.js b/console/config/js/qdrTopology.js index 421191b..535bb52 100644 --- a/console/config/js/qdrTopology.js +++ b/console/config/js/qdrTopology.js @@ -31,6 +31,19 @@ var QDR = (function(QDR) { var settings = {baseName: "A", http_port: 5673, normal_port: 22000, internal_port: 2000, default_host: "0.0.0.0", artemis_port: 61616, qpid_port: 5672} var sections = ['log', 'connector', 'sslProfile', 'listener'] + // mouse event vars + var selected_link = null, + mousedown_link = null, + mousedown_node = null, + mouseover_node = null, + mouseup_node = null, + initial_mouse_down_position = null; + + $scope.selected_node = null + $scope.selected_nodes = {} + $scope.mockTopologies = [] + $scope.mockTopologyDir = "" + $scope.ansible = false $scope.Publish = function () { doOperation("PUBLISH", function (response) { @@ -38,13 +51,20 @@ var QDR = (function(QDR) { QDR.log.info("published " + $scope.mockTopologyDir) }) } - $scope.Deploy = function () { + var startDeploy = function (hosts) { + var extra = {} + for (var i=0; i<hosts.length; i++) { + if (hosts[i].pass !== '') + extra['ansible_become_pass_' + hosts[i].name] = hosts[i].pass + } // send the deploy command doOperation("DEPLOY", function (response) { QDR.log.info("deployment " + $scope.mockTopologyDir + " started") - }) - // show the deploy status dialog - doDeployDialog() + }, extra) + } + $scope.Deploy = function () { + // show the deploy dialog + doDeployDialog(startDeploy) } $scope.showConfig = function (node) { @@ -143,6 +163,7 @@ var QDR = (function(QDR) { $scope.Clear = function () { nodes = [] links = [] + $scope.selected_nodes = {} $scope.selected_node = null resetMouseVars() force.nodes(nodes).links(links).start(); @@ -151,7 +172,25 @@ var QDR = (function(QDR) { }) } - $scope.delNode = function (node, skipinit) { + $scope.deleteNode = function (multi) { + if (multi) { + var del = true + while (del) { + del = false + for (var i=0; i<nodes.length; i++) { + if (isSelectedNode(nodes[i])) { + delNode(nodes[i]) // this changes nodes. we can't continue looping + del = true + break; + } + } + } + } else { + delNode($scope.selected_node) + } + } + + var delNode = function (node, skipinit) { // loop through all the nodes if (node.nodeType !== 'inter-router') { var n = findParentNode(node) @@ -170,8 +209,9 @@ var QDR = (function(QDR) { } } for (var x=0; x<sub_nodes.length; x++) { - $scope.delNode(sub_nodes[x], true) + delNode(sub_nodes[x], true) } + removeSelectedNode(node) } // find the index of the node var index = nodes.findIndex( function (n) {return n.name === node.name}) @@ -192,6 +232,8 @@ var QDR = (function(QDR) { l.uid = l.source.id + "." + l.target.id }) + $scope.selected_node = null + if (!skipinit) { animate = true initGraph() @@ -200,6 +242,14 @@ var QDR = (function(QDR) { } } + $scope.showMultiActions = function (e) { + $(document).click(); + e.stopPropagation() + var position = $('#multiple_action_button').position() + position.top += $('#multiple_action_button').height() + 8 + position['display'] = "block" + $('#multiple_action_menu').css(position) + } $scope.showActions = function (e) { $(document).click(); e.stopPropagation() @@ -269,15 +319,6 @@ var QDR = (function(QDR) { $scope.addingNode.step = 0; } - $scope.selected_node = null - // mouse event vars - var selected_link = null, - mousedown_link = null, - mousedown_node = null, - mouseover_node = null, - mouseup_node = null, - initial_mouse_down_position = null; - $scope.hasConsoleListener = function (node) { if (!node) { for (var i=0; i<nodes.length; i++) { @@ -315,7 +356,7 @@ var QDR = (function(QDR) { // find the actual console listener var n = findChildNode('listener', settings.http_port, node.name) if (n) - $scope.delNode(n) + delNode(n) } var yoffset = 1; // toggles between 1 and -1. used to position new nodes @@ -443,18 +484,22 @@ var QDR = (function(QDR) { } } // menu item of router to set host of all listeners - $scope.setRouterHost = function (node) { - doSetRouterHostDialog(node) + $scope.setRouterHost = function (multi) { + if (!$scope.selected_node) + $scope.selected_node = firstSelectedNode() + doSetRouterHostDialog($scope.selected_node, multi) } // menu item of router to edit one of its sub-entities - $scope.editSection = function (node, type, section) { - doEditDialog(node, type, section) + $scope.editSection = function (multi, type, section) { + if (!$scope.selected_node) + $scope.selected_node = firstSelectedNode() + doEditDialog($scope.selected_node, type, section, multi) } // menu item of sub-entity to edit itself $scope.editThisSection = function (node) { var n = findParentNode(node) if (n) - doEditDialog(n, node.entity, node.entityKey) + doEditDialog(n, node.entity, node.entityKey, false) } var mouseX, mouseY; @@ -473,6 +518,7 @@ var QDR = (function(QDR) { $(document).mousemove(); $(document).click(function(e) { $("#svg_context_menu").fadeOut(200); + $("#multiple_action_menu").fadeOut(200); $("#action_menu").fadeOut(200); $("#link_context_menu").fadeOut(200); $("#client_context_menu").fadeOut(200); @@ -482,6 +528,7 @@ var QDR = (function(QDR) { $('.hastip').empty(); d3.select("#multiple_details").style("display", "none") d3.select("#link_details").style("display", "none") + d3.select('#multiple_action_menu').style('display', 'none'); d3.select('#action_menu').style('display', 'none'); d3.select('#svg_context_menu').style('display', 'none'); d3.select('#link_context_menu').style('display', 'none'); @@ -492,7 +539,8 @@ var QDR = (function(QDR) { 'inter-router': 25, 'normal': 15, 'on-demand': 15, - 'route-container': 15 + 'route-container': 15, + 'host': 20 }; var radius = 25; var radiusNormal = 15; @@ -674,6 +722,21 @@ var QDR = (function(QDR) { .append("svg:path") .attr('d', 'M 10 -5 L 0 0 L 10 5 z'); + var hostColors = d3.scale.category10(); + var colors = [] + for (var i=0; i<10; i++) { + colors.push(hostColors(i)) + } + svg.append("svg:defs").selectAll('pattern') + .data(colors) + .enter().append("pattern") + .attr({ id:"host"+i, width:"8", height:"8", patternUnits:"userSpaceOnUse", patternTransform:"rotate(-45)"}) + .attr('id', function (d, i) {return 'host_'+i}) + .attr({width:"4", height:"8", patternUnits:"userSpaceOnUse", patternTransform:"rotate(-45)"}) + .append("rect") + .attr('fill', function (d) {return d}) + .attr({ width:"2", height:"8", transform:"translate(0,0)"}); + // handles to link and node element groups path = svg.append('svg:g').selectAll('path') circle = svg.append('svg:g').selectAll('g') @@ -719,7 +782,8 @@ var QDR = (function(QDR) { var initForceGraph = function() { mouseover_node = null; - $scope.selected_node = null; + $scope.selected_nodes = {}; + $scope.selected_node = null selected_link = null; initGraph() @@ -814,8 +878,8 @@ var QDR = (function(QDR) { targetPadding = d.right ? radius + 16 : radius; } else { r = radiusNormal - 18; - sourcePadding = d.left ? radiusNormal + 18 : radiusNormal; - targetPadding = d.right ? radiusNormal + 16 : radiusNormal; + sourcePadding = d.left ? radiusNormal + 18 : radiusNormal + 10; + targetPadding = d.right ? radiusNormal + 6 : radiusNormal; } var dtx = Math.max(targetPadding, Math.min(width - r, d.target.x)), dty = Math.max(targetPadding, Math.min(height - r, d.target.y)), @@ -875,13 +939,148 @@ var QDR = (function(QDR) { }) } - function clerAllHighlights() { - for (var i = 0; i < links.length; ++i) { - links[i]['highlighted'] = false; - } - for (var i=0; i<nodes.length; ++i) { - nodes[i]['highlighted'] = false; - } + var appendTitle = function(g) { + g.append("svg:title").text(function(d) { + if (QDRService.isConsole(d)) { + return 'Dispatch console' + } + if (d.properties.product == 'qpid-cpp') { + return 'Broker - Qpid' + } + if (QDRService.isArtemis(d)) { + return 'Broker - Artemis' + } + if (d.cls === 'log') { + return 'Log' + (d.entityKey ? (': ' + d.entityKey) : '') + } + if (d.cdir === 'in') + return 'Listener on port ' + d.entityKey + if (d.cdir === 'out') + return 'Connector to ' + d.host + ':' + d.entityKey + if (d.cdir === 'both') + return 'sslProfile' + if (d.cls === 'host') + return 'Host ' + d.name + return d.nodeType == 'normal' ? 'client' : (d.nodeType == 'route-container' ? 'broker' : 'Router ' + d.name) + }) + } + + var appendCircle = function(g) { + // add new circles and set their attr/class/behavior + return g.append('svg:circle') + .attr('class', 'node') + .attr('r', function(d) { + return radii[d.nodeType] + }) + .classed('normal', function(d) { + return d.nodeType == 'normal' || QDRService.isConsole(d) + }) + .classed('in', function(d) { + return d.cdir == 'in' + }) + .classed('out', function(d) { + return d.cdir == 'out' + }) + .classed('connector', function(d) { + return d.cls == 'connector' + }) + .classed('listener', function(d) { + return d.cls == 'listener' + }) + .classed('selected', function (d) { + return $scope.selected_node === d + }) + .classed('inout', function(d) { + return d.cdir == 'both' + }) + .classed('inter-router', function(d) { + return d.nodeType == 'inter-router' + }) + .classed('on-demand', function(d) { + return d.nodeType == 'route-container' + }) + .classed('log', function(d) { + return d.cls === 'log' + }) + .classed('console', function(d) { + return QDRService.isConsole(d) + }) + .classed('artemis', function(d) { + return QDRService.isArtemis(d) + }) + .classed('qpid-cpp', function(d) { + return QDRService.isQpid(d) + }) + .classed('route-container', function (d) { + return (!QDRService.isArtemis(d) && !QDRService.isQpid(d) && d.nodeType === 'route-container') + }) + .classed('client', function(d) { + return d.nodeType === 'normal' && !d.properties.console_identifier + }) + } + + var appendContent = function(g) { + // show node IDs + g.append('svg:text') + .attr('x', 0) + .attr('y', function(d) { + var y = 7; + if (QDRService.isArtemis(d)) + y = 8; + else if (QDRService.isQpid(d)) + y = 9; + else if (d.nodeType === 'inter-router') + y = 4; + return y; + }) + .attr('class', 'id') + .classed('log', function(d) { + return d.cls === 'log' + }) + .classed('console', function(d) { + return QDRService.isConsole(d) + }) + .classed('normal', function(d) { + return d.nodeType === 'normal' + }) + .classed('on-demand', function(d) { + return d.nodeType === 'on-demand' + }) + .classed('artemis', function(d) { + return QDRService.isArtemis(d) + }) + .classed('qpid-cpp', function(d) { + return QDRService.isQpid(d) + }) + .text(function(d) { + if (QDRService.isConsole(d)) { + return '\uf108'; // icon-desktop for this console + } else if (QDRService.isArtemis(d)) { + return '\ue900' + } else if (QDRService.isQpid(d)) { + return '\ue901'; + } else if (d.cls === 'log') { + return '\uf036'; + } else if (d.nodeType === 'route-container') { + return d.properties.product ? d.properties.product[0].toUpperCase() : 'S' + } else if (d.nodeType === 'normal' && d.cdir === "in") // listener + return '\uf2a0'; // phone top + else if (d.nodeType === 'normal' && d.cdir === "out") // connector + return '\uf2a0'; // phone top (will be rotated) + else if (d.nodeType === 'normal' && d.cdir === "both") // not used + return '\uf023'; // icon-laptop for clients + else if (d.nodeType === 'host') + return '' + + return d.name.length > 7 ? d.name.substr(0, 6) + '...' : d.name; + }) + // rotatie the listener icon 180 degrees to use as the connector icon + .attr("transform", function (d) { + var nAngle = 0 + if (d.nodeType === 'normal' && d.cdir === "out" && d.cls === 'connector') + nAngle = 180 + return "rotate("+nAngle+")" + }); } // takes the nodes and links array of objects and adds svg elements for everything that hasn't already @@ -971,31 +1170,7 @@ var QDR = (function(QDR) { // circle (node) group // nodes are known by id - circle = circle.data(nodes, function(d) { return d.name }); - - var appendTitle = function(g) { - g.append("svg:title").text(function(d) { - if (QDRService.isConsole(d)) { - return 'Dispatch console' - } - if (d.properties.product == 'qpid-cpp') { - return 'Broker - Qpid' - } - if (QDRService.isArtemis(d)) { - return 'Broker - Artemis' - } - if (d.cls === 'log') { - return 'Log' + (d.entityKey ? (': ' + d.entityKey) : '') - } - if (d.cdir === 'in') - return 'Listener on port ' + d.entityKey - if (d.cdir === 'out') - return 'Connector to ' + d.host + ':' + d.entityKey - if (d.cdir === 'both') - return 'sslProfile' - return d.nodeType == 'normal' ? 'client' : (d.nodeType == 'route-container' ? 'broker' : 'Router ' + d.name) - }) - } + circle = circle.data(nodes, function(d) { return d.name + d.host }); // update existing nodes visual states circle.selectAll('circle') @@ -1022,59 +1197,6 @@ var QDR = (function(QDR) { return (d.normals && d.normals.length > 1) }) - var appendCircle = function(g) { - // add new circles and set their attr/class/behavior - return g.append('svg:circle') - .attr('class', 'node') - .attr('r', function(d) { - return radii[d.nodeType] - }) - .classed('normal', function(d) { - return d.nodeType == 'normal' || QDRService.isConsole(d) - }) - .classed('in', function(d) { - return d.cdir == 'in' - }) - .classed('out', function(d) { - return d.cdir == 'out' - }) - .classed('connector', function(d) { - return d.cls == 'connector' - }) - .classed('listener', function(d) { - return d.cls == 'listener' - }) - .classed('selected', function (d) { - return $scope.selected_node === d - }) - .classed('inout', function(d) { - return d.cdir == 'both' - }) - .classed('inter-router', function(d) { - return d.nodeType == 'inter-router' - }) - .classed('on-demand', function(d) { - return d.nodeType == 'route-container' - }) - .classed('log', function(d) { - return d.cls === 'log' - }) - .classed('console', function(d) { - return QDRService.isConsole(d) - }) - .classed('artemis', function(d) { - return QDRService.isArtemis(d) - }) - .classed('qpid-cpp', function(d) { - return QDRService.isQpid(d) - }) - .classed('route-container', function (d) { - return (!QDRService.isArtemis(d) && !QDRService.isQpid(d) && d.nodeType === 'route-container') - }) - .classed('client', function(d) { - return d.nodeType === 'normal' && !d.properties.console_identifier - }) - } appendCircle(g) .on('mouseover', function(d) { // mouseover a circle if ($scope.addingNode.step > 0) { @@ -1087,16 +1209,10 @@ var QDR = (function(QDR) { // enlarge target node d3.select(this).attr('transform', 'scale(1.1)'); mousedown_node = null; - - if (!$scope.selected_node) { - return; - } - clerAllHighlights() }) .on('mouseout', function(d) { // mouse out for a circle // unenlarge target node d3.select(this).attr('transform', ''); - clerAllHighlights() mouseover_node = null; restart(); }) @@ -1125,6 +1241,9 @@ var QDR = (function(QDR) { Math.abs(cur_mouse[1] - initial_mouse_down_position[1]) > 4) { return } + if (d3.event.ctrlKey && d.cls === 'router') { + return + } // we want a link between the selected_node and this node if ($scope.selected_node && d !== $scope.selected_node) { if (d.nodeType !== 'inter-router') @@ -1159,7 +1278,6 @@ var QDR = (function(QDR) { if (d.nodeType !== 'normal' && d.nodeType !== 'on-demand') $scope.selected_node = mousedown_node; } - clerAllHighlights() mousedown_node = null; if (!$scope.$$phase) $scope.$apply() restart(false); @@ -1173,6 +1291,8 @@ var QDR = (function(QDR) { if (!$scope.$$phase) $scope.$apply() // we just changed a scope variable during an async event var rm = relativeMouse() var menu = d.nodeType === 'inter-router' ? 'action_menu' : 'client_context_menu' + if (isSelectedNode(d)) + menu = 'multiple_action_menu' d3.select('#'+menu) .style('left', rm.left + "px") .style('top', (rm.top - rm.offset.top) + "px") @@ -1181,6 +1301,11 @@ var QDR = (function(QDR) { .on("click", function(d) { // circle if (!mouseup_node) return; + if (d3.event.ctrlKey && d.cls === 'router') { + $timeout( function () { + toggleSelectedNode(d) + }) + } // clicked on a circle clearPopups(); clickPos = d3.mouse(this); @@ -1188,69 +1313,7 @@ var QDR = (function(QDR) { }) //.attr("transform", function (d) {return "scale(" + (d.nodeType === 'normal' ? .5 : 1) + ")"}) //.transition().duration(function (d) {return d.nodeType === 'normal' ? 3000 : 0}).ease("elastic").attr("transform", "scale(1)") - - var appendContent = function(g) { - // show node IDs - g.append('svg:text') - .attr('x', 0) - .attr('y', function(d) { - var y = 7; - if (QDRService.isArtemis(d)) - y = 8; - else if (QDRService.isQpid(d)) - y = 9; - else if (d.nodeType === 'inter-router') - y = 4; - return y; - }) - .attr('class', 'id') - .classed('log', function(d) { - return d.cls === 'log' - }) - .classed('console', function(d) { - return QDRService.isConsole(d) - }) - .classed('normal', function(d) { - return d.nodeType === 'normal' - }) - .classed('on-demand', function(d) { - return d.nodeType === 'on-demand' - }) - .classed('artemis', function(d) { - return QDRService.isArtemis(d) - }) - .classed('qpid-cpp', function(d) { - return QDRService.isQpid(d) - }) - .text(function(d) { - if (QDRService.isConsole(d)) { - return '\uf108'; // icon-desktop for this console - } else if (QDRService.isArtemis(d)) { - return '\ue900' - } else if (QDRService.isQpid(d)) { - return '\ue901'; - } else if (d.cls === 'log') { - return '\uf036'; - } else if (d.nodeType === 'route-container') { - return d.properties.product ? d.properties.product[0].toUpperCase() : 'S' - } else if (d.nodeType === 'normal' && d.cdir === "in") // listener - return '\uf2a0'; // phone top - else if (d.nodeType === 'normal' && d.cdir === "out") // connector - return '\uf2a0'; // phone top (will be rotated) - else if (d.nodeType === 'normal' && d.cdir === "both") // not used - return '\uf023'; // icon-laptop for clients - - return d.name.length > 7 ? d.name.substr(0, 6) + '...' : d.name; - }) - // rotatie the listener icon 180 degrees to use as the connector icon - .attr("transform", function (d) { - var nAngle = 0 - if (d.nodeType === 'normal' && d.cdir === "out" && d.cls === 'connector') - nAngle = 180 - return "rotate("+nAngle+")" - }); - } - + resetCircleHosts() appendContent(g) appendTitle(g); @@ -1267,15 +1330,22 @@ var QDR = (function(QDR) { // dynamically create the legend based on which node types are present // the legend - d3.select("#svg_legend svg").remove(); - lsvg = d3.select("#svg_legend") - .append('svg') - .attr('id', 'svglegend') - lsvg = lsvg.append('svg:g') - .attr('transform', 'translate(' + (radii['inter-router'] + 2) + ',' + (radii['inter-router'] + 2) + ')') - .selectAll('g'); + updateLegend() + + if (!mousedown_node || !$scope.selected_node) + return; + + if (!start) + return; + // set the graph in motion + force.start(); + + } + + function updateLegend() { + initLegend() var legendNodes = []; - legendNodes.push(aNode("Router", "", "inter-router", 'router', 0, 0, 0, 0, false, {})) + //legendNodes.push(aNode("Router", "", "inter-router", 'router', 0, 0, 0, 0, false, {})) if (!svg.selectAll('circle.console').empty()) { legendNodes.push(aNode("Console", "", "normal", 'console', 1, 0, 0, 0, false, { @@ -1310,16 +1380,24 @@ var QDR = (function(QDR) { legendNodes.push(aNode("Service", "", "route-container", 'service', 8, 0, 0, 0, false, {product: ' External Service'})) } + // add a circle for each unique host in nodes + var hosts = getHosts() + for (var i=0; i<hosts.length; i++) { + var host = hosts[i] + legendNodes.push({key: host, name: host, nodeType: 'host', id: getHostIndex(host), properties: {}, host: host, cls: 'host'}) + } + lsvg = lsvg.data(legendNodes, function(d) { return d.key; }); var lg = lsvg.enter().append('svg:g') .attr('transform', function(d, i) { // 45px between lines and add 10px space after 1st line - return "translate(0, " + (45 * i + (i > 0 ? 10 : 0)) + ")" + return "translate(0, " + (45 * i) + ")" }) appendCircle(lg) + resetCircleHosts() appendContent(lg) appendTitle(lg) lg.append('svg:text') @@ -1348,16 +1426,47 @@ var QDR = (function(QDR) { svgEl.style.width = (bb.x + bb.width) + 'px'; } - if (!mousedown_node || !$scope.selected_node) - return; + } - if (!start) - return; - // set the graph in motion - force.start(); + // Ensure a host name will always return the same number (for this session) + // This is to make sure the colors associated with host names don't change + // In other words, if you have a node with host==localhost and then delete it, all the + // other host colors won't change and if you re-add localhost it would get the original color + var knownHosts = {} + function initHosts() { + var hosts = getHosts() + var hostColors = d3.scale.category10(); + for (var i=0; i<hosts.length; i++) { + knownHosts[hosts[i]] = i + } - } + var style = document.createElement("style"); + style.appendChild(document.createTextNode("")); + document.head.appendChild(style); + for (var i=0; i<10; i++) { + //console.log("adding rule " + "circle.node.host"+i + ", fill: " + hostColors(i) + ";") + style.sheet.addRule("circle.node.host"+i, "fill: " + hostColors(i) + ";", 0); + style.sheet.addRule("circle.node.host"+i+".multi-selected", "fill: url(#host_"+i+")", 1); + style.sheet.addRule("div.node.host"+i, "background-color: " + hostColors(i) + ";", 2); + } + } + function getHosts() { + var hosts = {} + for (var i=0; i<nodes.length; i++) { + var node = nodes[i] + if (node.host && node.cls === 'router') + hosts[node.host] = 1 + } + return Object.keys(hosts) + } + initHosts() + function getHostIndex(h) { + if (h in knownHosts) + return knownHosts[h] + knownHosts[h] = Object.keys(knownHosts).length + return knownHosts[h] + } function mousedown() { // prevent I-bar on drag //d3.event.preventDefault(); @@ -1379,9 +1488,6 @@ var QDR = (function(QDR) { window.removeEventListener('resize', resize); }); - $scope.mockTopologies = [] - $scope.mockTopologyDir = "" - $scope.ansible = false QDRService.sendMethod("ANSIBLE-INSTALLED", {}, function (response) { $scope.ansible = (response !== "") }) @@ -1410,8 +1516,55 @@ var QDR = (function(QDR) { } }); } + // set the host# class for all inter-router circles + function resetCircleHosts() { + var sel = d3.selectAll('circle.node') + for (var i=0; i<10; i++) { + sel.classed('host'+i, function (d) { + return i === getHostIndex(d.host) && (d.cls === 'router' || d.cls === 'host') + }) + } + } + + function firstSelectedNode() { + return $scope.selected_nodes[Object.keys($scope.selected_nodes)[0]] + } + function isSelectedNode(d) { + return d.name in $scope.selected_nodes + } + $scope.anySelectedNodes = function() { + return (Object.keys($scope.selected_nodes).length > 0) + } + function updateSelectedNodes() { + d3.selectAll('circle.node.inter-router') + .classed('multi-selected', function (d) { + return d.name in $scope.selected_nodes + }) + .attr('fill', function (d) { + var hostIndex = getHostIndex(d.host) + return "url(#host_" + hostIndex+")" + }) + restart(); + } + function removeSelectedNode(d) { + if (d.name in $scope.selected_nodes) { + delete $scope.selected_nodes[d.name] + updateSelectedNodes() + } + } + function toggleSelectedNode(d) { + if (d.name in $scope.selected_nodes) + delete $scope.selected_nodes[d.name] + else + $scope.selected_nodes[d.name] = d + updateSelectedNodes() + } + function addSelectedNode(d) { + $scope.selected_nodes[d.name] = d + updateSelectedNodes() + } - function doSetRouterHostDialog(node) { + function doSetRouterHostDialog(node, multi) { var d = $uibModal.open({ dialogClass: "modal dlg-large", backdrop: true, @@ -1429,13 +1582,24 @@ var QDR = (function(QDR) { $timeout(function () { d.result.then(function(result) { if (result) { - node.host = result.host - // loop through all listeners and set the host - if (node.listeners) { - for (var listener in node.listeners) { - node.listeners[listener].host = result.host + function setAHost(n) { + n.host = result.host + // loop through all listeners and set the host + if (node.listeners) { + for (var listener in n.listeners) { + n.listeners[listener].host = result.host + } } } + if (multi) { + for (var i=0; i<nodes.length; i++) { + if (isSelectedNode(nodes[i])) + setAHost(nodes[i]) + } + } else + setAHost(node) + resetCircleHosts() + restart() } }); }) @@ -1468,7 +1632,7 @@ var QDR = (function(QDR) { }); }) }; - function doDeployDialog() { + function doDeployDialog(startFn) { var host = undefined var port = undefined for (var i=0; i<nodes.length; i++) { @@ -1499,6 +1663,25 @@ var QDR = (function(QDR) { }, http_port: function () { return port + }, + start_fn: function () { + return startFn + }, + hosts: function () { + var hosts = [] + var hostNames = getHosts() + for (var i=0; i<hostNames.length; i++) { + var hostName = hostNames[i] + var hostIndex = getHostIndex(hostName) + var hostNodes = [] + for (var j=0; j<nodes.length; j++) { + if (nodes[j].host === hostName && nodes[j].cls === 'router') { + hostNodes.push(nodes[j].name) + } + } + hosts.push({name: hostName, nodes: hostNodes, color: 'host'+hostIndex, pass: ''}) + } + return hosts } } }); @@ -1540,7 +1723,7 @@ var QDR = (function(QDR) { return undefined } - function doEditDialog(node, entity, context) { + function doEditDialog(node, entity, context, multi) { var entity2key = {router: 'name', log: 'module', sslProfile: 'name', connector: 'port', listener: 'port'} var d = $uibModal.open({ dialogClass: "modal dlg-large", @@ -1601,7 +1784,7 @@ var QDR = (function(QDR) { // find the 'normal' node that is associated with this entry var n = findChildNode(entity, context, node.name) if (n) - $scope.delNode(n) + delNode(n) } else { var rVals = valFromMapArray(result.entities, "actualName", entity) if (rVals) { @@ -1615,10 +1798,10 @@ var QDR = (function(QDR) { delete nodeObj[context] } if (entity === 'log') { - if (o.node.apply) { - // apply this log module/enable to all routers + if (multi) { + // apply this log module/enable to all selected routers for (var i=0; i<nodes.length; i++) { - if (nodes[i].nodeType === 'inter-router') { + if (isSelectedNode(nodes[i])) { var logs = nodes[i]['logs'] if (!logs) nodes[i]['logs'] = {} @@ -1713,8 +1896,7 @@ var QDR = (function(QDR) { {name: "baseName", humanName: "Starting router name", input: "input", type: "text", value: local_settings.baseName, required: true}, {name: "http", humanName: "Port for console listeners", input: "input", type: "text", value: local_settings.http_port, required: true}, {name: "normal_port", humanName: "Starting port for normal listeners/connectors", input: "input", type: "text", value: local_settings.normal_port, required: true}, - {name: "internal_port", humanName: "Starting port for inter-router listeners/connectors", input: "input", type: "text", value: local_settings.internal_port, required: true}, - {name: "default_host", humanName: "Default host for inter-router listeners/connectors", input: "input", type: "text", value: local_settings.default_host, required: true}, + {name: "internal_port", humanName: "Starting port for inter-router listeners/connectors", input: "input", type: "text", value: local_settings.internal_port, required: true} ]} $scope.setSettings = function () { @@ -1731,36 +1913,74 @@ var QDR = (function(QDR) { }) - QDR.module.controller("QDR.DeployDialogController", function($scope, $uibModalInstance, QDRService, $timeout, $sce, dir, http_host, http_port) { + QDR.module.directive('focusWhen', function($timeout) { + return { + restrict : 'A', + link : function($scope, $element, $attr) { + $scope.$watch($attr.focusWhen, function(val) { + $timeout(function() { + if (val) + $element[0].focus() + }); + }); + } + } + }) + + QDR.module.controller("QDR.DeployDialogController", function($scope, $uibModalInstance, QDRService, $timeout, $sce, dir, http_host, http_port, hosts, start_fn) { // setup polling to get deployment status $scope.polling = true - $scope.state = "Deploying" + $scope.state = "Deploying " + dir + $scope.deploy_state = "" + $scope.close_button_class = "btn-warning" + var success_state = "Deploy Completed" $scope.address = "" var pollTimer = null function doPoll() { QDRService.sendMethod("DEPLOY-STATUS", {config: dir}, function (response) { - if (response[1] === 'DONE') { + if (response[1] !== 'DEPLOYING') { $scope.polling = false - $scope.state = "Deploy Completed" - Core.notification('info', dir + " deployed"); - if (http_host && http_port) { - $scope.address = $sce.trustAsHtml("http://" + http_host + ":" + http_port + "/#!/topology") + if (response[1] === "DONE") { + $scope.state = success_state + $scope.close_button_class = "btn-success" + Core.notification('info', dir + " deployed"); + if (http_host && http_port) { + $scope.address = $sce.trustAsHtml("http://" + http_host + ":" + http_port + "/#!/topology") + } + } else { + $scope.state = "Deploy Failed" + $scope.close_button_class = "btn-danger" + Core.notification('error', dir + " deployment failed"); } } $timeout(function () { $scope.status = response[0] scrollToEnd() - if (response[1] === 'DONE') { - } if ($scope.polling) ( pollTimer = setTimeout(doPoll, 1000) ) }) }) } - pollTimer = setTimeout(doPoll, 1000) + $scope.deploy = function () { + $scope.deploy_state = "deploying" + start_fn(hosts) + doPoll() + } + $scope.done = function () { + return !$scope.polling && $scope.state === success_state + } $scope.hasConsole = function () { - return http_host && http_port + return http_host && http_port && $scope.state === success_state + } + + $scope.showing_pass = undefined + $scope.show_pass = function (host) { + $scope.showing_pass = (host) ? host.name : '' + } + $scope.hosts = hosts + $scope.pass = function () { + console.log('set password') } function scrollTopTween(scrollTop) { @@ -1770,9 +1990,9 @@ var QDR = (function(QDR) { } } var scrollToEnd = function () { - var scrollheight = d3.select("#deploy_status").property("scrollHeight"); + var scrollheight = d3.select("#deploy_output").property("scrollHeight"); - d3.select('#deploy_status') + d3.select('#deploy_output') .transition().duration(1000) .tween("uniquetweenname", scrollTopTween(scrollheight)); } --------------------------------------------------------------------- To unsubscribe, e-mail: commits-unsubscr...@qpid.apache.org For additional commands, e-mail: commits-h...@qpid.apache.org