| /* |
| 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. |
| */ |
| |
| import { utils } from "../common/amqp/utilities.js"; |
| import * as d3 from "d3"; |
| |
| // highlight the paths between the selected node and the hovered node |
| function findNextHopNode(from, d, nodeInfo, selected_node, nodes) { |
| // d is the node that the mouse is over |
| // from is the selected_node .... |
| if (!from) return null; |
| |
| if (from === d) return selected_node; |
| |
| let sInfo = nodeInfo[from.key]; |
| |
| // find the hovered name in the selected name's .router.node results |
| if (!sInfo["router.node"]) return null; |
| let aAr = sInfo["router.node"].attributeNames; |
| let vAr = sInfo["router.node"].results; |
| for (let hIdx = 0; hIdx < vAr.length; ++hIdx) { |
| let addrT = utils.valFor(aAr, vAr[hIdx], "id"); |
| if (d.name && addrT === d.name) { |
| let next = utils.valFor(aAr, vAr[hIdx], "nextHop"); |
| return next === null ? nodes.nodeFor(addrT) : nodes.nodeFor(next); |
| } |
| } |
| return null; |
| } |
| export function nextHop(thisNode, d, nodes, links, nodeInfo, selected_node, cb) { |
| if (thisNode && thisNode !== d) { |
| let target = findNextHopNode(thisNode, d, nodeInfo, selected_node, nodes); |
| if (target) { |
| let hnode = nodes.nodeFor(thisNode.name); |
| let hlLink = links.linkFor(hnode, target); |
| if (hlLink) { |
| if (cb) { |
| cb(hlLink, hnode, target); |
| } |
| } else target = null; |
| } |
| nextHop(target, d, nodes, links, nodeInfo, selected_node, cb); |
| } |
| } |
| |
| let linkRateHistory = {}; |
| export function connectionPopupHTML(d, nodeInfo) { |
| if (!d) { |
| linkRateHistory = {}; |
| return; |
| } |
| // return all of onode's connections that connecto to right |
| let getConnsArray = function(onode, key, right) { |
| if (!onode) return []; |
| if (right.normals) { |
| // if we want connections between a router and a client[s] |
| let connIds = new Set(); |
| let connIndex = onode.connection.attributeNames.indexOf("identity"); |
| for (let n = 0; n < right.normals.length; n++) { |
| let normal = right.normals[n]; |
| if (normal.key === key) { |
| connIds.add(normal.connectionId); |
| } else if (normal.alsoConnectsTo) { |
| normal.alsoConnectsTo.forEach(function(ac2) { |
| if (ac2.key === key) connIds.add(ac2.connectionId); |
| }); |
| } |
| } |
| return onode.connection.results |
| .filter(function(result) { |
| return connIds.has(result[connIndex]); |
| }) |
| .map(function(c) { |
| return utils.flatten(onode.connection.attributeNames, c); |
| }); |
| } else { |
| // we want the connection between two routers |
| let container = utils.nameFromId(right.key); |
| let containerIndex = onode.connection.attributeNames.indexOf("container"); |
| let roleIndex = onode.connection.attributeNames.indexOf("role"); |
| return onode.connection.results |
| .filter(function(conn) { |
| return conn[containerIndex] === container && conn[roleIndex] === "inter-router"; |
| }) |
| .map(function(c) { |
| return utils.flatten(onode.connection.attributeNames, c); |
| }); |
| } |
| }; |
| // construct HTML to be used in a popup when the mouse is moved over a link. |
| // The HTML is sanitized elsewhere before it is displayed |
| let linksHTML = function(onode, conns) { |
| if (!onode) return ""; |
| const max_links = 10; |
| const fields = [ |
| "deliveryCount", |
| "undeliveredCount", |
| "unsettledCount", |
| "rejectedCount", |
| "releasedCount", |
| "modifiedCount" |
| ]; |
| // local function to determine if a link's connectionId is in any of the connections |
| let isLinkFor = function(connectionId, conns) { |
| for (let c = 0; c < conns.length; c++) { |
| if (conns[c].identity === connectionId) return true; |
| } |
| return false; |
| }; |
| let fnJoin = function(ar, sepfn) { |
| let out = ""; |
| out = ar[0]; |
| for (let i = 1; i < ar.length; i++) { |
| let sep = sepfn(ar[i], i === ar.length - 1); |
| out += sep[0] + sep[1]; |
| } |
| return out; |
| }; |
| // if the data for the line is from a client (small circle), we may have multiple connections |
| // loop through all links for this router and accumulate those belonging to the connection(s) |
| let nodeLinks = onode["router.link"]; |
| if (!nodeLinks) return ""; |
| let links = []; |
| let hasAddress = false; |
| for (let n = 0; n < nodeLinks.results.length; n++) { |
| let link = utils.flatten(nodeLinks.attributeNames, nodeLinks.results[n]); |
| let allZero = true; |
| if (link.linkType !== "router-control") { |
| if (isLinkFor(link.connectionId, conns)) { |
| if (link.owningAddr) hasAddress = true; |
| if (link.name) { |
| let rates = utils.rates(link, fields, linkRateHistory, link.name, 1); |
| // replace the raw value with the rate |
| for (let i = 0; i < fields.length; i++) { |
| if (rates[fields[i]] > 0) allZero = false; |
| link[fields[i]] = rates[fields[i]]; |
| } |
| } |
| if (!allZero) links.push(link); |
| } |
| } |
| } |
| // we may need to limit the number of links displayed, so sort descending by the sum of the field values |
| let sum = function(a) { |
| let s = 0; |
| for (let i = 0; i < fields.length; i++) { |
| s += a[fields[i]]; |
| } |
| return s; |
| }; |
| links.sort(function(a, b) { |
| let asum = sum(a); |
| let bsum = sum(b); |
| return asum < bsum ? 1 : asum > bsum ? -1 : 0; |
| }); |
| |
| let HTMLHeading = "<h5>Rates (per second) for links</h5>"; |
| let HTML = '<table class="popupTable">'; |
| // copy of fields since we may be prepending an address |
| let th = fields.slice(); |
| let td = fields; |
| th.unshift("dir"); |
| td.unshift("linkDir"); |
| th.push("priority"); |
| td.push("priority"); |
| // add an address field if any of the links had an owningAddress |
| if (hasAddress) { |
| th.unshift("address"); |
| td.unshift("owningAddr"); |
| } |
| |
| let rate_th = function(th) { |
| let rth = th.map(function(t) { |
| if (t.endsWith("Count")) t = t.replace("Count", "Rate"); |
| return utils.humanify(t); |
| }); |
| return rth; |
| }; |
| HTML += '<tr class="header"><td>' + rate_th(th).join("</td><td>") + "</td></tr>"; |
| // add rows to the table for each link |
| for (let l = 0; l < links.length; l++) { |
| if (l >= max_links) { |
| HTMLHeading = `<h5>Rates (per second) for top ${max_links} links</h5>`; |
| break; |
| } |
| let link = links[l]; |
| let vals = td.map(function(f) { |
| if (f === "owningAddr") { |
| let identity = utils.identity_clean(link.owningAddr); |
| return utils.addr_text(identity); |
| } |
| return link[f]; |
| }); |
| let joinedVals = fnJoin(vals, function(v1, last) { |
| return [ |
| `</td><td${isNaN(+v1) ? "" : ' align="right"'}>`, |
| last ? v1 : utils.pretty(v1 || "0", ",.2f") |
| ]; |
| }); |
| HTML += `<tr><td> ${joinedVals} </td></tr>`; |
| } |
| return links.length > 0 ? `${HTMLHeading}${HTML}</table>` : ""; |
| }; |
| |
| let left, right; |
| if (d.left) { |
| left = d.source; |
| right = d.target; |
| } else { |
| left = d.target; |
| right = d.source; |
| } |
| if (left.normals) { |
| // swap left and right |
| [left, right] = [right, left]; |
| } |
| // left is a router. right is either a router or a client[s] |
| let onode = nodeInfo[left.key]; |
| // find all the connections for left that go to right |
| let conns = getConnsArray(onode, left.key, right); |
| |
| let HTML = ""; |
| HTML += "<h5>Connection" + (conns.length > 1 ? "s" : "") + "</h5>"; |
| HTML += |
| '<table class="popupTable"><tr class="header"><td>Security</td><td>Authentication</td><td>Tenant</td><td>Host</td>'; |
| |
| for (let c = 0; c < Math.min(conns.length, 10); c++) { |
| HTML += "<tr><td>" + utils.connSecurity(conns[c]) + "</td>"; |
| HTML += "<td>" + utils.connAuth(conns[c]) + "</td>"; |
| HTML += "<td>" + (utils.connTenant(conns[c]) || "--") + "</td>"; |
| HTML += "<td>" + conns[c].host + "</td>"; |
| HTML += "</tr>"; |
| } |
| HTML += "</table>"; |
| HTML += linksHTML(onode, conns); |
| return HTML; |
| } |
| |
| export function getSizes(id) { |
| const gap = 5; |
| const sel = d3.select(CSS.escape(`#${id}`)); |
| if (!sel.empty()) { |
| const brect = sel.node().getBoundingClientRect(); |
| return { width: brect.width - gap, height: brect.height - gap }; |
| } |
| return { width: window.innerWidth - 200, height: window.innerHeight - 100 }; |
| } |
| |
| // The following 2 reconcile functions are needed due to the way |
| // d3 associates data with svg elements. |
| // If we were to create new nodes and links arrays, even if the |
| // new array elements had the same uid, the svg elements would not be |
| // associated with the new array elements. This would cause |
| // multiple, difficult to diagnose problems. |
| |
| // combine two arrays into the 1st array without creating a new array |
| export function reconcileArrays(existing, newArray) { |
| // remove from existing, any elements that are not in newArray |
| for (let i = existing.length - 1; i >= 0; --i) { |
| const uid = existing[i].uid(); |
| if (!newArray.some(n => n.uid() === uid)) { |
| existing.splice(i, 1); |
| } |
| } |
| // add to existing, any elements that are only in newArray |
| newArray.forEach(n => { |
| const uid = n.uid(); |
| if (!existing.some(e => e.uid() === uid)) { |
| existing.push(n); |
| } |
| }); |
| } |
| |
| // Links are 'special' in that each link contians a reference |
| // to the two nodes that it is linking. |
| // So we need to fix the new links' source and target |
| export function reconcileLinks(existingLinks, newLinks, existingNodes) { |
| // find links that are mirror images |
| newLinks.forEach(n => { |
| existingLinks.forEach(e => { |
| if ( |
| e.suid === n.tuid && |
| e.tuid === n.suid && |
| e.left === n.right && |
| e.right === n.left |
| ) { |
| e.suid = n.suid; |
| e.tuid = n.tuid; |
| e.left = n.left; |
| e.right = n.right; |
| e.uuid = n.uuid; |
| const tmp = e.source; |
| e.source = e.target; |
| e.target = tmp; |
| } |
| }); |
| }); |
| reconcileArrays(existingLinks, newLinks); |
| existingLinks.forEach(e => { |
| // in new links, the source and target will be a number |
| // instead of references to the node |
| if (typeof e.source === "number") { |
| e.source = existingNodes.findIndex(nn => nn.uid() === e.suid); |
| e.target = existingNodes.findIndex(nn => nn.uid() === e.tuid); |
| } |
| }); |
| } |
| |
| function getNearestRouter(node, nodes, links) { |
| let link = null; |
| while (node && node.nodeType !== "_topo" && link !== undefined) { |
| if (node.nodeType === "_edge") { |
| // we are at an edge, find the link between the edge and a router |
| const uid = node.uid(); |
| for (let l = 0; l < links.links.length; ++l) { |
| const lnk = links.links[l]; |
| if (lnk.suid === uid && lnk.target.nodeType === "_topo") { |
| link = lnk; |
| node = lnk.target; |
| break; |
| } else if (lnk.tuid === uid && lnk.source.nodeType === "_topo") { |
| link = lnk; |
| node = lnk.source; |
| break; |
| } |
| } |
| } else { |
| // we are at a client (or group) |
| let connected_node = nodes.find(node.routerId, {}, node.routerId); |
| link = links.linkFor(node, connected_node); |
| node = connected_node; |
| } |
| // highlight the link between the target_node and its router |
| if (link) { |
| node.highlighted = true; |
| link.highlighted = true; |
| d3.select(CSS.escape(`path[id='hitpath-${link.uid()}']`)).classed( |
| "highlighted", |
| true |
| ); |
| } |
| } |
| return node; |
| } |
| |
| // entry point when highlighting path between selected node and |
| // the node the mouse is over |
| export function nextHopHighlight(selected_node, d, nodes, links, nodeInfo) { |
| selected_node.highlighted = true; |
| d.highlighted = true; |
| |
| // if the selected node isn't a router, |
| // find the router to which it is connected |
| selected_node = getNearestRouter(selected_node, nodes, links); |
| |
| // this the hovered node isn't a router |
| // find the router to which it is connected |
| d = getNearestRouter(d, nodes, links); |
| |
| // selected_node and d are now routers |
| nextHop( |
| selected_node, |
| d, |
| nodes, |
| links, |
| nodeInfo, |
| selected_node, |
| (link, fnode, tnode) => { |
| link.highlighted = true; |
| d3.select(CSS.escape(`path[id='hitpath-${link.uid()}']`)).classed( |
| "highlighted", |
| true |
| ); |
| fnode.highlighted = true; |
| tnode.highlighted = true; |
| } |
| ); |
| let hnode = nodes.nodeFor(d.name); |
| hnode.highlighted = true; |
| } |