|
|
@@ -34,8 +34,9 @@
|
|
|
var countdown = 0;
|
|
|
var timer = null;
|
|
|
var lastSnap = null;
|
|
|
- var viewGroup = null; // null = overview, otherwise workshop label
|
|
|
+ var viewGroup = null; // direct-link focus group, never hides the full chart
|
|
|
var detailNet = null; // null/auto, 'SURVE' or 'OFFICE' inside a workshop
|
|
|
+ var focusGroupLabel = null; // ?g= target, zoomed after the full chart is built
|
|
|
var pathParent = {}; // child -> parent chain for hover highlighting
|
|
|
var lockedPath = null; // switch id whose path is locked by click
|
|
|
|
|
|
@@ -50,11 +51,12 @@
|
|
|
for (var i = 0; i < toks.length; i++) {
|
|
|
var tk = toks[i];
|
|
|
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(tk)) { r.ip = tk; continue; }
|
|
|
- if (!r.workshop && /^\d{4}$/.test(tk)) { r.workshop = tk; continue; }
|
|
|
- if (!r.floor && /^\d[A-Z]\w*$/i.test(tk)) {
|
|
|
- var m = /^(\d)([A-Z])(\w+)$/i.exec(tk);
|
|
|
- if (m) { r.floor = m[1]; r.cab = m[2]; r.u = m[3]; continue; }
|
|
|
+ if (!r.workshop && /^\d{4}[A-Za-z]?$/.test(tk)) { r.workshop = tk; continue; }
|
|
|
+ if (!r.floor) {
|
|
|
+ var fm = /^(GRD|\d{1,2}F)([A-Z])?(\d+)?$/i.exec(tk);
|
|
|
+ if (fm) { r.floor = fm[1].toUpperCase(); r.cab = fm[2] || ''; r.u = fm[3] || ''; continue; }
|
|
|
}
|
|
|
+ if (r.floor && !r.u && /^\d{1,3}$/.test(tk)) { r.u = tk; continue; }
|
|
|
if (!r.area && /^(OFFICE|SURVE|GRDA|DC|IDC|LAB|PROD)$/i.test(tk)) { r.area = tk.toUpperCase(); continue; }
|
|
|
if (!r.role && /^(Dist|Access|Core)$/i.test(tk)) { r.role = tk; }
|
|
|
}
|
|
|
@@ -442,6 +444,350 @@
|
|
|
};
|
|
|
}
|
|
|
|
|
|
+ /* ---------------- one-chart layout: fixed columns by tier ----------------
|
|
|
+ Left to right: physical core → each network's main switch → every
|
|
|
+ workshop distribution switch → access switches. A workshop frame spans
|
|
|
+ the distribution + access columns; each floor is a small frame inside it.
|
|
|
+ Same workshop on two networks is always two separate frames. */
|
|
|
+ var CORE_X = 130, MAIN_X = 430, DIST_X = 790, ACCESS_X = 1300;
|
|
|
+ var CORE_W = 200, MAIN_W = 300, NODE_W3 = 300;
|
|
|
+ var WORK_LEFT = 610, WORK_RIGHT = 1530;
|
|
|
+ var FLOOR_LEFT = 1130, FLOOR_RIGHT = 1525;
|
|
|
+ var CHART_W = 1560;
|
|
|
+ var SEC_HEAD = 46, CORE_ROW = 92, WORK_TOP = 148;
|
|
|
+ var ROW_H = 24, ROW_GAP = 3, WS_GAP = 10, SEC_GAP = 34;
|
|
|
+
|
|
|
+ function makeNetworks() {
|
|
|
+ var raw = CFG.networks;
|
|
|
+ if (!raw || !raw.length) {
|
|
|
+ raw = [
|
|
|
+ {key: 'SURVE', label: t('l2.net_surve', 'SURVE 监控网'), keywords: ['SURVE', '10.169.'], color: '#2E75B6'},
|
|
|
+ {key: 'OFFICE', label: t('l2.net_office', 'OFFICE 办公网'), keywords: ['OFFICE', '10.160.'], color: '#548235'}
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ return raw.map(function (n, i) {
|
|
|
+ var kws = n.keywords;
|
|
|
+ if (!(kws instanceof Array)) kws = String(kws || '').split(/[,,;;\s]+/);
|
|
|
+ return {
|
|
|
+ key: String(n.key || ('NET' + i)).trim().toUpperCase(),
|
|
|
+ label: String(n.label || n.key || ('NET' + i)),
|
|
|
+ keywords: kws.map(function (s) { return String(s).trim(); }).filter(Boolean),
|
|
|
+ color: /^#[0-9a-fA-F]{6}$/.test(String(n.color || '')) ? n.color : '#607D8B'
|
|
|
+ };
|
|
|
+ });
|
|
|
+ }
|
|
|
+ var NETWORKS = makeNetworks();
|
|
|
+
|
|
|
+ function netLabel(key) {
|
|
|
+ for (var i = 0; i < NETWORKS.length; i++) if (NETWORKS[i].key === key) return NETWORKS[i].label;
|
|
|
+ return key === 'OTHER' ? t('l2.net_other', '其他网络') : String(key || '');
|
|
|
+ }
|
|
|
+ function netColor(key) {
|
|
|
+ for (var i = 0; i < NETWORKS.length; i++) if (NETWORKS[i].key === key) return NETWORKS[i].color;
|
|
|
+ return '#607D8B';
|
|
|
+ }
|
|
|
+ function rawNetOf(n) {
|
|
|
+ var hay = (String((n && n.name) || '') + ' ' + String((n && n.ip) || '')).toUpperCase();
|
|
|
+ for (var i = 0; i < NETWORKS.length; i++) {
|
|
|
+ for (var j = 0; j < NETWORKS[i].keywords.length; j++) {
|
|
|
+ if (hay.indexOf(String(NETWORKS[i].keywords[j]).toUpperCase()) >= 0) return NETWORKS[i].key;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return 'OTHER';
|
|
|
+ }
|
|
|
+
|
|
|
+ // Core/root switch rules are configured from the L2 tab, one row per core.
|
|
|
+ // Multiple cores in the same network are supported and all of them are
|
|
|
+ // placed in the left-most core column.
|
|
|
+ function coreRules() {
|
|
|
+ var r = CFG.core_rules;
|
|
|
+ return (r instanceof Array) ? r : [];
|
|
|
+ }
|
|
|
+ function coreRuleHit(rule, n) {
|
|
|
+ var pattern = String(rule.pattern || '');
|
|
|
+ if (!pattern) return false;
|
|
|
+ var hay = String((n && n.name) || '') + ' ' + String((n && n.ip) || '');
|
|
|
+ if (String(rule.match || 'contains') === 'regex') {
|
|
|
+ try { return new RegExp(pattern, 'i').test(hay); } catch (e) { return false; }
|
|
|
+ }
|
|
|
+ return hay.toLowerCase().indexOf(pattern.toLowerCase()) >= 0;
|
|
|
+ }
|
|
|
+ function configuredCore(n) {
|
|
|
+ var rules = coreRules();
|
|
|
+ for (var i = 0; i < rules.length; i++) if (coreRuleHit(rules[i], n)) return true;
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ function configuredCoreNet(n) {
|
|
|
+ var rules = coreRules();
|
|
|
+ for (var i = 0; i < rules.length; i++) {
|
|
|
+ if (coreRuleHit(rules[i], n) && String(rules[i].net || '').trim() !== '') return String(rules[i].net).toUpperCase();
|
|
|
+ }
|
|
|
+ return '';
|
|
|
+ }
|
|
|
+
|
|
|
+ // Network is decided first by name/IP keyword, then by the rest of the
|
|
|
+ // workshop, then by graph adjacency. No switch is ever shown under the
|
|
|
+ // wrong upstream network.
|
|
|
+ function assignNetworks(snap) {
|
|
|
+ var nodes = snap.nodes || [], byId = {}, net = {}, adj = {};
|
|
|
+ nodes.forEach(function (n) {
|
|
|
+ byId[n.id] = n; net[n.id] = rawNetOf(n); adj[n.id] = [];
|
|
|
+ var cn = configuredCoreNet(n);
|
|
|
+ if (cn) net[n.id] = cn;
|
|
|
+ });
|
|
|
+ (snap.edges || []).forEach(function (e) {
|
|
|
+ if (adj[e.source] && adj[e.target]) { adj[e.source].push(e.target); adj[e.target].push(e.source); }
|
|
|
+ });
|
|
|
+ var groupVote = {};
|
|
|
+ nodes.forEach(function (n) {
|
|
|
+ var k = groupOf(n).label, v = net[n.id];
|
|
|
+ if (v && v !== 'OTHER') (groupVote[k] = groupVote[k] || {})[v] = true;
|
|
|
+ });
|
|
|
+ nodes.forEach(function (n) {
|
|
|
+ if (net[n.id] !== 'OTHER') return;
|
|
|
+ var v = groupVote[groupOf(n).label], keys = v ? Object.keys(v) : [];
|
|
|
+ if (keys.length === 1) net[n.id] = keys[0];
|
|
|
+ });
|
|
|
+ for (var pass = 0; pass < 8; pass++) {
|
|
|
+ var changed = false;
|
|
|
+ nodes.forEach(function (n) {
|
|
|
+ if (net[n.id] !== 'OTHER') return;
|
|
|
+ var votes = {};
|
|
|
+ adj[n.id].forEach(function (id) { if (net[id] && net[id] !== 'OTHER') votes[net[id]] = (votes[net[id]] || 0) + 1; });
|
|
|
+ var best = null, bn = 0;
|
|
|
+ Object.keys(votes).forEach(function (k) { if (votes[k] > bn) { best = k; bn = votes[k]; } });
|
|
|
+ if (best) { net[n.id] = best; changed = true; }
|
|
|
+ });
|
|
|
+ if (!changed) break;
|
|
|
+ }
|
|
|
+ return net;
|
|
|
+ }
|
|
|
+
|
|
|
+ function roleOf(n) {
|
|
|
+ var p = parseName(n.name || '');
|
|
|
+ var r = String(p.role || '').toLowerCase();
|
|
|
+ var tier = String(n.tier || '').toLowerCase();
|
|
|
+ if (configuredCore(n)) return 'core';
|
|
|
+ // Backward-compatible fallback only when the core list is absent (old
|
|
|
+ // config). An explicitly empty list means the administrator wants no
|
|
|
+ // configured core column.
|
|
|
+ if (!(CFG.core_rules instanceof Array) && (/^core(\s|-)/i.test(String(n.name || '')) || groupOf(n).label === 'Core')) return 'core';
|
|
|
+ if (tier === 'core' || r === 'core') return 'main';
|
|
|
+ if (r === 'dist') return 'dist';
|
|
|
+ if (r === 'access') return 'access';
|
|
|
+ if (tier.indexOf('dist') === 0) return 'dist';
|
|
|
+ return 'access';
|
|
|
+ }
|
|
|
+
|
|
|
+ function roleName(r) {
|
|
|
+ if (r === 'core') return t('l2.core', '核心');
|
|
|
+ if (r === 'main') return t('l2.main', '主交换');
|
|
|
+ if (r === 'dist') return t('l2.dist', '汇聚');
|
|
|
+ return t('l2.access', '接入');
|
|
|
+ }
|
|
|
+
|
|
|
+ function shortLoc(n) {
|
|
|
+ var p = parseName(n.name || '');
|
|
|
+ if (!p.floor) return '';
|
|
|
+ return p.floor + (p.cab || '') + (p.u ? '-' + p.u : '');
|
|
|
+ }
|
|
|
+
|
|
|
+ function switchLabel(n, role, netKey) {
|
|
|
+ var g = groupOf(n).label;
|
|
|
+ if (role === 'core') return roleName('core') + ' · ' + (netKey || rawNetOf(n)) + ' · ' + (n.ip || '');
|
|
|
+ if (role === 'main') return roleName('main') + ' · ' + g + ' · ' + (n.ip || '');
|
|
|
+ var loc = shortLoc(n);
|
|
|
+ return [g, loc, roleName(role)].filter(Boolean).join(' · ') + ' · ' + (n.ip || '');
|
|
|
+ }
|
|
|
+
|
|
|
+ // A workshop is the physical area served by one distribution switch, not
|
|
|
+ // the four-digit prefix printed on each access switch. The same dist can
|
|
|
+ // serve access switches in several rooms/buildings; they must share one
|
|
|
+ // frame, otherwise their uplink line would cross unrelated frames.
|
|
|
+ function workshopKeys(snap) {
|
|
|
+ var byId = {}, adj = {}, keys = {};
|
|
|
+ (snap.nodes || []).forEach(function (n) { byId[n.id] = n; adj[n.id] = []; });
|
|
|
+ (snap.edges || []).forEach(function (e) {
|
|
|
+ if (adj[e.source] && adj[e.target]) { adj[e.source].push(e.target); adj[e.target].push(e.source); }
|
|
|
+ });
|
|
|
+ (snap.nodes || []).forEach(function (n) {
|
|
|
+ var r = roleOf(n);
|
|
|
+ if (r === 'core' || r === 'main') return;
|
|
|
+ if (r === 'dist') { keys[n.id] = groupOf(n).label; return; }
|
|
|
+ var ds = (adj[n.id] || []).map(function (id) { return byId[id]; })
|
|
|
+ .filter(function (d) { return d && roleOf(d) === 'dist'; });
|
|
|
+ var same = ds.filter(function (d) { return groupOf(d).label === groupOf(n).label; });
|
|
|
+ var pick = same[0] || ds[0];
|
|
|
+ keys[n.id] = pick ? groupOf(pick).label : groupOf(n).label;
|
|
|
+ });
|
|
|
+ return keys;
|
|
|
+ }
|
|
|
+
|
|
|
+ function workshopList(snap, netMap, netKey, wsKeys) {
|
|
|
+ var map = {}, order = [];
|
|
|
+ (snap.nodes || []).forEach(function (n) {
|
|
|
+ if (netMap[n.id] !== netKey) return;
|
|
|
+ var r = roleOf(n);
|
|
|
+ if (r === 'core' || r === 'main') return;
|
|
|
+ var g = (wsKeys && wsKeys[n.id]) || groupOf(n).label;
|
|
|
+ if (!map[g]) { map[g] = {key: g, label: g, extra: [], net: netKey, dists: [], access: [], all: []}; order.push(g); }
|
|
|
+ map[g].all.push(n);
|
|
|
+ if (r === 'dist') map[g].dists.push(n); else map[g].access.push(n);
|
|
|
+ });
|
|
|
+ return order.map(function (g) { return map[g]; }).filter(function (w) { return w.all.length > 0; })
|
|
|
+ .map(function (w) {
|
|
|
+ w.access.forEach(function (a) {
|
|
|
+ var raw = groupOf(a).label;
|
|
|
+ if (raw !== w.key && w.extra.indexOf(raw) < 0) w.extra.push(raw);
|
|
|
+ });
|
|
|
+ w.label = w.key + (w.extra.length ? ' + ' + w.extra.slice(0, 3).join('/') + (w.extra.length > 3 ? '…' : '') : '');
|
|
|
+ return w;
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ function layoutWorkshop(w, top) {
|
|
|
+ var rowH = ROW_H + (showTerm ? 24 : 0);
|
|
|
+ var floorMap = {}, floors = [];
|
|
|
+ w.access.forEach(function (n) {
|
|
|
+ var fi = floorInfo(n);
|
|
|
+ var k = fi.key;
|
|
|
+ if (!floorMap[k]) { floorMap[k] = {key: k, label: fi.label, rank: (fi.key === 'NA' ? 999 : fi.rank), nodes: [], rows: []}; floors.push(floorMap[k]); }
|
|
|
+ floorMap[k].nodes.push(n);
|
|
|
+ });
|
|
|
+ floors.sort(function (a, b) { return a.rank - b.rank || String(a.label).localeCompare(String(b.label)); });
|
|
|
+ var accessH = 0;
|
|
|
+ floors.forEach(function (f) {
|
|
|
+ f.nodes.sort(function (a, b) { return String(a.name).localeCompare(String(b.name)); });
|
|
|
+ f.h = 22 + f.nodes.length * rowH + 6;
|
|
|
+ accessH += f.h;
|
|
|
+ });
|
|
|
+ if (floors.length > 1) accessH += (floors.length - 1) * 5;
|
|
|
+ var distH = w.dists.length ? (w.dists.length * 64 + (w.dists.length - 1) * 8) : 0;
|
|
|
+ var inner = Math.max(accessH, distH, 54);
|
|
|
+ var h = 38 + inner + 10;
|
|
|
+ w.top = top; w.h = h; w.rowH = rowH;
|
|
|
+ // Floors are stacked bottom-up: GRD lowest, then 1F → 3F → 4F → 7F.
|
|
|
+ var cursor = top + h - 8;
|
|
|
+ floors.forEach(function (f) {
|
|
|
+ cursor -= f.h;
|
|
|
+ f.frame = {x: FLOOR_LEFT, y: cursor, w: FLOOR_RIGHT - FLOOR_LEFT, h: f.h};
|
|
|
+ f.nodes.forEach(function (n, i) {
|
|
|
+ f.rows.push({id: n.id, x: ACCESS_X, y: cursor + 22 + rowH / 2 + i * rowH});
|
|
|
+ });
|
|
|
+ cursor -= 5;
|
|
|
+ });
|
|
|
+ var dstart = top + 38 + (inner - distH) / 2;
|
|
|
+ w.distPos = w.dists.map(function (n, i) {
|
|
|
+ return {id: n.id, x: DIST_X, y: dstart + 32 + i * 72};
|
|
|
+ });
|
|
|
+ w.floors = floors;
|
|
|
+ w.title = w.label + ' · ' + netLabel(w.net) + ' · ' + w.all.length + ' ' + t('l2.unit_sw', '台');
|
|
|
+ // A workshop without a distribution switch only needs a box around its
|
|
|
+ // access column; keeping the full dist-column width would leave a large
|
|
|
+ // empty rectangle and make the chart look sparse.
|
|
|
+ var frameLeft = w.dists.length ? WORK_LEFT : FLOOR_LEFT - 40;
|
|
|
+ w.frame = {x: frameLeft, y: top, w: WORK_RIGHT - frameLeft, h: h};
|
|
|
+ return h;
|
|
|
+ }
|
|
|
+
|
|
|
+ function computeFull(snap) {
|
|
|
+ var nodes = snap.nodes || [];
|
|
|
+ var netMap = assignNetworks(snap);
|
|
|
+ var sections = [], placements = [], frames = [], roleMap = {};
|
|
|
+ var wsKeys = workshopKeys(snap);
|
|
|
+ var y = 100;
|
|
|
+ NETWORKS.forEach(function (net) {
|
|
|
+ var secNodes = nodes.filter(function (n) { return netMap[n.id] === net.key; });
|
|
|
+ if (!secNodes.length) return;
|
|
|
+ var cores = [], main = null;
|
|
|
+ secNodes.forEach(function (n) {
|
|
|
+ roleMap[n.id] = roleOf(n);
|
|
|
+ if (roleMap[n.id] === 'core') cores.push(n);
|
|
|
+ if (roleMap[n.id] === 'main' && !main) main = n;
|
|
|
+ });
|
|
|
+ var ws = workshopList(snap, netMap, net.key, wsKeys);
|
|
|
+ var sec = {
|
|
|
+ key: net.key, label: netLabel(net.key) + ' · ' + secNodes.length + ' ' + t('l2.unit_sw', '台'),
|
|
|
+ x: 10, y: y, w: CHART_W - 20, h: 0, color: netColor(net.key),
|
|
|
+ core: cores[0] || null, cores: cores, main: main, workshops: ws
|
|
|
+ };
|
|
|
+ var cur = y + WORK_TOP;
|
|
|
+ ws.forEach(function (w) {
|
|
|
+ layoutWorkshop(w, cur);
|
|
|
+ cur += w.h + WS_GAP;
|
|
|
+ frames.push({id: 'ws_' + net.key + '_' + w.key, type: 'wsframe', x: w.frame.x, y: w.frame.y, w: w.frame.w, h: w.frame.h,
|
|
|
+ label: w.title, group: w.key, net: net.key, color: net.color});
|
|
|
+ w.floors.forEach(function (f, fi) {
|
|
|
+ frames.push({id: 'ff_' + net.key + '_' + w.key + '_' + fi, type: 'floorframe', x: f.frame.x, y: f.frame.y,
|
|
|
+ w: f.frame.w, h: f.frame.h, label: f.label + ' · ' + f.nodes.length + ' ' + t('l2.unit_sw', '台'), net: net.key, color: net.color});
|
|
|
+ });
|
|
|
+ w.distPos.forEach(function (p) {
|
|
|
+ var n = nodes.filter(function (x) { return x.id === p.id; })[0];
|
|
|
+ if (n) placements.push({node: n, role: 'dist', x: p.x, y: p.y, w: NODE_W3, h: 26, net: net.key, group: w.key, ws: net.key + '|' + w.key});
|
|
|
+ });
|
|
|
+ w.floors.forEach(function (f) {
|
|
|
+ f.rows.forEach(function (p) {
|
|
|
+ var n = nodes.filter(function (x) { return x.id === p.id; })[0];
|
|
|
+ if (n) placements.push({node: n, role: 'access', x: p.x, y: p.y, w: NODE_W3, h: 26, net: net.key, group: w.key, ws: net.key + '|' + w.key});
|
|
|
+ });
|
|
|
+ });
|
|
|
+ });
|
|
|
+ sec.h = (cur - y) + 16;
|
|
|
+ // The network main switch stays at the top of its tier column (its access
|
|
|
+ // children sit directly under it, as in the reference chart); the core
|
|
|
+ // sits lower in the column so its uplinks fan symmetrically.
|
|
|
+ sec.mainY = y + SEC_HEAD + 46;
|
|
|
+ sec.coreY = y + Math.max(CORE_ROW + SEC_HEAD, Math.round(sec.h * 0.55));
|
|
|
+ if (cores.length) {
|
|
|
+ var coreStep = 64;
|
|
|
+ var coreTop = sec.coreY - ((cores.length - 1) * coreStep) / 2;
|
|
|
+ cores.forEach(function (cn, ci) {
|
|
|
+ placements.push({node: cn, role: 'core', x: CORE_X, y: coreTop + ci * coreStep, w: CORE_W, h: 54, net: net.key, group: 'Core'});
|
|
|
+ });
|
|
|
+ }
|
|
|
+ if (main) placements.push({node: main, role: 'main', x: MAIN_X, y: sec.mainY, w: MAIN_W, h: 46, net: net.key, group: groupOf(main).label});
|
|
|
+ sections.push(sec);
|
|
|
+ y += sec.h + SEC_GAP;
|
|
|
+ });
|
|
|
+ var root = {x: 0, y: 0, w: CHART_W, h: y + 6};
|
|
|
+ return {sections: sections, placements: placements, frames: frames, netMap: netMap, roleMap: roleMap, root: root};
|
|
|
+ }
|
|
|
+
|
|
|
+ // Immediate upstream parent per switch: access -> its workshop dist;
|
|
|
+ // dist -> core when directly attached, otherwise the network main switch.
|
|
|
+ function buildParents(snap, L) {
|
|
|
+ var byId = {}, adj = {}, edgeSpeed = {};
|
|
|
+ L.placements.forEach(function (p) { byId[p.node.id] = p; });
|
|
|
+ (snap.nodes || []).forEach(function (n) { adj[n.id] = []; });
|
|
|
+ (snap.edges || []).forEach(function (e) {
|
|
|
+ if (!adj[e.source] || !adj[e.target]) return;
|
|
|
+ adj[e.source].push(e.target); adj[e.target].push(e.source);
|
|
|
+ edgeSpeed[e.source + '|' + e.target] = edgeSpeed[e.target + '|' + e.source] = parseFloat(e.speed) || 0;
|
|
|
+ });
|
|
|
+ var parent = {};
|
|
|
+ Object.keys(byId).forEach(function (id) {
|
|
|
+ var role = L.roleMap[id] || 'access';
|
|
|
+ if (role === 'core') return;
|
|
|
+ var p = byId[id], cands = (adj[id] || []).map(function (x) { return {id: x, role: L.roleMap[x] || 'access', p: byId[x]}; })
|
|
|
+ .filter(function (c) { return c.p; });
|
|
|
+ var want = role === 'main' ? ['core'] : (role === 'dist' ? ['core', 'main'] : ['dist', 'main', 'core']);
|
|
|
+ var best = null;
|
|
|
+ for (var wi = 0; wi < want.length && !best; wi++) {
|
|
|
+ var list = cands.filter(function (c) {
|
|
|
+ if (c.role !== want[wi]) return false;
|
|
|
+ if (role === 'access' && want[wi] === 'dist' && c.p.ws && p.ws && c.p.ws !== p.ws) return false;
|
|
|
+ return true;
|
|
|
+ });
|
|
|
+ if (list.length) {
|
|
|
+ list.sort(function (a, b) { return (edgeSpeed[id + '|' + b.id] || 0) - (edgeSpeed[id + '|' + a.id] || 0); });
|
|
|
+ best = list[0];
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (best) parent[id] = best.id;
|
|
|
+ });
|
|
|
+ return parent;
|
|
|
+ }
|
|
|
+
|
|
|
/* ---------------- speed colour ---------------- */
|
|
|
function speedColor(speed, state) {
|
|
|
if (state && state !== 'up') return colors.down;
|
|
|
@@ -485,22 +831,30 @@
|
|
|
'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
|
|
|
}},
|
|
|
- {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',
|
|
|
+ {selector: 'node[type="netframe"]', style: {
|
|
|
+ 'background-opacity': d ? 0.03 : 0.02, 'background-color': d ? '#7b91b8' : '#adc0d8',
|
|
|
+ 'border-width': 3, 'border-style': 'solid', 'border-color': 'data(bd)',
|
|
|
+ 'label': 'data(label)', 'text-valign': 'top', 'text-halign': 'center',
|
|
|
+ 'font-size': 22, 'font-weight': 900, 'color': d ? '#dbe9ff' : '#29425f',
|
|
|
+ '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',
|
|
|
+ {selector: 'node[type="wsframe"]', style: {
|
|
|
+ 'background-opacity': d ? 0.02 : 0.014, 'background-color': d ? '#6f8fc4' : '#8fa6c4',
|
|
|
+ 'border-width': 2.2, 'border-style': 'solid', 'border-color': 'data(bd)',
|
|
|
'label': 'data(label)', 'text-valign': 'top', 'text-halign': 'center', 'text-wrap': 'none',
|
|
|
- 'font-size': 13, 'font-weight': 800, 'color': d ? '#a9c1e3' : '#425a78',
|
|
|
+ 'font-size': 15.5, 'font-weight': 900, 'color': d ? '#d7e6ff' : '#2f4560',
|
|
|
'text-margin-x': 0, 'text-margin-y': 7, 'padding': '0px', 'events': 'no',
|
|
|
'z-index-compare': 'manual', 'z-index': 2
|
|
|
}},
|
|
|
+ {selector: 'node[type="floorframe"]', style: {
|
|
|
+ 'background-opacity': d ? 0.04 : 0.025, 'background-color': d ? '#5b7bb0' : '#8fa6c4',
|
|
|
+ 'border-width': 1.1, 'border-style': 'dashed', 'border-color': 'data(bd)',
|
|
|
+ 'label': 'data(label)', 'text-valign': 'top', 'text-halign': 'center', 'text-wrap': 'none',
|
|
|
+ 'font-size': 11.5, 'font-weight': 800, 'color': d ? '#a9c1e3' : '#425a78',
|
|
|
+ 'text-margin-x': 0, 'text-margin-y': 6, 'padding': '0px', 'events': 'no',
|
|
|
+ 'z-index-compare': 'manual', 'z-index': 3
|
|
|
+ }},
|
|
|
{selector: 'node[type="root"]', style: {
|
|
|
'background-opacity': d ? 0.02 : 0.012, 'background-color': d ? '#7aa0d8' : '#8ea6c2',
|
|
|
'border-width': 2.2, 'border-style': 'solid', 'border-color': d ? '#476492' : '#9fb0c6',
|
|
|
@@ -508,6 +862,30 @@
|
|
|
'font-size': 28, 'font-weight': 900, 'color': d ? '#cfe0ff' : '#33465f',
|
|
|
'text-margin-y': 10, 'events': 'no', 'z-index-compare': 'manual', 'z-index': 0
|
|
|
}},
|
|
|
+ {selector: 'node[type="roottitle"]', style: {
|
|
|
+ 'width': 'data(w)', 'height': 20, 'shape': 'rectangle', 'background-opacity': 0, 'border-width': 0,
|
|
|
+ 'label': 'data(label)', 'text-valign': 'center', 'text-halign': 'center', 'text-wrap': 'none',
|
|
|
+ 'font-size': 24, 'font-weight': 900, 'color': d ? '#cfe0ff' : '#33465f', 'events': 'no',
|
|
|
+ 'z-index-compare': 'manual', 'z-index': 5
|
|
|
+ }},
|
|
|
+ {selector: 'node[type="nettitle"]', style: {
|
|
|
+ 'width': 'data(w)', 'height': 20, 'shape': 'rectangle', 'background-opacity': 0, 'border-width': 0,
|
|
|
+ 'label': 'data(label)', 'text-valign': 'center', 'text-halign': 'center', 'text-wrap': 'none',
|
|
|
+ 'font-size': 20, 'font-weight': 900, 'color': d ? '#dbe9ff' : '#29425f', 'events': 'no',
|
|
|
+ 'z-index-compare': 'manual', 'z-index': 5
|
|
|
+ }},
|
|
|
+ {selector: 'node[type="wslabel"]', style: {
|
|
|
+ 'width': 'data(w)', 'height': 16, 'shape': 'rectangle', 'background-opacity': 0, 'border-width': 0,
|
|
|
+ 'label': 'data(label)', 'text-valign': 'center', 'text-halign': 'center', 'text-wrap': 'none',
|
|
|
+ 'font-size': 15, 'font-weight': 900, 'color': d ? '#d7e6ff' : '#2f4560', 'events': 'no',
|
|
|
+ 'z-index-compare': 'manual', 'z-index': 5
|
|
|
+ }},
|
|
|
+ {selector: 'node[type="floorlabel"]', style: {
|
|
|
+ 'width': 'data(w)', 'height': 16, 'shape': 'rectangle', 'background-opacity': 0, 'border-width': 0,
|
|
|
+ 'label': 'data(label)', 'text-valign': 'center', 'text-halign': 'center', 'text-wrap': 'none',
|
|
|
+ 'font-size': 11, 'font-weight': 800, 'color': d ? '#a9c1e3' : '#425a78', 'events': 'no',
|
|
|
+ 'z-index-compare': 'manual', 'z-index': 5
|
|
|
+ }},
|
|
|
{selector: 'node[type="tile"]', style: {
|
|
|
'width': 'data(w)', 'height': 'data(h)', 'shape': 'round-rectangle',
|
|
|
'background-color': 'data(bg)', 'background-opacity': 0.97,
|
|
|
@@ -531,7 +909,7 @@
|
|
|
'width': 1, 'height': 1, 'opacity': 0, 'events': 'no'
|
|
|
}},
|
|
|
{selector: 'node[type="term"]', style: {
|
|
|
- 'width': 12, 'height': 12, 'shape': 'ellipse', 'background-color': 'data(bg)',
|
|
|
+ 'width': 7, 'height': 7, 'shape': 'ellipse', 'background-color': 'data(bg)',
|
|
|
'border-width': 1, 'border-color': d ? '#2a3648' : '#cfd8e3', 'label': '',
|
|
|
'z-index-compare': 'manual', 'z-index': 30
|
|
|
}},
|
|
|
@@ -559,6 +937,12 @@
|
|
|
'curve-style': 'bezier', 'opacity': 0.92, 'target-arrow-shape': 'none',
|
|
|
'z-index-compare': 'manual', 'z-index': 12
|
|
|
}},
|
|
|
+ {selector: 'edge[type="link"]', style: {
|
|
|
+ 'width': 'data(w)', 'line-color': 'data(color)', 'line-style': 'data(ls)',
|
|
|
+ 'opacity': 0.62, 'curve-style': 'straight',
|
|
|
+ 'source-endpoint': '0% 50%', 'target-endpoint': '0% 50%',
|
|
|
+ 'target-arrow-shape': 'none', 'z-index-compare': 'manual', 'z-index': 1
|
|
|
+ }},
|
|
|
{selector: 'edge[type="term"]', style: {
|
|
|
'width': 0.7, 'line-color': d ? '#3b4a5e' : '#c5cfdb', 'opacity': 0.55,
|
|
|
'curve-style': 'bezier', 'z-index-compare': 'manual', 'z-index': 11
|
|
|
@@ -583,43 +967,158 @@
|
|
|
}
|
|
|
|
|
|
/* ---------------- render ---------------- */
|
|
|
- function applyElements(els) {
|
|
|
+ // One tall column chart cannot be squeezed into a wide window without
|
|
|
+ // wasting half the canvas. The initial view fits the chart WIDTH and
|
|
|
+ // anchors at the top (core switches first); the user can pan down or press
|
|
|
+ // "适应窗口" to see the entire graph. This keeps labels readable.
|
|
|
+ function initialView() {
|
|
|
+ if (!cy) return;
|
|
|
+ var bb = cy.elements().boundingBox();
|
|
|
+ if (!bb || !bb.w || !bb.h) return;
|
|
|
+ var zw = (cy.width() - 30) / bb.w;
|
|
|
+ var zh = (cy.height() - 30) / bb.h;
|
|
|
+ var tall = (bb.h * zw > cy.height() * 1.5);
|
|
|
+ var z = tall ? zw * 0.95 : Math.min(zw, zh);
|
|
|
+ cy.zoom({level: z});
|
|
|
+ if (tall) {
|
|
|
+ cy.pan({x: 15 - bb.x * z, y: 28 - bb.y * z});
|
|
|
+ } else {
|
|
|
+ cy.pan({x: (cy.width() - bb.w * z) / 2 - bb.x * z, y: (cy.height() - bb.h * z) / 2 - bb.y * z});
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function applyElements(els, keepView) {
|
|
|
if (!cy) {
|
|
|
cy = cytoscape({
|
|
|
container: el('l2-cy'),
|
|
|
elements: els,
|
|
|
style: styleFor(dark),
|
|
|
- layout: {name: 'preset', fit: true, padding: 40},
|
|
|
+ layout: {name: 'preset', fit: false},
|
|
|
wheelSensitivity: 0.2,
|
|
|
- minZoom: 0.15, maxZoom: 4
|
|
|
+ minZoom: 0.02, maxZoom: 4
|
|
|
});
|
|
|
bindCy();
|
|
|
+ setTimeout(initialView, 0);
|
|
|
} else {
|
|
|
+ var z = cy.zoom(), p = cy.pan();
|
|
|
cy.elements().remove();
|
|
|
cy.add(els);
|
|
|
cy.style(styleFor(dark));
|
|
|
- cy.layout({name: 'preset', fit: true, padding: 40}).run();
|
|
|
+ cy.layout({name: 'preset', fit: false}).run();
|
|
|
+ if (keepView) { cy.zoom(z); cy.pan(p); }
|
|
|
+ else { initialView(); }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ function fitGroup(label) {
|
|
|
+ if (!cy || !label) return;
|
|
|
+ var eles = cy.nodes().filter(function (n) {
|
|
|
+ return n.data('blockGroup') === label || n.data('group') === label;
|
|
|
+ });
|
|
|
+ if (eles.length) cy.fit(eles, 70);
|
|
|
+ }
|
|
|
+
|
|
|
function render(snap) {
|
|
|
if (!snap || !snap.nodes) {
|
|
|
el('l2-cy').innerHTML = '<div class="l2-empty">' + t('l2.no_data', 'No data') + '</div>';
|
|
|
return;
|
|
|
}
|
|
|
lastSnap = snap;
|
|
|
- if (viewGroup) {
|
|
|
- var exists = (snap.nodes || []).some(function (n) { return groupOf(n).label === viewGroup; });
|
|
|
- if (!exists) viewGroup = null;
|
|
|
- }
|
|
|
- if (viewGroup) renderDetail(snap); else renderOverview(snap);
|
|
|
- if (el('l2-back')) el('l2-back').style.display = viewGroup ? '' : 'none';
|
|
|
+ renderFull(snap);
|
|
|
updateStats(snap);
|
|
|
buildLegend();
|
|
|
buildAlarms(snap);
|
|
|
buildOffline(snap);
|
|
|
}
|
|
|
|
|
|
+ function renderFull(snap) {
|
|
|
+ var L = computeFull(snap), els = [];
|
|
|
+ if (!L.placements.length) { applyElements([], false); return; }
|
|
|
+ var byId = {};
|
|
|
+ L.placements.forEach(function (p) { byId[p.node.id] = p; });
|
|
|
+
|
|
|
+ els.push({group: 'nodes', data: {id: '__root', type: 'root', label: '', w: L.root.w, h: L.root.h},
|
|
|
+ position: {x: L.root.w / 2, y: L.root.h / 2}, selectable: false, grabbable: false});
|
|
|
+ els.push({group: 'nodes', data: {id: '__roottitle', type: 'roottitle', label: CFG.root_label || 'COBCO', w: L.root.w, h: 20},
|
|
|
+ position: {x: L.root.w / 2, y: 22}, selectable: false, grabbable: false});
|
|
|
+ L.sections.forEach(function (sec, si) {
|
|
|
+ els.push({group: 'nodes', data: {
|
|
|
+ id: '__net_' + si, type: 'netframe', label: '', w: sec.w, h: sec.h, net: sec.key, bd: sec.color
|
|
|
+ }, position: {x: sec.x + sec.w / 2, y: sec.y + sec.h / 2}, selectable: false, grabbable: false});
|
|
|
+ els.push({group: 'nodes', data: {id: '__nettitle_' + si, type: 'nettitle', label: sec.label, w: sec.w, h: 20},
|
|
|
+ position: {x: sec.x + sec.w / 2, y: sec.y + 21}, selectable: false, grabbable: false});
|
|
|
+ });
|
|
|
+ L.frames.forEach(function (f) {
|
|
|
+ els.push({group: 'nodes', data: {
|
|
|
+ id: f.id, type: f.type, label: '', w: f.w, h: f.h, group: f.group || '', net: f.net, bd: f.color || netColor(f.net)
|
|
|
+ }, position: {x: f.x + f.w / 2, y: f.y + f.h / 2}, selectable: false, grabbable: false});
|
|
|
+ var tt = f.type === 'floorframe' ? 'floorlabel' : 'wslabel';
|
|
|
+ var ty = f.type === 'floorframe' ? 11 : 18;
|
|
|
+ els.push({group: 'nodes', data: {id: 'label_' + f.id, type: tt, label: f.label, w: f.w, h: 16},
|
|
|
+ position: {x: f.x + f.w / 2, y: f.y + ty}, selectable: false, grabbable: false});
|
|
|
+ });
|
|
|
+
|
|
|
+ L.placements.forEach(function (p) {
|
|
|
+ var n = p.node, cls = [];
|
|
|
+ var bg = p.role === 'core' ? TIER_BG.core : (p.role === 'main' ? '#2E75B6' : (p.role === 'dist' ? TIER_BG.dist : TIER_BG.access));
|
|
|
+ var bd = bg;
|
|
|
+ if (n.alarms && n.alarms.length) { cls.push('alarm'); bd = '#E45959'; }
|
|
|
+ if (n.latency_ms != null) {
|
|
|
+ if (+n.latency_ms > LATB) cls.push('lat-bad');
|
|
|
+ else if (+n.latency_ms > LATW) cls.push('lat-warn');
|
|
|
+ }
|
|
|
+ if (p.role === 'core') cls.push('root');
|
|
|
+ els.push({group: 'nodes', data: {
|
|
|
+ id: n.id, type: 'switch', name: n.name, ip: n.ip, tier: n.tier, depth: n.depth, 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,
|
|
|
+ label: switchLabel(n, p.role, p.net), tw: p.w - 12, bg: bg, bd: bd, w: p.w, h: p.h,
|
|
|
+ group: p.group + ' · ' + netLabel(p.net), blockGroup: p.group, net: p.net, role: p.role
|
|
|
+ }, position: {x: p.x, y: p.y}, classes: cls.join(' ')});
|
|
|
+ });
|
|
|
+
|
|
|
+ // Every protocol-discovered link = one straight segment. Endpoints sit at
|
|
|
+ // the label left-edge midpoint; the labels paint on top, so a parent line
|
|
|
+ // starts under the parent label and ends exactly at the child's left edge.
|
|
|
+ pathParent = buildParents(snap, L);
|
|
|
+ (snap.edges || []).forEach(function (e, i) {
|
|
|
+ if (!byId[e.source] || !byId[e.target]) return;
|
|
|
+ var col = speedColor(e.speed, e.state);
|
|
|
+ els.push({group: 'edges', data: {
|
|
|
+ id: 'e_' + i, type: 'link', source: e.source, target: e.target, p: e.source, c: e.target,
|
|
|
+ speed: e.speed, state: e.state, a_port: e.a_port, b_port: e.b_port,
|
|
|
+ color: col, ls: (e.state && e.state !== 'up') ? 'dashed' : 'solid', w: Math.min(4, speedWidth(e.speed))
|
|
|
+ }});
|
|
|
+ });
|
|
|
+
|
|
|
+ if (showTerm) addTerminals(L, byId, els);
|
|
|
+ applyElements(els, true);
|
|
|
+ if (focusGroupLabel) {
|
|
|
+ setTimeout(function () { fitGroup(focusGroupLabel); }, 80);
|
|
|
+ focusGroupLabel = null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function addTerminals(L, byId, els) {
|
|
|
+ var bySw = {};
|
|
|
+ ((lastSnap && lastSnap.terminals) || []).forEach(function (tm) {
|
|
|
+ if (!byId[tm.sw_ip]) return;
|
|
|
+ if (onlyOff && tm.online) return;
|
|
|
+ (bySw[tm.sw_ip] = bySw[tm.sw_ip] || []).push(tm);
|
|
|
+ });
|
|
|
+ Object.keys(bySw).forEach(function (sw) {
|
|
|
+ var p = byId[sw]; if (!p || p.role === 'core' || p.role === 'main') return;
|
|
|
+ bySw[sw].sort(function (a, b) { return (a.online === b.online) ? 0 : (a.online ? -1 : 1); });
|
|
|
+ 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: tm.online ? '#2ECC71' : '#95a5a6', mac: tm.mac, ip: tm.ip, online: tm.online},
|
|
|
+ position: {x: x, y: y}, selectable: false});
|
|
|
+ els.push({group: 'edges', data: {id: 'te_' + tm.mac, type: 'term', source: sw, target: 't_' + tm.mac}});
|
|
|
+ });
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
function renderOverview(snap) {
|
|
|
var O = computeOverview(snap), els = [], idx = {};
|
|
|
var nb = el('l2-net-tabs'); if (nb) { nb.style.display = 'none'; nb.innerHTML = ''; }
|
|
|
@@ -787,6 +1286,7 @@
|
|
|
h += sw(colors.down, t('lbl.color_down', 'Down'), true);
|
|
|
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') +
|
|
|
'<span class="lg-box" style="background:' + TIER_BG.dist + ';margin-left:8px"></span>' + t('l2.dist', 'Distribution') +
|
|
|
'<span class="lg-box" style="background:' + TIER_BG.access + ';margin-left:8px"></span>' + t('l2.access', 'Access') + '</div>';
|
|
|
h += '<div class="lg-title">' + t('l2.legend_lat', 'Latency') + '</div>';
|
|
|
@@ -875,21 +1375,12 @@
|
|
|
if (id.indexOf('t_') === 0) {
|
|
|
var tm = (lastSnap.terminals || []).filter(function (x) { return 't_' + x.mac === id; })[0];
|
|
|
if (!tm) return;
|
|
|
- var g = groupOfNodeId(tm.sw_ip);
|
|
|
- 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;
|
|
|
if (el('l2-showterm')) el('l2-showterm').checked = true;
|
|
|
- render(lastSnap);
|
|
|
+ if (!cy || cy.getElementById(id).empty()) render(lastSnap);
|
|
|
setTimeout(function () { focusNode(id); }, 90);
|
|
|
} else {
|
|
|
- var g2 = groupOfNodeId(id);
|
|
|
- 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);
|
|
|
+ focusNode(id);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -921,7 +1412,7 @@
|
|
|
|
|
|
function bindCy() {
|
|
|
cy.on('mouseover', 'node[type="switch"]', function (e) {
|
|
|
- if (viewGroup) highlightPath(e.target.id());
|
|
|
+ highlightPath(e.target.id());
|
|
|
var d = e.target.data();
|
|
|
var arr = d.alarms || [];
|
|
|
var sev = (d.maxsev != null && d.maxsev >= 0) ? ('<span style="color:' + SEV_COLORS[d.maxsev] + '">● ' + SEV_NAMES[d.maxsev] + '</span>') : '—';
|
|
|
@@ -938,18 +1429,6 @@
|
|
|
if (!lockedPath) clearPath();
|
|
|
hideTip();
|
|
|
});
|
|
|
- cy.on('mouseover', 'node[type="tile"]', function (e) {
|
|
|
- var d = e.target.data();
|
|
|
- var html = '<b>' + esc(d.group) + '</b><br>' + d.n + ' ' + t('l2.unit_sw', 'sw') + '<br>' +
|
|
|
- '↑ Core: ' + (d.up || 0) + ' · ⚠ ' + (d.alarms || 0) + ' · ◌ ' + (d.offline || 0) +
|
|
|
- '<br><span style="opacity:.7">' + t('l2.tile_hint', 'Click to open this workshop') + '</span>';
|
|
|
- showTip(e, html);
|
|
|
- });
|
|
|
- cy.on('mouseout', 'node[type="tile"]', hideTip);
|
|
|
- cy.on('tap', 'node[type="tile"]', function (e) {
|
|
|
- var g = e.target.data('group');
|
|
|
- if (g) { viewGroup = g; detailNet = null; render(lastSnap); }
|
|
|
- });
|
|
|
cy.on('mouseover', 'edge:not([type="term"])', function (e) {
|
|
|
var d = e.target.data();
|
|
|
var sp = (parseFloat(d.speed) || 0) >= 1000 ? ((parseFloat(d.speed) / 1000) + 'G') : (d.speed + 'M');
|
|
|
@@ -960,6 +1439,9 @@
|
|
|
lockedPath = e.target.id();
|
|
|
highlightPath(lockedPath);
|
|
|
});
|
|
|
+ cy.on('tap', 'node[type="wsframe"]', function (e) {
|
|
|
+ cy.fit(e.target, 60);
|
|
|
+ });
|
|
|
cy.on('tap', function (e) {
|
|
|
if (e.target === cy) clearPath();
|
|
|
});
|
|
|
@@ -1015,20 +1497,50 @@
|
|
|
}
|
|
|
|
|
|
/* ---------------- toolbar wiring ---------------- */
|
|
|
+ function isZabbixDark() {
|
|
|
+ var html = document.documentElement;
|
|
|
+ var cs = String(html.getAttribute('color-scheme') || '').toLowerCase();
|
|
|
+ if (cs === 'dark') return true;
|
|
|
+ if (cs === 'light') return false;
|
|
|
+ var body = document.body;
|
|
|
+ var theme = [
|
|
|
+ html.getAttribute('theme'), html.getAttribute('data-theme'), html.className,
|
|
|
+ body ? body.getAttribute('theme') : '', body ? body.getAttribute('data-theme') : '',
|
|
|
+ body ? body.className : ''
|
|
|
+ ].join(' ');
|
|
|
+ return /dark/i.test(theme);
|
|
|
+ }
|
|
|
+
|
|
|
+ function applyTheme() {
|
|
|
+ var nd = isZabbixDark();
|
|
|
+ var changed = nd !== dark;
|
|
|
+ dark = nd;
|
|
|
+ var wrap = el('l2-wrap');
|
|
|
+ if (wrap) wrap.classList.toggle('l2-dark', dark);
|
|
|
+ if (changed && cy) cy.style(styleFor(dark));
|
|
|
+ }
|
|
|
+
|
|
|
+ function fitViewport() {
|
|
|
+ var wrap = el('l2-wrap');
|
|
|
+ if (!wrap) return;
|
|
|
+ var top = Math.max(0, wrap.getBoundingClientRect().top);
|
|
|
+ var h = Math.max(520, window.innerHeight - top - 8);
|
|
|
+ wrap.style.height = h + 'px';
|
|
|
+ if (cy) cy.resize();
|
|
|
+ }
|
|
|
+
|
|
|
function setupControls() {
|
|
|
- dark = el('l2-wrap').classList.contains('l2-dark');
|
|
|
- el('l2-dark').checked = dark;
|
|
|
- var back = el('l2-back');
|
|
|
- if (back) {
|
|
|
- back.textContent = t('l2.back_overview', '← Overview');
|
|
|
- back.addEventListener('click', function () { viewGroup = null; detailNet = null; render(lastSnap); });
|
|
|
+ applyTheme();
|
|
|
+ if (window.MutationObserver) {
|
|
|
+ var mo = new MutationObserver(applyTheme);
|
|
|
+ mo.observe(document.documentElement, {attributes: true, attributeFilter: ['theme', 'data-theme', 'color-scheme', 'class']});
|
|
|
+ if (document.body) {
|
|
|
+ mo.observe(document.body, {attributes: true, attributeFilter: ['theme', 'data-theme', 'color-scheme', 'class']});
|
|
|
+ }
|
|
|
}
|
|
|
+ var back = el('l2-back');
|
|
|
+ if (back) back.style.display = 'none';
|
|
|
|
|
|
- el('l2-dark').addEventListener('change', function () {
|
|
|
- dark = el('l2-dark').checked;
|
|
|
- el('l2-wrap').classList.toggle('l2-dark', dark);
|
|
|
- render(lastSnap);
|
|
|
- });
|
|
|
el('l2-showterm').addEventListener('change', function () { showTerm = el('l2-showterm').checked; render(lastSnap); });
|
|
|
el('l2-onlyoff').addEventListener('change', function () {
|
|
|
onlyOff = el('l2-onlyoff').checked;
|
|
|
@@ -1037,8 +1549,9 @@
|
|
|
});
|
|
|
el('l2-zin').addEventListener('click', function () { if (cy) cy.zoom({level: cy.zoom() * 1.25, renderedPosition: {x: cy.width() / 2, y: cy.height() / 2}}); });
|
|
|
el('l2-zout').addEventListener('click', function () { if (cy) cy.zoom({level: cy.zoom() / 1.25, renderedPosition: {x: cy.width() / 2, y: cy.height() / 2}}); });
|
|
|
- el('l2-fit').addEventListener('click', function () { if (cy) cy.fit(undefined, 50); });
|
|
|
+ el('l2-fit').addEventListener('click', function () { if (cy) cy.fit(cy.elements(), 50); });
|
|
|
el('l2-refresh-btn').addEventListener('click', function () { reload(true); });
|
|
|
+ window.addEventListener('resize', fitViewport);
|
|
|
|
|
|
var s = el('l2-search');
|
|
|
s.addEventListener('input', function () { doSearch(s.value); });
|
|
|
@@ -1086,12 +1599,11 @@
|
|
|
/* ---------------- boot ---------------- */
|
|
|
function boot() {
|
|
|
setupControls();
|
|
|
+ fitViewport();
|
|
|
try {
|
|
|
var qs = new URLSearchParams(window.location.search);
|
|
|
var qg = qs.get('g');
|
|
|
- if (qg) viewGroup = qg;
|
|
|
- var qn = qs.get('net');
|
|
|
- if (qn) detailNet = qn;
|
|
|
+ if (qg) focusGroupLabel = qg;
|
|
|
} catch (e) {}
|
|
|
if (!SNAP || !SNAP.nodes) {
|
|
|
el('l2-cy').innerHTML = '<div class="l2-empty">' + t('l2.no_data', 'No data') + '</div>';
|
|
|
@@ -1099,6 +1611,7 @@
|
|
|
INDEX = buildIndex(SNAP);
|
|
|
render(SNAP);
|
|
|
}
|
|
|
+ setTimeout(fitViewport, 60);
|
|
|
if (META.isLive) startTimer();
|
|
|
else { el('l2-pause').disabled = true; el('l2-countdown').textContent = t('l2.snapshot', 'Snapshot'); }
|
|
|
}
|