|
|
@@ -13,6 +13,7 @@
|
|
|
var SNAP = window.L2SNAP || null;
|
|
|
|
|
|
var colors = CFG.colors || {100:'#F39C12',1000:'#2ECC71',10000:'#3498DB',20000:'#9B59B6',unknown:'#E74C3C',down:'#C0392B'};
|
|
|
+if (!colors['2500']) colors['2500'] = '#17C3B2';
|
|
|
var LATW = (CFG.latency_warn != null) ? +CFG.latency_warn : 3;
|
|
|
var LATB = (CFG.latency_bad != null) ? +CFG.latency_bad : 5;
|
|
|
|
|
|
@@ -31,6 +32,7 @@
|
|
|
var showTerm = false;
|
|
|
var onlyOff = false;
|
|
|
var pinnedTerm = null;
|
|
|
+ var termPortByMac = {}; // terminal MAC -> rendered port node id
|
|
|
var paused = false;
|
|
|
var countdown = 0;
|
|
|
var timer = null;
|
|
|
@@ -468,6 +470,67 @@
|
|
|
var ROW_H = 34, ROW_GAP = 14, WS_GAP = 36, SEC_GAP = 52;
|
|
|
var FLOOR_GAP = 26;
|
|
|
|
|
|
+ // Front-panel port colours are deliberately separate from link colours:
|
|
|
+ // the ring shows the port's maximum capability, the inner fill shows the
|
|
|
+ // negotiated rate. This is why a 10GE port with a 1G link is purple/green.
|
|
|
+ var PORT_COLORS = {
|
|
|
+ 100: '#F39C12', 1000: '#2ECC71', 2500: '#17C3B2', 5000: '#17C3B2',
|
|
|
+ 10000: '#9B59B6', 20000: '#8E44AD', 40000: '#7D3C98', 100000: '#6C3483',
|
|
|
+ unknown: '#95A5A6'
|
|
|
+ };
|
|
|
+ function portColor(v) {
|
|
|
+ var n = parseFloat(v);
|
|
|
+ if (!isFinite(n) || n <= 0) return PORT_COLORS.unknown;
|
|
|
+ if (n <= 100) return PORT_COLORS[100];
|
|
|
+ if (n <= 1000) return PORT_COLORS[1000];
|
|
|
+ if (n <= 2500) return PORT_COLORS[2500];
|
|
|
+ if (n <= 5000) return PORT_COLORS[5000];
|
|
|
+ if (n <= 10000) return PORT_COLORS[10000];
|
|
|
+ if (n <= 20000) return PORT_COLORS[20000];
|
|
|
+ if (n <= 40000) return PORT_COLORS[40000];
|
|
|
+ return PORT_COLORS[100000];
|
|
|
+ }
|
|
|
+ function portMaxFromName(name, fallback) {
|
|
|
+ var s = String(name || '').toUpperCase().replace(/\s+/g, '');
|
|
|
+ var m = s.match(/^(100GE|50GE|40GE|25GE|10GE|XGE|2\.5GE|GE|FE)/);
|
|
|
+ if (m) {
|
|
|
+ return { '100GE': 100000, '50GE': 50000, '40GE': 40000, '25GE': 25000,
|
|
|
+ '10GE': 10000, 'XGE': 10000, '2.5GE': 2500, 'GE': 1000, 'FE': 100 }[m[1]] || fallback || 0;
|
|
|
+ }
|
|
|
+ if (/^GIGABITETHERNET/.test(s)) return 1000;
|
|
|
+ if (/^XGIGABITETHERNET/.test(s)) return 10000;
|
|
|
+ if (/^FASTETHERNET/.test(s)) return 100;
|
|
|
+ return parseFloat(fallback) || 0;
|
|
|
+ }
|
|
|
+ function portFamily(name) {
|
|
|
+ var s = String(name || '').toUpperCase().replace(/\s+/g, '');
|
|
|
+ var m = s.match(/^(100GE|50GE|40GE|25GE|10GE|XGE|2\.5GE|GE|FE)/);
|
|
|
+ if (m) return m[1];
|
|
|
+ if (/^GIGABITETHERNET/.test(s)) return 'GE';
|
|
|
+ if (/^XGIGABITETHERNET/.test(s)) return 'XGE';
|
|
|
+ if (/^FASTETHERNET/.test(s)) return 'FE';
|
|
|
+ return 'OTHER';
|
|
|
+ }
|
|
|
+ function portRows(ports) {
|
|
|
+ var groups = {}, order = [];
|
|
|
+ (ports || []).slice().sort(function (a, b) {
|
|
|
+ return (parseInt(a.order, 10) || 0) - (parseInt(b.order, 10) || 0);
|
|
|
+ }).forEach(function (p) {
|
|
|
+ var f = portFamily(p.name);
|
|
|
+ if (!groups[f]) { groups[f] = []; order.push(f); }
|
|
|
+ groups[f].push(p);
|
|
|
+ });
|
|
|
+ var rows = [];
|
|
|
+ order.forEach(function (f) {
|
|
|
+ var arr = groups[f];
|
|
|
+ // A normal access panel is two rows: 24 ports => 12+12, 48 => 24+24.
|
|
|
+ // Uplink groups (usually 4-8 ports) stay on their own single row.
|
|
|
+ var per = arr.length <= 12 ? arr.length : Math.ceil(arr.length / 2);
|
|
|
+ for (var i = 0; i < arr.length; i += per) rows.push(arr.slice(i, i + per));
|
|
|
+ });
|
|
|
+ return rows;
|
|
|
+ }
|
|
|
+
|
|
|
function makeNetworks() {
|
|
|
var raw = CFG.networks;
|
|
|
if (!raw || !raw.length) {
|
|
|
@@ -707,7 +770,18 @@
|
|
|
}
|
|
|
|
|
|
function layoutWorkshop(w, top, snap) {
|
|
|
- var rowH = ROW_H + ROW_GAP + (showTerm ? 24 : 0);
|
|
|
+ var portRowMax = 0;
|
|
|
+ if (showTerm) {
|
|
|
+ w.all.forEach(function (n) {
|
|
|
+ var rows = portRows(n.ports).length;
|
|
|
+ if (rows > portRowMax) portRowMax = rows;
|
|
|
+ });
|
|
|
+ }
|
|
|
+ // Every port row is 14px high (9px circle + breathing room). Keep a
|
|
|
+ // little space below the last row; when ports are unavailable the old
|
|
|
+ // 24px terminal area remains.
|
|
|
+ var portExtra = showTerm ? Math.max(24, portRowMax * 14 + 10) : 0;
|
|
|
+ var rowH = ROW_H + ROW_GAP + portExtra;
|
|
|
var floorMap = {}, floors = [], accessFloor = {};
|
|
|
w.access.forEach(function (n) {
|
|
|
var fi = floorInfo(n);
|
|
|
@@ -930,6 +1004,7 @@
|
|
|
if (!isFinite(s) || s <= 0) return colors.unknown;
|
|
|
if (s <= 100) return colors['100'];
|
|
|
if (s <= 1000) return colors['1000'];
|
|
|
+ if (s <= 2500) return colors['2500'];
|
|
|
if (s <= 10000) return colors['10000'];
|
|
|
return colors['20000'];
|
|
|
}
|
|
|
@@ -1059,6 +1134,13 @@
|
|
|
{selector: 'node[type="edgepoint"]', style: {
|
|
|
'width': 1, 'height': 1, 'opacity': 0, 'events': 'no'
|
|
|
}},
|
|
|
+ {selector: 'node[type="port"]', style: {
|
|
|
+ 'width': 'data(size)', 'height': 'data(size)', 'shape': 'ellipse',
|
|
|
+ 'background-color': 'data(fill)', 'background-opacity': 'data(fillOpacity)',
|
|
|
+ 'border-width': 1.5, 'border-color': 'data(ring)', 'padding': '0px',
|
|
|
+ 'label': '', 'overlay-opacity': 0, 'events': 'yes',
|
|
|
+ 'z-index-compare': 'manual', 'z-index': 28
|
|
|
+ }},
|
|
|
{selector: 'node[type="term"]', style: {
|
|
|
'width': 7, 'height': 7, 'shape': 'ellipse', 'background-color': 'data(bg)',
|
|
|
'border-width': 1, 'border-color': d ? '#2a3648' : '#cfd8e3',
|
|
|
@@ -1267,7 +1349,9 @@
|
|
|
// orange frame. Alarm must never be represented by a red border alone.
|
|
|
if (n.alarms && n.alarms.length) { cls.push('alarm'); bg = '#E45959'; bd = '#E45959'; }
|
|
|
if (n.offline || n.oper === 'down') { cls.push('offline'); bg = '#C0392B'; bd = '#C0392B'; }
|
|
|
- if (n.latency_ms != null) {
|
|
|
+ // Alarm/offline already owns the whole fill; do not put a latency frame
|
|
|
+ // around a red alarm node (that would read as a contradictory state).
|
|
|
+ if (!cls.length && n.latency_ms != null) {
|
|
|
if (+n.latency_ms > LATB) cls.push('lat-bad');
|
|
|
else if (+n.latency_ms > LATW) cls.push('lat-warn');
|
|
|
}
|
|
|
@@ -1322,35 +1406,76 @@
|
|
|
}
|
|
|
|
|
|
function addTerminals(L, byId, els) {
|
|
|
- var bySw = {};
|
|
|
+ termPortByMac = {};
|
|
|
+ var bySw = {}, termBySwPort = {};
|
|
|
+ function pkey(s) { return String(s || '').toUpperCase(); }
|
|
|
((lastSnap && lastSnap.terminals) || []).forEach(function (tm) {
|
|
|
if (!byId[tm.sw_ip]) return;
|
|
|
if (!terminalOnline(tm)) return;
|
|
|
(bySw[tm.sw_ip] = bySw[tm.sw_ip] || []).push(tm);
|
|
|
+ if (tm.port) termBySwPort[tm.sw_ip + '|' + pkey(tm.port)] = tm;
|
|
|
});
|
|
|
- Object.keys(bySw).forEach(function (sw) {
|
|
|
+ function pushTermDot(sw, p, tm, x, y, id) {
|
|
|
+ els.push({group: 'nodes', data: {
|
|
|
+ id: id, type: 'term', bg: terminalOnline(tm) ? '#2ECC71' : '#95a5a6',
|
|
|
+ mac: tm.mac, ip: tm.ip, online: terminalOnline(tm), speed: tm.speed,
|
|
|
+ sw_name: tm.sw_name, port: tm.port, label: '', termText: terminalText(tm)
|
|
|
+ }, position: {x: x, y: y}, selectable: false});
|
|
|
+ els.push({group: 'edges', data: {id: 'te_' + tm.mac, type: 'term', source: sw, target: id}});
|
|
|
+ }
|
|
|
+ Object.keys(byId).forEach(function (sw) {
|
|
|
var p = byId[sw]; if (!p || p.role === 'core' || p.role === 'main') return;
|
|
|
- bySw[sw].sort(function (a, b) { return (terminalOnline(a) === terminalOnline(b)) ? 0 : (terminalOnline(a) ? -1 : 1); });
|
|
|
- // A search hit is pinned into the visible 24 so any MAC in the snapshot
|
|
|
- // can be located even when its switch has more than 24 terminals.
|
|
|
- if (pinnedTerm) {
|
|
|
- for (var pi = 0; pi < bySw[sw].length; pi++) {
|
|
|
- if (bySw[sw][pi].mac === pinnedTerm) {
|
|
|
- if (pi > 0) { var pin = bySw[sw].splice(pi, 1)[0]; bySw[sw].unshift(pin); }
|
|
|
- break;
|
|
|
- }
|
|
|
- }
|
|
|
+ var terms = bySw[sw] || [];
|
|
|
+ var rawPorts = (p.node && p.node.ports) || [];
|
|
|
+ if (!rawPorts.length) {
|
|
|
+ // Legacy/history snapshot without a port inventory: keep the compact
|
|
|
+ // terminal dots as a fallback instead of inventing unused ports.
|
|
|
+ terms.slice(0, 24).forEach(function (tm, k) {
|
|
|
+ var col = k % 12, row = Math.floor(k / 12);
|
|
|
+ pushTermDot(sw, p, tm, p.x - p.w / 2 + 16 + col * 9, p.y + 16 + row * 9, 't_' + tm.mac);
|
|
|
+ });
|
|
|
+ return;
|
|
|
}
|
|
|
- bySw[sw].slice(0, 24).forEach(function (tm, k) {
|
|
|
- var col = k % 12, row = Math.floor(k / 12);
|
|
|
- var x = p.x - p.w / 2 + 16 + col * 9;
|
|
|
- var y = p.y + 16 + row * 9;
|
|
|
- els.push({group: 'nodes', data: {
|
|
|
- id: 't_' + tm.mac, type: 'term', bg: terminalOnline(tm) ? '#2ECC71' : '#95a5a6',
|
|
|
- mac: tm.mac, ip: tm.ip, online: terminalOnline(tm), speed: tm.speed,
|
|
|
- sw_name: tm.sw_name, port: tm.port, label: '', termText: terminalText(tm)
|
|
|
- }, position: {x: x, y: y}, selectable: false});
|
|
|
- els.push({group: 'edges', data: {id: 'te_' + tm.mac, type: 'term', source: sw, target: 't_' + tm.mac}});
|
|
|
+ var ports = rawPorts.slice();
|
|
|
+ var known = {};
|
|
|
+ ports.forEach(function (pt) { known[pkey(pt.name)] = true; });
|
|
|
+ // A terminal may sit on a logical interface (for example an Eth-Trunk)
|
|
|
+ // whose physical ports are not individually listed. Keep it visible as
|
|
|
+ // an extra port-like circle rather than silently dropping it.
|
|
|
+ terms.forEach(function (tm) {
|
|
|
+ if (tm.port && !known[pkey(tm.port)]) {
|
|
|
+ ports.push({name: tm.port, order: 9999, speed: parseFloat(tm.speed) || 0,
|
|
|
+ max: portMaxFromName(tm.port, parseFloat(tm.speed) || 0), up: true, synthetic: true});
|
|
|
+ known[pkey(tm.port)] = true;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ var rows = portRows(ports), maxCols = 0;
|
|
|
+ rows.forEach(function (r) { if (r.length > maxCols) maxCols = r.length; });
|
|
|
+ var step = 12.5;
|
|
|
+ var startX = p.x - ((maxCols - 1) * step) / 2;
|
|
|
+ var pidx = 0;
|
|
|
+ rows.forEach(function (row, ri) {
|
|
|
+ row.forEach(function (port, ci) {
|
|
|
+ var tm = termBySwPort[sw + '|' + pkey(port.name)] || null;
|
|
|
+ // The IF-MIB port speed is the negotiated physical link rate, so it
|
|
|
+ // is the primary source for the filled centre. Terminal records are
|
|
|
+ // only a fallback when the switch did not report a speed.
|
|
|
+ var actual = parseFloat(port.speed) || 0;
|
|
|
+ if (!actual) actual = parseFloat(tm && tm.speed) || 0;
|
|
|
+ var max = parseFloat(port.max) || portMaxFromName(port.name, actual);
|
|
|
+ var filled = !!port.up && actual > 0;
|
|
|
+ var pid = 'p_' + sw + '_' + (port.order || pidx) + '_' + pidx;
|
|
|
+ pidx++;
|
|
|
+ els.push({group: 'nodes', data: {
|
|
|
+ id: pid, type: 'port', name: port.name, port: port.name,
|
|
|
+ sw_name: (p.node && p.node.name) || '', sw_ip: sw,
|
|
|
+ max: max, speed: actual, up: !!port.up, filled: filled ? 1 : 0,
|
|
|
+ fill: filled ? portColor(actual) : '#000000', fillOpacity: filled ? 1 : 0,
|
|
|
+ ring: portColor(max), size: 9,
|
|
|
+ term: tm || null, termText: tm ? terminalText(tm) : ''
|
|
|
+ }, position: {x: startX + ci * step, y: p.y + 18 + ri * 14}, selectable: false});
|
|
|
+ if (tm) termPortByMac[tm.mac] = pid;
|
|
|
+ });
|
|
|
});
|
|
|
});
|
|
|
}
|
|
|
@@ -1444,7 +1569,7 @@
|
|
|
bg = '#C0392B';
|
|
|
bd = '#C0392B';
|
|
|
}
|
|
|
- if (n.latency_ms != null) {
|
|
|
+ if (!cls.length && n.latency_ms != null) {
|
|
|
if (+n.latency_ms > LATB) cls.push('lat-bad');
|
|
|
else if (+n.latency_ms > LATW) cls.push('lat-warn');
|
|
|
}
|
|
|
@@ -1542,11 +1667,11 @@
|
|
|
|
|
|
function buildLegend() {
|
|
|
var h = '';
|
|
|
- function sw(c, label, dashed) { return '<div class="lg-row"><span class="lg-line' + (dashed ? ' lg-dash' : '') + '" style="border-top-color:' + c + '"></span>' + label + '</div>'; }
|
|
|
+ function sw(c, label, dashed) { return '<span class="lg-item"><span class="lg-line' + (dashed ? ' lg-dash' : '') + '" style="border-top-color:' + c + '"></span>' + label + '</span>'; }
|
|
|
h += '<div class="lg-title">' + t('l2.legend_speed', 'Link speed') + '</div>';
|
|
|
- h += sw(colors['100'], '100M') + sw(colors['1000'], '1G') + sw(colors['10000'], '10G') + sw(colors['20000'], '20G+');
|
|
|
- h += sw(colors.unknown, t('lbl.color_unknown', 'Unknown'));
|
|
|
- h += sw(colors.down, t('lbl.color_down', 'Down'), true);
|
|
|
+ h += '<div class="lg-grid">' + sw(colors['100'], '100M') + sw(colors['1000'], '1G') +
|
|
|
+ sw(colors['2500'], '2.5G') + sw(colors['10000'], '10G') + sw(colors['20000'], '20G+') +
|
|
|
+ sw(colors.unknown, t('lbl.color_unknown', 'Unknown')) + sw(colors.down, t('lbl.color_down', 'Down'), true) + '</div>';
|
|
|
h += '<div class="lg-title">' + t('l2.legend_tier', 'Tier') + '</div>';
|
|
|
h += '<div class="lg-row"><span class="lg-box" style="background:' + TIER_BG.core + '"></span>' + t('l2.core', 'Core') +
|
|
|
'<span class="lg-box" style="background:#2E75B6;margin-left:8px"></span>' + t('l2.main', 'Main switch') +
|
|
|
@@ -1781,12 +1906,14 @@
|
|
|
pinnedTerm = tm.mac;
|
|
|
showTerm = true;
|
|
|
if (el('l2-showterm')) el('l2-showterm').checked = true;
|
|
|
- if (!cy || cy.getElementById(id).empty()) render(lastSnap);
|
|
|
+ var targetId = termPortByMac[tm.mac] || id;
|
|
|
+ if (!cy || cy.getElementById(targetId).empty()) render(lastSnap);
|
|
|
+ targetId = termPortByMac[tm.mac] || id;
|
|
|
setTimeout(function () {
|
|
|
// Center the switch that the terminal hangs under, then flash the
|
|
|
- // terminal dot so the exact port location is obvious.
|
|
|
+ // exact physical port circle so the port location is obvious.
|
|
|
focusNode(tm.sw_ip || id);
|
|
|
- var tn = cy && cy.getElementById(id);
|
|
|
+ var tn = cy && cy.getElementById(targetId);
|
|
|
if (tn && tn.length) {
|
|
|
tn.addClass('hl');
|
|
|
setTimeout(function () { tn.removeClass('hl'); }, 2600);
|
|
|
@@ -1906,6 +2033,20 @@
|
|
|
e.target.data('label', '');
|
|
|
hideTip();
|
|
|
});
|
|
|
+ cy.on('mouseover', 'node[type="port"]', function (e) {
|
|
|
+ var d = e.target.data();
|
|
|
+ var html = '<b>' + esc(d.name || '') + '</b><br>' +
|
|
|
+ esc(t('l2.term_switch', 'Switch')) + ': ' + esc(d.sw_name || d.sw_ip || '') + '<br>' +
|
|
|
+ esc(t('l2.term_rate', 'Link rate')) + ': ' + (d.filled ? esc(speedText(d.speed)) : '—') + '<br>' +
|
|
|
+ 'Port max: ' + esc(speedText(d.max));
|
|
|
+ if (d.term) {
|
|
|
+ html += '<hr><b>' + esc(t('l2.term_title', 'Terminal')) + '</b><br>' +
|
|
|
+ esc(t('l2.term_mac', 'MAC address')) + ': ' + esc(d.term.mac || '') + '<br>' +
|
|
|
+ esc(t('l2.term_ip', 'IP address')) + ': ' + esc(d.term.ip || '—');
|
|
|
+ }
|
|
|
+ showTip(e, html);
|
|
|
+ });
|
|
|
+ cy.on('mouseout', 'node[type="port"]', hideTip);
|
|
|
cy.on('tap', 'node[type="switch"]', function (e) {
|
|
|
lockedPath = e.target.id();
|
|
|
highlightPath(lockedPath);
|