UI: LLDP column in device list with No LLDP data marker.

Made-with: Cursor
This commit is contained in:
Луценко Андрей Анатольевич
2026-04-10 13:17:14 +10:00
parent 7e79072619
commit 6d75de1e2e
+26 -10
View File
@@ -60,7 +60,7 @@
<div class="panel"> <div class="panel">
<div class="row" style="margin-bottom: 6px;"> <div class="row" style="margin-bottom: 6px;">
<strong>Найденные устройства</strong> <strong>Найденные устройства</strong>
<span class="hint" style="margin:0;">отвечают на ping; клик по строке — связи этого устройства в таблицах ниже. Граф — «Загрузить».</span> <span class="hint" style="margin:0;">отвечают на ping; колонка LLDP: при отсутствии соседей в MIB — пометка «No LLDP data» (часто для коммутаторов без LLDP или без успешного SNMP). Клик по строке — связи ниже.</span>
<button type="button" id="refreshDevicesBtn">Обновить список</button> <button type="button" id="refreshDevicesBtn">Обновить список</button>
</div> </div>
<div style="overflow:auto; max-height: 240px; border: 1px solid #e5e7eb; border-radius: 8px;"> <div style="overflow:auto; max-height: 240px; border: 1px solid #e5e7eb; border-radius: 8px;">
@@ -71,10 +71,11 @@
<th class="sortable-th" data-sort="ping" style="text-align:left;">Ping <span class="sort-ind"></span></th> <th class="sortable-th" data-sort="ping" style="text-align:left;">Ping <span class="sort-ind"></span></th>
<th class="sortable-th" data-sort="name" style="text-align:left;">Имя (SNMP) <span class="sort-ind"></span></th> <th class="sortable-th" data-sort="name" style="text-align:left;">Имя (SNMP) <span class="sort-ind"></span></th>
<th class="sortable-th" data-sort="snmp" style="text-align:left;">SNMP <span class="sort-ind"></span></th> <th class="sortable-th" data-sort="snmp" style="text-align:left;">SNMP <span class="sort-ind"></span></th>
<th class="sortable-th" data-sort="lldp" style="text-align:left;">LLDP <span class="sort-ind"></span></th>
</tr> </tr>
</thead> </thead>
<tbody id="deviceTableBody"> <tbody id="deviceTableBody">
<tr><td colspan="4" style="padding:10px;color:#64748b;">Укажите scan_id или создайте scan.</td></tr> <tr><td colspan="5" style="padding:10px;color:#64748b;">Укажите scan_id или создайте scan.</td></tr>
</tbody> </tbody>
</table> </table>
</div> </div>
@@ -173,7 +174,7 @@
let lastTopology = null; let lastTopology = null;
let lastHighlight = null; let lastHighlight = null;
let lastDiff = null; let lastDiff = null;
/** @type {{ip:string,ping:string,name:string,snmp:string}[]} */ /** @type {{ip:string,ping:string,name:string,snmp:string,lldp:string,lldpCount:number}[]} */
let deviceRowsCache = []; let deviceRowsCache = [];
let deviceSort = { key: "ip", dir: 1 }; let deviceSort = { key: "ip", dir: 1 };
let selectedDeviceIP = null; let selectedDeviceIP = null;
@@ -264,6 +265,8 @@
cmp = String(a.name).localeCompare(String(b.name), "ru", { sensitivity: "base" }); cmp = String(a.name).localeCompare(String(b.name), "ru", { sensitivity: "base" });
} else if (key === "snmp") { } else if (key === "snmp") {
cmp = String(a.snmp).localeCompare(String(b.snmp), "ru", { sensitivity: "base" }); cmp = String(a.snmp).localeCompare(String(b.snmp), "ru", { sensitivity: "base" });
} else if (key === "lldp") {
cmp = (a.lldpCount || 0) - (b.lldpCount || 0);
} }
return cmp; return cmp;
} }
@@ -281,14 +284,16 @@
function renderDeviceTableBody() { function renderDeviceTableBody() {
updateDeviceSortIndicators(); updateDeviceSortIndicators();
if (deviceRowsCache.length === 0) { if (deviceRowsCache.length === 0) {
deviceTableBody.innerHTML = '<tr><td colspan="4" style="padding:10px;color:#64748b;">Нет хостов с ping «вверх» для этого scan (или скан ещё не дошёл до них).</td></tr>'; deviceTableBody.innerHTML = '<tr><td colspan="5" style="padding:10px;color:#64748b;">Нет хостов с ping «вверх» для этого scan (или скан ещё не дошёл до них).</td></tr>';
return; return;
} }
const sorted = [...deviceRowsCache].sort((a, b) => deviceSort.dir * compareRows(a, b, deviceSort.key)); const sorted = [...deviceRowsCache].sort((a, b) => deviceSort.dir * compareRows(a, b, deviceSort.key));
deviceTableBody.innerHTML = sorted deviceTableBody.innerHTML = sorted
.map((r) => { .map((r) => {
const dip = escapeHtml(r.ip); const dip = escapeHtml(r.ip);
return `<tr class="device-row" data-ip="${dip}" title="Показать связи этого устройства"><td>${dip}</td><td>${escapeHtml(r.ping)}</td><td>${escapeHtml(r.name)}</td><td>${escapeHtml(r.snmp)}</td></tr>`; const noLldp = (r.lldpCount || 0) === 0;
const lldpStyle = noLldp ? ' style="color:#9a3412;font-weight:500;"' : "";
return `<tr class="device-row" data-ip="${dip}" title="Показать связи этого устройства"><td>${dip}</td><td>${escapeHtml(r.ping)}</td><td>${escapeHtml(r.name)}</td><td>${escapeHtml(r.snmp)}</td><td${lldpStyle}>${escapeHtml(r.lldp)}</td></tr>`;
}) })
.join(""); .join("");
deviceTableBody.querySelectorAll("tr.device-row").forEach((tr) => { deviceTableBody.querySelectorAll("tr.device-row").forEach((tr) => {
@@ -405,38 +410,49 @@
if (!scanId) { if (!scanId) {
deviceRowsCache = []; deviceRowsCache = [];
clearDeviceDetailPanel(); clearDeviceDetailPanel();
deviceTableBody.innerHTML = '<tr><td colspan="4" style="padding:10px;color:#64748b;">Нет scan_id.</td></tr>'; deviceTableBody.innerHTML = '<tr><td colspan="5" style="padding:10px;color:#64748b;">Нет scan_id.</td></tr>';
updateDeviceSortIndicators(); updateDeviceSortIndicators();
return; return;
} }
clearDeviceDetailPanel(); clearDeviceDetailPanel();
deviceTableBody.innerHTML = '<tr><td colspan="4" style="padding:10px;color:#64748b;">Загрузка…</td></tr>'; deviceTableBody.innerHTML = '<tr><td colspan="5" style="padding:10px;color:#64748b;">Загрузка…</td></tr>';
try { try {
const [hRes, sRes] = await Promise.all([ const [hRes, sRes, lRes] = await Promise.all([
fetch(`/api/scans/${encodeURIComponent(scanId)}/hosts?limit=5000`), fetch(`/api/scans/${encodeURIComponent(scanId)}/hosts?limit=5000`),
fetch(`/api/scans/${encodeURIComponent(scanId)}/snmp?limit=5000`), fetch(`/api/scans/${encodeURIComponent(scanId)}/snmp?limit=5000`),
fetch(`/api/scans/${encodeURIComponent(scanId)}/lldp?limit=10000`),
]); ]);
if (!hRes.ok) throw new Error(await hRes.text()); if (!hRes.ok) throw new Error(await hRes.text());
if (!sRes.ok) throw new Error(await sRes.text()); if (!sRes.ok) throw new Error(await sRes.text());
if (!lRes.ok) throw new Error((await lRes.text()) || "LLDP");
const hData = await hRes.json(); const hData = await hRes.json();
const sData = await sRes.json(); const sData = await sRes.json();
const lData = await lRes.json();
const snmpByIp = new Map(); const snmpByIp = new Map();
for (const r of sData.items || []) { for (const r of sData.items || []) {
snmpByIp.set(r.ip, r); snmpByIp.set(r.ip, r);
} }
const lldpCountByIp = new Map();
for (const row of lData.items || []) {
const ipk = row.ip;
if (!ipk) continue;
lldpCountByIp.set(ipk, (lldpCountByIp.get(ipk) || 0) + 1);
}
deviceRowsCache = []; deviceRowsCache = [];
for (const h of hData.items || []) { for (const h of hData.items || []) {
if (!h.is_up) continue; if (!h.is_up) continue;
const s = snmpByIp.get(h.ip); const s = snmpByIp.get(h.ip);
const name = s && s.success ? s.sys_name || "(пусто)" : "—"; const name = s && s.success ? s.sys_name || "(пусто)" : "—";
const snmpCell = s ? (s.success ? "OK" : s.error || "нет ответа") : "не опрашивался"; const snmpCell = s ? (s.success ? "OK" : s.error || "нет ответа") : "не опрашивался";
deviceRowsCache.push({ ip: h.ip, ping: "да", name, snmp: snmpCell }); const lldpCount = lldpCountByIp.get(h.ip) || 0;
const lldpCell = lldpCount === 0 ? "No LLDP data" : `${lldpCount} записей`;
deviceRowsCache.push({ ip: h.ip, ping: "да", name, snmp: snmpCell, lldp: lldpCell, lldpCount });
} }
renderDeviceTableBody(); renderDeviceTableBody();
} catch (e) { } catch (e) {
deviceRowsCache = []; deviceRowsCache = [];
clearDeviceDetailPanel(); clearDeviceDetailPanel();
deviceTableBody.innerHTML = `<tr><td colspan="4" style="padding:10px;color:#b91c1c;">Ошибка: ${escapeHtml(e.message)}</td></tr>`; deviceTableBody.innerHTML = `<tr><td colspan="5" style="padding:10px;color:#b91c1c;">Ошибка: ${escapeHtml(e.message)}</td></tr>`;
updateDeviceSortIndicators(); updateDeviceSortIndicators();
} }
} }