|
@@ -35,6 +35,9 @@
|
|
|
var timer = null;
|
|
var timer = null;
|
|
|
var lastSnap = null;
|
|
var lastSnap = null;
|
|
|
var viewGroup = null; // null = overview, otherwise workshop label
|
|
var viewGroup = null; // null = overview, otherwise workshop label
|
|
|
|
|
+ var detailNet = null; // null/auto, 'SURVE' or 'OFFICE' inside a workshop
|
|
|
|
|
+ var pathParent = {}; // child -> parent chain for hover highlighting
|
|
|
|
|
+ var lockedPath = null; // switch id whose path is locked by click
|
|
|
|
|
|
|
|
function el(id) { return document.getElementById(id); }
|
|
function el(id) { return document.getElementById(id); }
|
|
|
function t(k, d) { return (T[k] != null) ? T[k] : (d != null ? d : k); }
|
|
function t(k, d) { return (T[k] != null) ? T[k] : (d != null ? d : k); }
|
|
@@ -73,6 +76,31 @@
|
|
|
return l1.join(' · ') + '\n' + l2.join(' · ');
|
|
return l1.join(' · ') + '\n' + l2.join(' · ');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ function detailLabel(n) {
|
|
|
|
|
+ var s = labelFor(n);
|
|
|
|
|
+ if (viewGroup) {
|
|
|
|
|
+ var p = viewGroup + ' · ';
|
|
|
|
|
+ if (s.indexOf(p) === 0) s = s.substring(p.length);
|
|
|
|
|
+ }
|
|
|
|
|
+ return s;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function rowLabel(n) {
|
|
|
|
|
+ var s = String((n && n.name) || (n && n.ip) || '');
|
|
|
|
|
+ s = s.replace(/^COBCO-/i, '');
|
|
|
|
|
+ if (viewGroup) {
|
|
|
|
|
+ try { s = s.replace(new RegExp('^' + viewGroup + '-', 'i'), ''); } catch (e) {}
|
|
|
|
|
+ }
|
|
|
|
|
+ return s;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function rootLabel(n) {
|
|
|
|
|
+ var s = rowLabel(n);
|
|
|
|
|
+ var m = /^(.+?)-(SURVE|OFFICE|OTHER)-(Dist|Access)-(.+)$/i.exec(s);
|
|
|
|
|
+ if (m) return m[2].toUpperCase() + ' · ' + m[1] + '\n' + m[3] + ' · ' + m[4];
|
|
|
|
|
+ return s;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
function expandLabel(tpl, m) {
|
|
function expandLabel(tpl, m) {
|
|
|
return String(tpl).replace(/\$(\d)/g, function (_, d) { return m && m[+d] != null ? m[+d] : ''; });
|
|
return String(tpl).replace(/\$(\d)/g, function (_, d) { return m && m[+d] != null ? m[+d] : ''; });
|
|
|
}
|
|
}
|
|
@@ -188,11 +216,88 @@
|
|
|
return Object.keys(pairs).map(function (k) { return pairs[k]; });
|
|
return Object.keys(pairs).map(function (k) { return pairs[k]; });
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /* ---------------- detail layout (one workshop, tidy tree) ---------------- */
|
|
|
|
|
|
|
+ /* ---------------- detail layout (one workshop, left-to-right tree) ---------------- */
|
|
|
|
|
+ function floorInfo(n) {
|
|
|
|
|
+ var toks = String((n && n.name) || '').split('-');
|
|
|
|
|
+ for (var i = 0; i < toks.length; i++) {
|
|
|
|
|
+ var tk = toks[i];
|
|
|
|
|
+ var bm = /^B(\d+)$/i.exec(tk);
|
|
|
|
|
+ if (bm) { var bn = parseInt(bm[1], 10); return {key: 'B' + bn, label: 'B' + bn, rank: -200 + bn}; }
|
|
|
|
|
+ if (/^GRD/i.test(tk)) return {key: 'GRD', label: 'GRD', rank: -100};
|
|
|
|
|
+ var fm = /^(\d+(?:\.\d+)?)F/i.exec(tk);
|
|
|
|
|
+ if (fm) {
|
|
|
|
|
+ var num = parseFloat(fm[1]);
|
|
|
|
|
+ var txt = (num % 1 ? num.toFixed(1) : String(num)) + 'F';
|
|
|
|
|
+ return {key: txt, label: txt, rank: num * 10};
|
|
|
|
|
+ }
|
|
|
|
|
+ var cm = /^(\d)([A-Z])(\w+)$/i.exec(tk);
|
|
|
|
|
+ if (cm) {
|
|
|
|
|
+ var n2 = parseInt(cm[1], 10);
|
|
|
|
|
+ return {key: n2 + 'F', label: n2 + 'F', rank: n2 * 10};
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return {key: 'NA', label: t('l2.floor_na', 'Other'), rank: -500};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function baseNetworkOf(n) {
|
|
|
|
|
+ var s = String((n && n.name) || '').toUpperCase();
|
|
|
|
|
+ if (s.indexOf('SURVE') >= 0) return 'SURVE';
|
|
|
|
|
+ if (s.indexOf('OFFICE') >= 0) return 'OFFICE';
|
|
|
|
|
+ var ip = String((n && n.ip) || '');
|
|
|
|
|
+ if (/^10\.169\./.test(ip)) return 'SURVE';
|
|
|
|
|
+ if (/^10\.160\./.test(ip)) return 'OFFICE';
|
|
|
|
|
+ return 'OTHER';
|
|
|
|
|
+ }
|
|
|
|
|
+ function networkOf(n) { return baseNetworkOf(n); }
|
|
|
|
|
+
|
|
|
|
|
+ function networkLabel(key) {
|
|
|
|
|
+ if (key === 'SURVE') return t('l2.net_surve', 'SURVE 监控网');
|
|
|
|
|
+ if (key === 'OFFICE') return t('l2.net_office', 'OFFICE 办公网');
|
|
|
|
|
+ return t('l2.net_other', '其他网络');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
function computeDetail(snap, label) {
|
|
function computeDetail(snap, label) {
|
|
|
var all = snap.nodes || [];
|
|
var all = snap.nodes || [];
|
|
|
- var byId = {}, nodes = [];
|
|
|
|
|
- all.forEach(function (n) { byId[n.id] = n; if (groupOf(n).label === label) nodes.push(n); });
|
|
|
|
|
|
|
+ var byId = {}, groupNodes = [];
|
|
|
|
|
+ all.forEach(function (n) { byId[n.id] = n; if (groupOf(n).label === label) groupNodes.push(n); });
|
|
|
|
|
+
|
|
|
|
|
+ // Split one workshop by physical network first: office and surveillance never mix.
|
|
|
|
|
+ var gids = {}, rawAdj = {};
|
|
|
|
|
+ groupNodes.forEach(function (n) { gids[n.id] = true; rawAdj[n.id] = []; });
|
|
|
|
|
+ (snap.edges || []).forEach(function (e) {
|
|
|
|
|
+ if (gids[e.source] && gids[e.target]) { rawAdj[e.source].push(e.target); rawAdj[e.target].push(e.source); }
|
|
|
|
|
+ });
|
|
|
|
|
+ var netMap = {};
|
|
|
|
|
+ groupNodes.forEach(function (n) { netMap[n.id] = baseNetworkOf(n); });
|
|
|
|
|
+ // Unlabelled access switches inherit the network of the distribution they hang from.
|
|
|
|
|
+ for (var pass = 0; pass < 4; pass++) {
|
|
|
|
|
+ groupNodes.forEach(function (n) {
|
|
|
|
|
+ if (netMap[n.id] !== 'OTHER') return;
|
|
|
|
|
+ var votes = {};
|
|
|
|
|
+ rawAdj[n.id].forEach(function (id) {
|
|
|
|
|
+ var k = netMap[id];
|
|
|
|
|
+ if (k && k !== 'OTHER') votes[k] = (votes[k] || 0) + 1;
|
|
|
|
|
+ });
|
|
|
|
|
+ var best = null, bestN = 0;
|
|
|
|
|
+ Object.keys(votes).forEach(function (k) { if (votes[k] > bestN) { best = k; bestN = votes[k]; } });
|
|
|
|
|
+ if (best) netMap[n.id] = best;
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ var counts = {}, nets = [];
|
|
|
|
|
+ groupNodes.forEach(function (n) {
|
|
|
|
|
+ var k = netMap[n.id];
|
|
|
|
|
+ if (!counts[k]) { counts[k] = 0; nets.push(k); }
|
|
|
|
|
+ counts[k]++;
|
|
|
|
|
+ });
|
|
|
|
|
+ nets.sort(function (a, b) {
|
|
|
|
|
+ if (a === 'SURVE') return -1;
|
|
|
|
|
+ if (b === 'SURVE') return 1;
|
|
|
|
|
+ if (a === 'OFFICE') return -1;
|
|
|
|
|
+ if (b === 'OFFICE') return 1;
|
|
|
|
|
+ return counts[b] - counts[a];
|
|
|
|
|
+ });
|
|
|
|
|
+ var activeNet = (detailNet && counts[detailNet]) ? detailNet : (nets.length ? nets[0] : 'OTHER');
|
|
|
|
|
+ var nodes = groupNodes.filter(function (n) { return netMap[n.id] === activeNet; });
|
|
|
var ids = {};
|
|
var ids = {};
|
|
|
nodes.forEach(function (n) { ids[n.id] = true; });
|
|
nodes.forEach(function (n) { ids[n.id] = true; });
|
|
|
var internal = [], external = [];
|
|
var internal = [], external = [];
|
|
@@ -202,100 +307,139 @@
|
|
|
else if (a || b) external.push(e);
|
|
else if (a || b) external.push(e);
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- var adj = {}, kids = {};
|
|
|
|
|
- nodes.forEach(function (n) { adj[n.id] = []; kids[n.id] = []; });
|
|
|
|
|
|
|
+ var adj = {};
|
|
|
|
|
+ nodes.forEach(function (n) { adj[n.id] = []; });
|
|
|
internal.forEach(function (e) { adj[e.source].push(e.target); adj[e.target].push(e.source); });
|
|
internal.forEach(function (e) { adj[e.source].push(e.target); adj[e.target].push(e.source); });
|
|
|
|
|
|
|
|
- function rankNode(n) {
|
|
|
|
|
- var tier = n.tier || '';
|
|
|
|
|
- if (/core/i.test(tier)) return 0;
|
|
|
|
|
- if (/dist/i.test(tier)) return 1;
|
|
|
|
|
- return 2;
|
|
|
|
|
|
|
+ // Distribution switches are the left-hand roots of the workshop tree.
|
|
|
|
|
+ var dists = nodes.filter(function (n) { return /dist/i.test(n.tier || '') && /dist/i.test(n.name || ''); })
|
|
|
|
|
+ .sort(function (a, b) { return (adj[b.id].length - adj[a.id].length) || String(a.name).localeCompare(String(b.name)); });
|
|
|
|
|
+ if (!dists.length) {
|
|
|
|
|
+ var cand = nodes.slice().sort(function (a, b) { return adj[b.id].length - adj[a.id].length; })[0];
|
|
|
|
|
+ if (cand) dists = [cand];
|
|
|
}
|
|
}
|
|
|
- var roots = nodes.slice().sort(function (a, b) {
|
|
|
|
|
- var ra = rankNode(a), rb = rankNode(b);
|
|
|
|
|
- if (ra !== rb) return ra - rb;
|
|
|
|
|
- return adj[b.id].length - adj[a.id].length;
|
|
|
|
|
- });
|
|
|
|
|
- var seen = {}, orderedRoots = [];
|
|
|
|
|
- roots.forEach(function (r) {
|
|
|
|
|
- if (seen[r.id]) return;
|
|
|
|
|
- orderedRoots.push(r);
|
|
|
|
|
- var queue = [r.id]; seen[r.id] = true;
|
|
|
|
|
- while (queue.length) {
|
|
|
|
|
- var cur = queue.shift();
|
|
|
|
|
- adj[cur].forEach(function (nb) {
|
|
|
|
|
- if (seen[nb]) return;
|
|
|
|
|
- seen[nb] = true;
|
|
|
|
|
- kids[cur].push(nb);
|
|
|
|
|
- queue.push(nb);
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // One physical root per network tab; peer/redundant distribution switches are listed below it.
|
|
|
|
|
+ if (dists.length > 1) dists = [dists[0]];
|
|
|
|
|
+
|
|
|
|
|
+ // Group all non-distribution switches into floor buckets (GRD lowest, then 1F/2F/3F...).
|
|
|
|
|
+ var floorMap = {};
|
|
|
|
|
+ nodes.forEach(function (n) {
|
|
|
|
|
+ if (dists.indexOf(n) >= 0) return;
|
|
|
|
|
+ if (/core/i.test(n.tier || '')) return;
|
|
|
|
|
+ var fi = floorInfo(n);
|
|
|
|
|
+ var f = floorMap[fi.key] || (floorMap[fi.key] = {key: fi.key, label: fi.label, rank: fi.rank, nodes: []});
|
|
|
|
|
+ f.nodes.push(n);
|
|
|
});
|
|
});
|
|
|
- nodes.forEach(function (n) { if (!seen[n.id]) { seen[n.id] = true; orderedRoots.push(n); } });
|
|
|
|
|
-
|
|
|
|
|
- // Compact blocks: each dist root gets a grid of access switches underneath it.
|
|
|
|
|
- var pos = {}, blockX = 0, maxBlockH = 0;
|
|
|
|
|
- orderedRoots.forEach(function (rootN) {
|
|
|
|
|
- var d1 = kids[rootN.id] || [];
|
|
|
|
|
- var cols = Math.max(1, Math.min(5, Math.ceil(Math.sqrt(Math.max(1, d1.length) * 1.2))));
|
|
|
|
|
- var blockW = cols * NODE_W + (cols - 1) * DGAP_X;
|
|
|
|
|
- pos[rootN.id] = {x: blockX + blockW / 2, y: 0};
|
|
|
|
|
- d1.forEach(function (c, i) {
|
|
|
|
|
- var col = i % cols, row = Math.floor(i / cols);
|
|
|
|
|
- pos[c] = {x: blockX + col * (NODE_W + DGAP_X) + NODE_W / 2, y: (row + 1) * (NODE_H + DGAP_Y)};
|
|
|
|
|
|
|
+ var floors = Object.keys(floorMap).map(function (k) { return floorMap[k]; });
|
|
|
|
|
+ floors.forEach(function (f) {
|
|
|
|
|
+ f.nodes.sort(function (a, b) {
|
|
|
|
|
+ var pa = parseName(a.name), pb = parseName(b.name);
|
|
|
|
|
+ var ca = (pa.cab || '') + (pa.u || ''), cb = (pb.cab || '') + (pb.u || '');
|
|
|
|
|
+ if (ca !== cb) return ca.localeCompare(cb);
|
|
|
|
|
+ return String(a.name).localeCompare(String(b.name));
|
|
|
});
|
|
});
|
|
|
- var rows = Math.ceil(d1.length / cols);
|
|
|
|
|
- var d2 = [];
|
|
|
|
|
- d1.forEach(function (c) {
|
|
|
|
|
- (kids[c] || []).forEach(function (k) { if (k !== rootN.id) d2.push(k); });
|
|
|
|
|
- });
|
|
|
|
|
- var y2 = (rows + 1) * (NODE_H + DGAP_Y) + 16;
|
|
|
|
|
- d2.forEach(function (id, i) {
|
|
|
|
|
- pos[id] = {x: blockX + (i % cols) * (NODE_W + DGAP_X) + NODE_W / 2, y: y2 + Math.floor(i / cols) * (NODE_H + DGAP_Y)};
|
|
|
|
|
|
|
+ });
|
|
|
|
|
+ // Highest floor at the top, GRD at the bottom.
|
|
|
|
|
+ floors.sort(function (a, b) { return b.rank - a.rank; });
|
|
|
|
|
+
|
|
|
|
|
+ var WORK_L = 300, FLOOR_X = 520, DIST_FRAME_W = 190, TOP_OFF = 34;
|
|
|
|
|
+ var ROW_W = 380, ROW_H = 24, ROW_GAP = 8, FLOOR_HEAD = 28, FLOOR_GAP = 12;
|
|
|
|
|
+ var floorW = ROW_W + PADX * 2 + 110;
|
|
|
|
|
+ floors.forEach(function (f) {
|
|
|
|
|
+ f.cols = 1;
|
|
|
|
|
+ f.rows = f.nodes.length;
|
|
|
|
|
+ f.innerW = ROW_W;
|
|
|
|
|
+ f.h = FLOOR_HEAD + f.rows * ROW_H + Math.max(0, f.rows - 1) * ROW_GAP + 10;
|
|
|
|
|
+ });
|
|
|
|
|
+ var workRight = FLOOR_X + floorW + 14;
|
|
|
|
|
+ var workW = workRight - WORK_L;
|
|
|
|
|
+ var pos = {}, top = 44 + TOP_OFF;
|
|
|
|
|
+ floors.forEach(function (f) {
|
|
|
|
|
+ f.frame = {x: FLOOR_X, y: top, w: floorW, h: f.h};
|
|
|
|
|
+ f.nodes.forEach(function (n, i) {
|
|
|
|
|
+ pos[n.id] = {
|
|
|
|
|
+ x: FLOOR_X + PADX + ROW_W / 2,
|
|
|
|
|
+ y: top + FLOOR_HEAD + i * (ROW_H + ROW_GAP) + ROW_H / 2
|
|
|
|
|
+ };
|
|
|
});
|
|
});
|
|
|
- maxBlockH = Math.max(maxBlockH, y2 + Math.ceil(d2.length / cols) * (NODE_H + DGAP_Y));
|
|
|
|
|
- blockX += blockW + 70;
|
|
|
|
|
|
|
+ top += f.h + FLOOR_GAP;
|
|
|
});
|
|
});
|
|
|
- var missing = [];
|
|
|
|
|
- nodes.forEach(function (n) { if (!pos[n.id]) missing.push(n.id); });
|
|
|
|
|
- missing.forEach(function (id, i) {
|
|
|
|
|
- pos[id] = {x: (i % 6) * (NODE_W + DGAP_X) + NODE_W / 2, y: maxBlockH + 70 + Math.floor(i / 6) * (NODE_H + DGAP_Y)};
|
|
|
|
|
|
|
+ var workH = Math.max(220, top + 6);
|
|
|
|
|
+ var distFrame = dists.length ? {x: WORK_L + 8, y: 44 + TOP_OFF, w: DIST_FRAME_W, h: workH - (44 + TOP_OFF) - 14} : null;
|
|
|
|
|
+ var distX = WORK_L + 8 + DIST_FRAME_W / 2;
|
|
|
|
|
+ if (dists.length) {
|
|
|
|
|
+ var childYs = floors.reduce(function (acc, f) {
|
|
|
|
|
+ return acc.concat(f.nodes.map(function (n) { return pos[n.id] ? pos[n.id].y : 0; }));
|
|
|
|
|
+ }, []);
|
|
|
|
|
+ var cy = childYs.length ? (Math.min.apply(null, childYs) + Math.max.apply(null, childYs)) / 2 : distFrame.y + distFrame.h / 2;
|
|
|
|
|
+ pos[dists[0].id] = {x: distX, y: cy};
|
|
|
|
|
+ }
|
|
|
|
|
+ var missing = nodes.filter(function (n) { return !pos[n.id]; });
|
|
|
|
|
+ missing.forEach(function (n, i) {
|
|
|
|
|
+ pos[n.id] = {x: distX, y: (distFrame ? distFrame.y + 30 : 80) + i * (NODE_H + 12)};
|
|
|
});
|
|
});
|
|
|
- var totalW = Math.max(NODE_W, blockX - 70);
|
|
|
|
|
|
|
|
|
|
|
|
+ // One ghost core per external core→distribution uplink, aligned left of its distribution switch.
|
|
|
var ghosts = [], ghostSeen = {};
|
|
var ghosts = [], ghostSeen = {};
|
|
|
external.forEach(function (e) {
|
|
external.forEach(function (e) {
|
|
|
var inside = ids[e.source] ? e.source : e.target;
|
|
var inside = ids[e.source] ? e.source : e.target;
|
|
|
var outside = ids[e.source] ? e.target : e.source;
|
|
var outside = ids[e.source] ? e.target : e.source;
|
|
|
- if (!ghostSeen[outside]) {
|
|
|
|
|
- ghostSeen[outside] = {id: outside, to: inside, node: byId[outside], speed: e.speed, state: e.state};
|
|
|
|
|
- ghosts.push(ghostSeen[outside]);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if (!pos[inside] || !byId[outside]) return;
|
|
|
|
|
+ // Only a real core uplink gets a ghost; other cross-group edges are not drawn as cores.
|
|
|
|
|
+ if (!/core/i.test(byId[outside].tier || '') && !/core/i.test(byId[outside].name || '')) return;
|
|
|
|
|
+ var key = outside + '|' + inside;
|
|
|
|
|
+ if (ghostSeen[key]) return;
|
|
|
|
|
+ var g = {id: 'gh_' + outside + '_' + inside, to: inside, node: byId[outside], speed: e.speed, state: e.state};
|
|
|
|
|
+ ghostSeen[key] = g;
|
|
|
|
|
+ ghosts.push(g);
|
|
|
});
|
|
});
|
|
|
var ghostAt = {};
|
|
var ghostAt = {};
|
|
|
ghosts.forEach(function (g) {
|
|
ghosts.forEach(function (g) {
|
|
|
- var base = pos[g.to] || {x: 80};
|
|
|
|
|
|
|
+ var base = pos[g.to];
|
|
|
var k = ghostAt[g.to] || 0;
|
|
var k = ghostAt[g.to] || 0;
|
|
|
ghostAt[g.to] = k + 1;
|
|
ghostAt[g.to] = k + 1;
|
|
|
- g.x = base.x + (k ? (k % 2 ? 1 : -1) * 95 * Math.ceil(k / 2) : 0);
|
|
|
|
|
- g.y = -86 - Math.floor(k / 2) * 48;
|
|
|
|
|
|
|
+ g.x = 92;
|
|
|
|
|
+ g.y = base.y + (k ? (k % 2 ? 1 : -1) * 46 * Math.ceil(k / 2) : 0);
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- var maxY = 0, minX = 0, maxX2 = totalW;
|
|
|
|
|
- Object.keys(pos).forEach(function (id) {
|
|
|
|
|
- maxY = Math.max(maxY, pos[id].y + NODE_H);
|
|
|
|
|
- minX = Math.min(minX, pos[id].x - NODE_W / 2);
|
|
|
|
|
- maxX2 = Math.max(maxX2, pos[id].x + NODE_W / 2);
|
|
|
|
|
|
|
+ // Root each tree at the distribution switch; remember every child's parent for path highlighting.
|
|
|
|
|
+ var parentOf = {}, ghostOfDist = {};
|
|
|
|
|
+ ghosts.forEach(function (g) { if (!ghostOfDist[g.to]) ghostOfDist[g.to] = g.id; });
|
|
|
|
|
+ var seen = {};
|
|
|
|
|
+ dists.forEach(function (d) {
|
|
|
|
|
+ if (seen[d.id]) return;
|
|
|
|
|
+ seen[d.id] = true;
|
|
|
|
|
+ parentOf[d.id] = ghostOfDist[d.id] || null;
|
|
|
|
|
+ var queue = [d.id];
|
|
|
|
|
+ while (queue.length) {
|
|
|
|
|
+ var cur = queue.shift();
|
|
|
|
|
+ adj[cur].forEach(function (nb) {
|
|
|
|
|
+ if (seen[nb]) return;
|
|
|
|
|
+ seen[nb] = true;
|
|
|
|
|
+ parentOf[nb] = cur;
|
|
|
|
|
+ queue.push(nb);
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
});
|
|
});
|
|
|
- ghosts.forEach(function (g) {
|
|
|
|
|
- minX = Math.min(minX, g.x - 95);
|
|
|
|
|
- maxX2 = Math.max(maxX2, g.x + 95);
|
|
|
|
|
|
|
+ nodes.forEach(function (n) { if (!seen[n.id]) seen[n.id] = true; });
|
|
|
|
|
+
|
|
|
|
|
+ var workshop = {x: WORK_L, y: 0, w: workW, h: workH, label: label + ' · ' + networkLabel(activeNet) + ' · ' + nodes.length + ' ' + t('l2.unit_sw', 'sw')};
|
|
|
|
|
+ var distFrameFull = distFrame ? {
|
|
|
|
|
+ x: distFrame.x, y: distFrame.y, w: distFrame.w, h: distFrame.h,
|
|
|
|
|
+ label: t('l2.dist', 'Distribution') + ' · ' + dists.length
|
|
|
|
|
+ } : null;
|
|
|
|
|
+ floors.forEach(function (f) {
|
|
|
|
|
+ f.frame.label = f.label + ' · ' + f.nodes.length + ' ' + t('l2.unit_sw', 'sw');
|
|
|
});
|
|
});
|
|
|
- var top = ghosts.length ? -168 : -120;
|
|
|
|
|
- var root = {x: minX - 38, y: top, w: (maxX2 - minX) + 76, h: maxY - top + 54};
|
|
|
|
|
|
|
+
|
|
|
|
|
+ var root = {x: -20, y: -48, w: workRight + 40, h: workH + 80};
|
|
|
root.cx = root.x + root.w / 2; root.cy = root.y + root.h / 2;
|
|
root.cx = root.x + root.w / 2; root.cy = root.y + root.h / 2;
|
|
|
- return {nodes: nodes, byId: byId, pos: pos, ghosts: ghosts, root: root, totalW: totalW, ids: ids, internal: internal};
|
|
|
|
|
|
|
+ return {
|
|
|
|
|
+ nodes: nodes, byId: byId, pos: pos, ghosts: ghosts, parentOf: parentOf,
|
|
|
|
|
+ dists: dists, floors: floors, frames: {workshop: workshop, dist: distFrameFull},
|
|
|
|
|
+ root: root, totalW: workRight, ids: ids, internal: internal,
|
|
|
|
|
+ nets: nets.map(function (k) { return {key: k, count: counts[k]}; }), activeNet: activeNet, totalNodes: groupNodes.length
|
|
|
|
|
+ };
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/* ---------------- speed colour ---------------- */
|
|
/* ---------------- speed colour ---------------- */
|
|
@@ -341,6 +485,22 @@
|
|
|
'font-size': 'data(fs)', 'font-weight': 800, 'color': d ? '#aebfd8' : '#526178',
|
|
'font-size': 'data(fs)', 'font-weight': 800, 'color': d ? '#aebfd8' : '#526178',
|
|
|
'text-margin-y': 8, 'padding': '0px', 'events': 'no', 'z-index-compare': 'manual', 'z-index': 2
|
|
'text-margin-y': 8, 'padding': '0px', 'events': 'no', 'z-index-compare': 'manual', 'z-index': 2
|
|
|
}},
|
|
}},
|
|
|
|
|
+ {selector: 'node[type="wsframe"]', style: {
|
|
|
|
|
+ 'background-opacity': d ? 0.025 : 0.018, 'background-color': d ? '#6f8fc4' : '#8fa6c4',
|
|
|
|
|
+ 'border-width': 2.2, 'border-style': 'solid', 'border-color': d ? '#4f6790' : '#9fb0c6',
|
|
|
|
|
+ 'label': 'data(label)', 'text-valign': 'top', 'text-halign': 'center', 'text-wrap': 'none',
|
|
|
|
|
+ 'font-size': 19, 'font-weight': 900, 'color': d ? '#d7e6ff' : '#2f4560',
|
|
|
|
|
+ 'text-margin-x': 0, 'text-margin-y': 8, 'padding': '0px', 'events': 'no',
|
|
|
|
|
+ 'z-index-compare': 'manual', 'z-index': 1
|
|
|
|
|
+ }},
|
|
|
|
|
+ {selector: 'node[type="floorframe"]', style: {
|
|
|
|
|
+ 'background-opacity': d ? 0.055 : 0.035, 'background-color': d ? '#5b7bb0' : '#8fa6c4',
|
|
|
|
|
+ 'border-width': 1.2, 'border-style': 'dashed', 'border-color': d ? '#3f5377' : '#b9c6d8',
|
|
|
|
|
+ 'label': 'data(label)', 'text-valign': 'top', 'text-halign': 'center', 'text-wrap': 'none',
|
|
|
|
|
+ 'font-size': 13, 'font-weight': 800, 'color': d ? '#a9c1e3' : '#425a78',
|
|
|
|
|
+ 'text-margin-x': 0, 'text-margin-y': 7, 'padding': '0px', 'events': 'no',
|
|
|
|
|
+ 'z-index-compare': 'manual', 'z-index': 2
|
|
|
|
|
+ }},
|
|
|
{selector: 'node[type="root"]', style: {
|
|
{selector: 'node[type="root"]', style: {
|
|
|
'background-opacity': d ? 0.02 : 0.012, 'background-color': d ? '#7aa0d8' : '#8ea6c2',
|
|
'background-opacity': d ? 0.02 : 0.012, 'background-color': d ? '#7aa0d8' : '#8ea6c2',
|
|
|
'border-width': 2.2, 'border-style': 'solid', 'border-color': d ? '#476492' : '#9fb0c6',
|
|
'border-width': 2.2, 'border-style': 'solid', 'border-color': d ? '#476492' : '#9fb0c6',
|
|
@@ -367,6 +527,9 @@
|
|
|
'font-size': 11, 'font-weight': 600, 'text-wrap': 'wrap', 'text-max-width': 176,
|
|
'font-size': 11, 'font-weight': 600, 'text-wrap': 'wrap', 'text-max-width': 176,
|
|
|
'z-index-compare': 'manual', 'z-index': 15
|
|
'z-index-compare': 'manual', 'z-index': 15
|
|
|
}},
|
|
}},
|
|
|
|
|
+ {selector: 'node[type="edgepoint"]', style: {
|
|
|
|
|
+ 'width': 1, 'height': 1, 'opacity': 0, 'events': 'no'
|
|
|
|
|
+ }},
|
|
|
{selector: 'node[type="term"]', style: {
|
|
{selector: 'node[type="term"]', style: {
|
|
|
'width': 12, 'height': 12, 'shape': 'ellipse', 'background-color': 'data(bg)',
|
|
'width': 12, 'height': 12, 'shape': 'ellipse', 'background-color': 'data(bg)',
|
|
|
'border-width': 1, 'border-color': d ? '#2a3648' : '#cfd8e3', 'label': '',
|
|
'border-width': 1, 'border-color': d ? '#2a3648' : '#cfd8e3', 'label': '',
|
|
@@ -407,7 +570,15 @@
|
|
|
{selector: 'node.lat-warn', style: {'border-color': '#F39C12'}},
|
|
{selector: 'node.lat-warn', style: {'border-color': '#F39C12'}},
|
|
|
{selector: 'node.lat-bad', style: {'border-color': '#E74C3C', 'border-width': 6}},
|
|
{selector: 'node.lat-bad', style: {'border-color': '#E74C3C', 'border-width': 6}},
|
|
|
{selector: 'node.hl', style: {'border-color': '#ff0', 'border-width': 7, 'overlay-color': '#ff0', 'overlay-opacity': 0.25, 'overlay-padding': 6}},
|
|
{selector: 'node.hl', style: {'border-color': '#ff0', 'border-width': 7, 'overlay-color': '#ff0', 'overlay-opacity': 0.25, 'overlay-padding': 6}},
|
|
|
- {selector: 'node[type="switch"]', style: {'background-color': function (e) { return e.data('bg'); }}}
|
|
|
|
|
|
|
+ {selector: 'node.pathhl', style: {'border-width': 5, 'border-color': '#FFD54A', 'overlay-color': '#FFD54A', 'overlay-opacity': 0.2, 'overlay-padding': 4}},
|
|
|
|
|
+ {selector: 'edge.pathhl', style: {'width': 5.5, 'line-color': '#FFD54A', 'opacity': 1}},
|
|
|
|
|
+ {selector: 'edge.detail', style: {'opacity': 0.72, 'curve-style': 'straight', 'source-endpoint': '0% 50%', 'target-endpoint': '0% 50%'}},
|
|
|
|
|
+ {selector: 'node[type="switch"]', style: {
|
|
|
|
|
+ 'background-color': function (e) { return e.data('bg'); },
|
|
|
|
|
+ 'text-max-width': 'data(tw)', 'text-wrap': 'none',
|
|
|
|
|
+ 'text-halign': 'center', 'font-size': 10.5
|
|
|
|
|
+ }},
|
|
|
|
|
+ {selector: 'node[type="switch"].root', style: {'text-wrap': 'wrap'}}
|
|
|
];
|
|
];
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -451,6 +622,7 @@
|
|
|
|
|
|
|
|
function renderOverview(snap) {
|
|
function renderOverview(snap) {
|
|
|
var O = computeOverview(snap), els = [], idx = {};
|
|
var O = computeOverview(snap), els = [], idx = {};
|
|
|
|
|
+ var nb = el('l2-net-tabs'); if (nb) { nb.style.display = 'none'; nb.innerHTML = ''; }
|
|
|
if (O.root) {
|
|
if (O.root) {
|
|
|
els.push({group: 'nodes', data: {id: '__root', type: 'root', label: CFG.root_label || 'COBCO', w: O.root.w, h: O.root.h},
|
|
els.push({group: 'nodes', data: {id: '__root', type: 'root', label: CFG.root_label || 'COBCO', w: O.root.w, h: O.root.h},
|
|
|
position: {x: O.root.cx, y: O.root.cy}, selectable: false, grabbable: false});
|
|
position: {x: O.root.cx, y: O.root.cy}, selectable: false, grabbable: false});
|
|
@@ -476,12 +648,33 @@
|
|
|
applyElements(els);
|
|
applyElements(els);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ function renderNetTabs(D) {
|
|
|
|
|
+ var box = el('l2-net-tabs');
|
|
|
|
|
+ if (!box) return;
|
|
|
|
|
+ box.innerHTML = '';
|
|
|
|
|
+ if (!D.nets || D.nets.length < 2) { box.style.display = 'none'; return; }
|
|
|
|
|
+ box.style.display = 'flex';
|
|
|
|
|
+ D.nets.forEach(function (n) {
|
|
|
|
|
+ var b = document.createElement('button');
|
|
|
|
|
+ b.type = 'button';
|
|
|
|
|
+ b.className = 'l2-net-tab' + (n.key === D.activeNet ? ' active' : '');
|
|
|
|
|
+ b.textContent = networkLabel(n.key) + ' · ' + n.count + ' ' + t('l2.unit_sw', 'sw');
|
|
|
|
|
+ b.addEventListener('click', function () { detailNet = n.key; render(lastSnap); });
|
|
|
|
|
+ box.appendChild(b);
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
function renderDetail(snap) {
|
|
function renderDetail(snap) {
|
|
|
var D = computeDetail(snap, viewGroup), els = [];
|
|
var D = computeDetail(snap, viewGroup), els = [];
|
|
|
- if (D.root) {
|
|
|
|
|
- els.push({group: 'nodes', data: {id: '__root', type: 'root', label: (CFG.root_label || 'COBCO') + ' / ' + viewGroup, w: D.root.w, h: D.root.h},
|
|
|
|
|
- position: {x: D.root.cx, y: D.root.cy}, selectable: false, grabbable: false});
|
|
|
|
|
|
|
+ renderNetTabs(D);
|
|
|
|
|
+ function frameEl(id, type, fr) {
|
|
|
|
|
+ els.push({group: 'nodes', data: {id: id, type: type, label: fr.label, w: fr.w, h: fr.h},
|
|
|
|
|
+ position: {x: fr.x + fr.w / 2, y: fr.y + fr.h / 2}, selectable: false, grabbable: false});
|
|
|
}
|
|
}
|
|
|
|
|
+ frameEl('__ws', 'wsframe', {x: D.frames.workshop.x, y: 36, w: D.frames.workshop.w, h: D.frames.workshop.h + 36, label: D.frames.workshop.label});
|
|
|
|
|
+ if (D.frames.dist) frameEl('__dist', 'floorframe', D.frames.dist);
|
|
|
|
|
+ D.floors.forEach(function (f, i) { frameEl('__floor_' + i, 'floorframe', f.frame); });
|
|
|
|
|
+
|
|
|
(D.ghosts || []).forEach(function (g) {
|
|
(D.ghosts || []).forEach(function (g) {
|
|
|
var nm = (g.node && g.node.name) || g.id;
|
|
var nm = (g.node && g.node.name) || g.id;
|
|
|
var ip = (g.node && g.node.ip) || '';
|
|
var ip = (g.node && g.node.ip) || '';
|
|
@@ -496,13 +689,16 @@
|
|
|
}});
|
|
}});
|
|
|
});
|
|
});
|
|
|
var switchIds = {};
|
|
var switchIds = {};
|
|
|
- D.nodes.forEach(function (n) {
|
|
|
|
|
|
|
+ D.nodes.forEach(function (n, ni) {
|
|
|
switchIds[n.id] = true;
|
|
switchIds[n.id] = true;
|
|
|
var p = D.pos[n.id] || {x: 0, y: 0};
|
|
var p = D.pos[n.id] || {x: 0, y: 0};
|
|
|
var tier = n.tier || 'access';
|
|
var tier = n.tier || 'access';
|
|
|
var tkey = (tier.indexOf('dist') === 0) ? 'dist' : (tier.indexOf('core') === 0 ? 'core' : 'access');
|
|
var tkey = (tier.indexOf('dist') === 0) ? 'dist' : (tier.indexOf('core') === 0 ? 'core' : 'access');
|
|
|
|
|
+ var isRoot = D.dists && D.dists.length && n.id === D.dists[0].id;
|
|
|
var cls = [];
|
|
var cls = [];
|
|
|
var bd = (dark ? TIER_BG_DARK[tkey] : TIER_BG[tkey]) || '#555';
|
|
var bd = (dark ? TIER_BG_DARK[tkey] : TIER_BG[tkey]) || '#555';
|
|
|
|
|
+ var bg = (dark ? TIER_BG_DARK[tkey] : TIER_BG[tkey]) || '#777';
|
|
|
|
|
+ if (!isRoot && ni % 2 === 1) bg = dark ? '#4f8a3a' : '#6ca75a';
|
|
|
if (n.alarms && n.alarms.length) {
|
|
if (n.alarms && n.alarms.length) {
|
|
|
cls.push('alarm');
|
|
cls.push('alarm');
|
|
|
bd = '#E45959';
|
|
bd = '#E45959';
|
|
@@ -511,21 +707,42 @@
|
|
|
if (+n.latency_ms > LATB) cls.push('lat-bad');
|
|
if (+n.latency_ms > LATB) cls.push('lat-bad');
|
|
|
else if (+n.latency_ms > LATW) cls.push('lat-warn');
|
|
else if (+n.latency_ms > LATW) cls.push('lat-warn');
|
|
|
}
|
|
}
|
|
|
|
|
+ if (isRoot) cls.push('root');
|
|
|
els.push({group: 'nodes', data: {
|
|
els.push({group: 'nodes', data: {
|
|
|
id: n.id, type: 'switch', name: n.name, ip: n.ip, tier: tier, depth: n.depth,
|
|
id: n.id, type: 'switch', name: n.name, ip: n.ip, tier: tier, depth: n.depth,
|
|
|
hostid: n.hostid, oper: n.oper, maxsev: n.maxsev, alarms: n.alarms || [],
|
|
hostid: n.hostid, oper: n.oper, maxsev: n.maxsev, alarms: n.alarms || [],
|
|
|
term_online: n.term_online, term_offline: n.term_offline, latency_ms: n.latency_ms,
|
|
term_online: n.term_online, term_offline: n.term_offline, latency_ms: n.latency_ms,
|
|
|
- label: labelFor(n), bg: (dark ? TIER_BG_DARK[tkey] : TIER_BG[tkey]) || '#777',
|
|
|
|
|
- bd: bd, w: NODE_W, h: NODE_H, group: viewGroup
|
|
|
|
|
|
|
+ label: isRoot ? rootLabel(n) : rowLabel(n), tw: isRoot ? 150 : 360,
|
|
|
|
|
+ bg: bg, bd: isRoot ? bd : '#25501e',
|
|
|
|
|
+ w: isRoot ? 170 : 380, h: isRoot ? 50 : 24, group: viewGroup
|
|
|
}, position: p, classes: cls.join(' ')});
|
|
}, position: p, classes: cls.join(' ')});
|
|
|
});
|
|
});
|
|
|
D.internal.forEach(function (e, i) {
|
|
D.internal.forEach(function (e, i) {
|
|
|
|
|
+ var root = D.dists && D.dists[0];
|
|
|
|
|
+ if (root && (e.source === root.id || e.target === root.id)) {
|
|
|
|
|
+ var childId = e.source === root.id ? e.target : e.source;
|
|
|
|
|
+ var pp = D.pos[root.id], cp = D.pos[childId];
|
|
|
|
|
+ if (pp && cp) {
|
|
|
|
|
+ var sp = 'sp_' + i, tp = 'tp_' + i;
|
|
|
|
|
+ // Exact left-edge midpoints, straight line. The parent label is
|
|
|
|
|
+ // allowed to cover the line where it exits the parent.
|
|
|
|
|
+ els.push({group: 'nodes', data: {id: sp, type: 'edgepoint'}, position: {x: pp.x - 86, y: pp.y}, selectable: false, grabbable: false});
|
|
|
|
|
+ els.push({group: 'nodes', data: {id: tp, type: 'edgepoint'}, position: {x: cp.x - 191, y: cp.y}, selectable: false, grabbable: false});
|
|
|
|
|
+ els.push({group: 'edges', data: {
|
|
|
|
|
+ id: 'e_' + i, type: 'detail', source: sp, target: tp, p: root.id, c: childId,
|
|
|
|
|
+ speed: e.speed, state: e.state, a_port: e.a_port, b_port: e.b_port,
|
|
|
|
|
+ color: speedColor(e.speed, e.state), ls: (e.state && e.state !== 'up') ? 'dashed' : 'solid',
|
|
|
|
|
+ w: Math.min(2.4, speedWidth(e.speed))
|
|
|
|
|
+ }, classes: 'detail'});
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
els.push({group: 'edges', data: {
|
|
els.push({group: 'edges', data: {
|
|
|
id: 'e_' + i, source: e.source, target: e.target, speed: e.speed, state: e.state,
|
|
id: 'e_' + i, source: e.source, target: e.target, speed: e.speed, state: e.state,
|
|
|
a_port: e.a_port, b_port: e.b_port,
|
|
a_port: e.a_port, b_port: e.b_port,
|
|
|
color: speedColor(e.speed, e.state), ls: (e.state && e.state !== 'up') ? 'dashed' : 'solid',
|
|
color: speedColor(e.speed, e.state), ls: (e.state && e.state !== 'up') ? 'dashed' : 'solid',
|
|
|
- w: speedWidth(e.speed)
|
|
|
|
|
- }});
|
|
|
|
|
|
|
+ w: Math.min(2.4, speedWidth(e.speed))
|
|
|
|
|
+ }, classes: 'detail'});
|
|
|
});
|
|
});
|
|
|
if (showTerm) {
|
|
if (showTerm) {
|
|
|
var bySw = {};
|
|
var bySw = {};
|
|
@@ -547,6 +764,8 @@
|
|
|
});
|
|
});
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
+ pathParent = D.parentOf || {};
|
|
|
|
|
+ lockedPath = null;
|
|
|
applyElements(els);
|
|
applyElements(els);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -576,7 +795,7 @@
|
|
|
h += '<div class="lg-row"><span class="lg-box" style="background:#E45959;border-radius:2px"></span>' + t('l2.alarms', 'Alarms') + '</div>';
|
|
h += '<div class="lg-row"><span class="lg-box" style="background:#E45959;border-radius:2px"></span>' + t('l2.alarms', 'Alarms') + '</div>';
|
|
|
h += '<div class="lg-row"><span class="lg-box" style="background:#2ECC71;border-radius:50%"></span>' + t('l2.term_on', 'Online') +
|
|
h += '<div class="lg-row"><span class="lg-box" style="background:#2ECC71;border-radius:50%"></span>' + t('l2.term_on', 'Online') +
|
|
|
'<span class="lg-box" style="background:#95a5a6;border-radius:50%;margin-left:8px"></span>' + t('l2.term_off', 'Offline') + '</div>';
|
|
'<span class="lg-box" style="background:#95a5a6;border-radius:50%;margin-left:8px"></span>' + t('l2.term_off', 'Offline') + '</div>';
|
|
|
- h += '<div class="lg-row" style="opacity:.72;margin-top:4px">' + t('l2.click_hint', 'Click a switch to highlight its links') + '</div>';
|
|
|
|
|
|
|
+ h += '<div class="lg-row" style="opacity:.72;margin-top:4px">' + t('l2.click_hint', 'Hover a switch to highlight its full upstream path') + '</div>';
|
|
|
el('l2-legend').innerHTML = h;
|
|
el('l2-legend').innerHTML = h;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -658,19 +877,51 @@
|
|
|
if (!tm) return;
|
|
if (!tm) return;
|
|
|
var g = groupOfNodeId(tm.sw_ip);
|
|
var g = groupOfNodeId(tm.sw_ip);
|
|
|
if (g) viewGroup = g;
|
|
if (g) viewGroup = g;
|
|
|
|
|
+ var swHit = (lastSnap.nodes || []).filter(function (n) { return n.id === tm.sw_ip; })[0];
|
|
|
|
|
+ if (swHit) detailNet = networkOf(swHit);
|
|
|
showTerm = true;
|
|
showTerm = true;
|
|
|
if (el('l2-showterm')) el('l2-showterm').checked = true;
|
|
if (el('l2-showterm')) el('l2-showterm').checked = true;
|
|
|
render(lastSnap);
|
|
render(lastSnap);
|
|
|
setTimeout(function () { focusNode(id); }, 90);
|
|
setTimeout(function () { focusNode(id); }, 90);
|
|
|
} else {
|
|
} else {
|
|
|
var g2 = groupOfNodeId(id);
|
|
var g2 = groupOfNodeId(id);
|
|
|
- if (g2 && viewGroup !== g2) { viewGroup = g2; render(lastSnap); }
|
|
|
|
|
|
|
+ if (g2 && viewGroup !== g2) { viewGroup = g2; detailNet = null; render(lastSnap); }
|
|
|
|
|
+ var hit2 = (lastSnap.nodes || []).filter(function (n) { return n.id === id; })[0];
|
|
|
|
|
+ if (hit2) detailNet = networkOf(hit2);
|
|
|
|
|
+ render(lastSnap);
|
|
|
setTimeout(function () { focusNode(id); }, 90);
|
|
setTimeout(function () { focusNode(id); }, 90);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ function clearPath() {
|
|
|
|
|
+ if (!cy) return;
|
|
|
|
|
+ cy.elements().removeClass('dim pathhl');
|
|
|
|
|
+ lockedPath = null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function highlightPath(nodeId) {
|
|
|
|
|
+ if (!cy) return;
|
|
|
|
|
+ var chain = [], id = nodeId, guard = 0;
|
|
|
|
|
+ while (id && guard++ < 40) {
|
|
|
|
|
+ chain.unshift(id);
|
|
|
|
|
+ id = pathParent[id] || null;
|
|
|
|
|
+ }
|
|
|
|
|
+ cy.elements('node[type="switch"], node[type="ghost"], edge').addClass('dim').removeClass('pathhl');
|
|
|
|
|
+ var nodes = cy.collection();
|
|
|
|
|
+ chain.forEach(function (cid) { var n = cy.getElementById(cid); if (n && n.length) nodes = nodes.union(n); });
|
|
|
|
|
+ nodes.removeClass('dim').addClass('pathhl');
|
|
|
|
|
+ var edges = cy.edges().filter(function (e) {
|
|
|
|
|
+ var p = e.data('p'), c = e.data('c');
|
|
|
|
|
+ if (p && c) return chain.indexOf(p) >= 0 && chain.indexOf(c) >= 0;
|
|
|
|
|
+ var s = e.data('source'), t = e.data('target');
|
|
|
|
|
+ return chain.indexOf(s) >= 0 && chain.indexOf(t) >= 0;
|
|
|
|
|
+ });
|
|
|
|
|
+ edges.removeClass('dim').addClass('pathhl');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
function bindCy() {
|
|
function bindCy() {
|
|
|
cy.on('mouseover', 'node[type="switch"]', function (e) {
|
|
cy.on('mouseover', 'node[type="switch"]', function (e) {
|
|
|
|
|
+ if (viewGroup) highlightPath(e.target.id());
|
|
|
var d = e.target.data();
|
|
var d = e.target.data();
|
|
|
var arr = d.alarms || [];
|
|
var arr = d.alarms || [];
|
|
|
var sev = (d.maxsev != null && d.maxsev >= 0) ? ('<span style="color:' + SEV_COLORS[d.maxsev] + '">● ' + SEV_NAMES[d.maxsev] + '</span>') : '—';
|
|
var sev = (d.maxsev != null && d.maxsev >= 0) ? ('<span style="color:' + SEV_COLORS[d.maxsev] + '">● ' + SEV_NAMES[d.maxsev] + '</span>') : '—';
|
|
@@ -683,7 +934,10 @@
|
|
|
if (arr.length) html += '<hr>' + arr.map(function (a) { return esc(a.name || a.description || ''); }).join('<br>');
|
|
if (arr.length) html += '<hr>' + arr.map(function (a) { return esc(a.name || a.description || ''); }).join('<br>');
|
|
|
showTip(e, html);
|
|
showTip(e, html);
|
|
|
});
|
|
});
|
|
|
- cy.on('mouseout', 'node[type="switch"]', hideTip);
|
|
|
|
|
|
|
+ cy.on('mouseout', 'node[type="switch"]', function () {
|
|
|
|
|
+ if (!lockedPath) clearPath();
|
|
|
|
|
+ hideTip();
|
|
|
|
|
+ });
|
|
|
cy.on('mouseover', 'node[type="tile"]', function (e) {
|
|
cy.on('mouseover', 'node[type="tile"]', function (e) {
|
|
|
var d = e.target.data();
|
|
var d = e.target.data();
|
|
|
var html = '<b>' + esc(d.group) + '</b><br>' + d.n + ' ' + t('l2.unit_sw', 'sw') + '<br>' +
|
|
var html = '<b>' + esc(d.group) + '</b><br>' + d.n + ' ' + t('l2.unit_sw', 'sw') + '<br>' +
|
|
@@ -694,7 +948,7 @@
|
|
|
cy.on('mouseout', 'node[type="tile"]', hideTip);
|
|
cy.on('mouseout', 'node[type="tile"]', hideTip);
|
|
|
cy.on('tap', 'node[type="tile"]', function (e) {
|
|
cy.on('tap', 'node[type="tile"]', function (e) {
|
|
|
var g = e.target.data('group');
|
|
var g = e.target.data('group');
|
|
|
- if (g) { viewGroup = g; render(lastSnap); }
|
|
|
|
|
|
|
+ if (g) { viewGroup = g; detailNet = null; render(lastSnap); }
|
|
|
});
|
|
});
|
|
|
cy.on('mouseover', 'edge:not([type="term"])', function (e) {
|
|
cy.on('mouseover', 'edge:not([type="term"])', function (e) {
|
|
|
var d = e.target.data();
|
|
var d = e.target.data();
|
|
@@ -703,16 +957,11 @@
|
|
|
});
|
|
});
|
|
|
cy.on('mouseout', 'edge', hideTip);
|
|
cy.on('mouseout', 'edge', hideTip);
|
|
|
cy.on('tap', 'node[type="switch"]', function (e) {
|
|
cy.on('tap', 'node[type="switch"]', function (e) {
|
|
|
- var n = e.target;
|
|
|
|
|
- cy.elements().removeClass('dim hl');
|
|
|
|
|
- cy.elements().not(n).addClass('dim');
|
|
|
|
|
- n.removeClass('dim');
|
|
|
|
|
- var links = n.connectedEdges();
|
|
|
|
|
- links.removeClass('dim').addClass('hl');
|
|
|
|
|
- links.connectedNodes().removeClass('dim');
|
|
|
|
|
|
|
+ lockedPath = e.target.id();
|
|
|
|
|
+ highlightPath(lockedPath);
|
|
|
});
|
|
});
|
|
|
cy.on('tap', function (e) {
|
|
cy.on('tap', function (e) {
|
|
|
- if (e.target === cy) cy.elements().removeClass('dim hl');
|
|
|
|
|
|
|
+ if (e.target === cy) clearPath();
|
|
|
});
|
|
});
|
|
|
cy.on('dbltap', 'node[type="switch"]', function (e) {
|
|
cy.on('dbltap', 'node[type="switch"]', function (e) {
|
|
|
var hid = e.target.data('hostid');
|
|
var hid = e.target.data('hostid');
|
|
@@ -772,7 +1021,7 @@
|
|
|
var back = el('l2-back');
|
|
var back = el('l2-back');
|
|
|
if (back) {
|
|
if (back) {
|
|
|
back.textContent = t('l2.back_overview', '← Overview');
|
|
back.textContent = t('l2.back_overview', '← Overview');
|
|
|
- back.addEventListener('click', function () { viewGroup = null; render(lastSnap); });
|
|
|
|
|
|
|
+ back.addEventListener('click', function () { viewGroup = null; detailNet = null; render(lastSnap); });
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
el('l2-dark').addEventListener('change', function () {
|
|
el('l2-dark').addEventListener('change', function () {
|
|
@@ -838,8 +1087,11 @@
|
|
|
function boot() {
|
|
function boot() {
|
|
|
setupControls();
|
|
setupControls();
|
|
|
try {
|
|
try {
|
|
|
- var qg = new URLSearchParams(window.location.search).get('g');
|
|
|
|
|
|
|
+ var qs = new URLSearchParams(window.location.search);
|
|
|
|
|
+ var qg = qs.get('g');
|
|
|
if (qg) viewGroup = qg;
|
|
if (qg) viewGroup = qg;
|
|
|
|
|
+ var qn = qs.get('net');
|
|
|
|
|
+ if (qn) detailNet = qn;
|
|
|
} catch (e) {}
|
|
} catch (e) {}
|
|
|
if (!SNAP || !SNAP.nodes) {
|
|
if (!SNAP || !SNAP.nodes) {
|
|
|
el('l2-cy').innerHTML = '<div class="l2-empty">' + t('l2.no_data', 'No data') + '</div>';
|
|
el('l2-cy').innerHTML = '<div class="l2-empty">' + t('l2.no_data', 'No data') + '</div>';
|