Add remove button for unreachable nodes not in config
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { getLabel, getShortLabel, getFirstName, isSwitch, getSpeedClass } from './nodes.js';
|
||||
import { addClickableValue, buildLinkStats, buildDanteDetail, buildClickableList } from './ui.js';
|
||||
import { addClickableValue, buildLinkStats, buildDanteDetail, buildClickableList, removeNode } from './ui.js';
|
||||
import { nodeElements, locationElements, usedNodeIds, usedLocationIds } from './state.js';
|
||||
|
||||
export function createNodeElement(node, switchConnection, nodeLocation, uplinkInfo, danteInfo, artnetInfo, sacnInfo, hasError, isUnreachable) {
|
||||
@@ -307,6 +307,24 @@ export function createNodeElement(node, switchConnection, nodeLocation, uplinkIn
|
||||
if (container) container.remove();
|
||||
}
|
||||
|
||||
if (node.unreachable && !node.in_config) {
|
||||
let removeBtn = div.querySelector(':scope > .remove-node-btn');
|
||||
if (!removeBtn) {
|
||||
removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'remove-node-btn';
|
||||
removeBtn.textContent = '×';
|
||||
removeBtn.title = 'Remove node';
|
||||
removeBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
removeNode(node.id);
|
||||
});
|
||||
div.appendChild(removeBtn);
|
||||
}
|
||||
} else {
|
||||
const removeBtn = div.querySelector(':scope > .remove-node-btn');
|
||||
if (removeBtn) removeBtn.remove();
|
||||
}
|
||||
|
||||
return div;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { getLabel, getFirstName, isSwitch, getInterfaceSpeed, getInterfaceErrors
|
||||
import { buildSwitchUplinks } from './topology.js';
|
||||
import { escapeHtml, formatUniverse } from './format.js';
|
||||
import { tableData, tableSortKeys, setTableSortKeys } from './state.js';
|
||||
import { removeNode } from './ui.js';
|
||||
|
||||
export function sortTable(column) {
|
||||
const existingIdx = tableSortKeys.findIndex(k => k.column === column);
|
||||
@@ -79,6 +80,12 @@ export function renderTable() {
|
||||
th.classList.add(primarySort.asc ? 'sorted-asc' : 'sorted-desc');
|
||||
}
|
||||
});
|
||||
|
||||
scrollDiv.querySelectorAll('.remove-node-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
removeNode(btn.dataset.nodeId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function renderNetworkTable() {
|
||||
@@ -223,6 +230,7 @@ export function renderNetworkTable() {
|
||||
const outRateVal = rates == null ? null : (useLocalPerspective ? rates.outBytes : rates.inBytes);
|
||||
|
||||
return {
|
||||
nodeId: node.id,
|
||||
name,
|
||||
ip: ips[0] || '',
|
||||
upstream,
|
||||
@@ -240,7 +248,8 @@ export function renderNetworkTable() {
|
||||
uptime,
|
||||
uptimeStr: formatUptime(uptime),
|
||||
lastErrorTime,
|
||||
lastErrorStr: formatTimeSince(lastErrorTime)
|
||||
lastErrorStr: formatTimeSince(lastErrorTime),
|
||||
removable: node.unreachable && !node.in_config
|
||||
};
|
||||
});
|
||||
|
||||
@@ -251,7 +260,7 @@ export function renderNetworkTable() {
|
||||
html += '<th colspan="4"></th>';
|
||||
html += '<th colspan="4" class="group-in">In</th>';
|
||||
html += '<th colspan="4" class="group-out">Out</th>';
|
||||
html += '<th colspan="3"></th>';
|
||||
html += '<th colspan="4"></th>';
|
||||
html += '</tr><tr>';
|
||||
html += '<th data-sort="name">Name</th>';
|
||||
html += '<th data-sort="ip">IP</th>';
|
||||
@@ -268,6 +277,7 @@ export function renderNetworkTable() {
|
||||
html += '<th data-sort="uptime">Uptime</th>';
|
||||
html += '<th data-sort="lastErrorTime">Last Err</th>';
|
||||
html += '<th data-sort="status">Status</th>';
|
||||
html += '<th></th>';
|
||||
html += '</tr></thead><tbody>';
|
||||
|
||||
rows.forEach(r => {
|
||||
@@ -288,6 +298,11 @@ export function renderNetworkTable() {
|
||||
html += '<td class="numeric">' + r.uptimeStr + '</td>';
|
||||
html += '<td class="numeric">' + r.lastErrorStr + '</td>';
|
||||
html += '<td class="' + statusClass + '">' + r.status + '</td>';
|
||||
html += '<td>';
|
||||
if (r.removable) {
|
||||
html += '<button class="remove-node-btn" data-node-id="' + escapeHtml(r.nodeId) + '" title="Remove node">×</button>';
|
||||
}
|
||||
html += '</td>';
|
||||
html += '</tr>';
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { formatBytes, formatPackets, formatMbps, formatPps, formatLinkSpeed } from './format.js';
|
||||
import { openFlowHash } from './flow.js';
|
||||
import { portErrors, setErrorPanelCollapsed, errorPanelCollapsed } from './state.js';
|
||||
import { portErrors, setErrorPanelCollapsed, errorPanelCollapsed, tableData } from './state.js';
|
||||
|
||||
export function addClickableValue(container, label, value, plainLines, plainFormat) {
|
||||
const lbl = document.createElement('span');
|
||||
@@ -193,6 +193,10 @@ export async function clearAllErrors() {
|
||||
await fetch('/tendrils/api/errors/clear?all=true', { method: 'POST' });
|
||||
}
|
||||
|
||||
export async function removeNode(nodeId) {
|
||||
await fetch('/tendrils/api/nodes/remove?id=' + encodeURIComponent(nodeId), { method: 'POST' });
|
||||
}
|
||||
|
||||
function formatLocalTime(utcString) {
|
||||
if (!utcString) return '';
|
||||
const date = new Date(utcString);
|
||||
@@ -285,6 +289,15 @@ export function updateErrorPanel() {
|
||||
timestampEl.textContent = 'First: ' + formatLocalTime(err.first_seen) + ' / Last: ' + formatLocalTime(err.last_seen);
|
||||
item.appendChild(timestampEl);
|
||||
|
||||
const node = tableData?.nodes?.find(n => n.id === err.node_id);
|
||||
if (node && node.unreachable && !node.in_config) {
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'remove-btn';
|
||||
removeBtn.textContent = 'Remove node';
|
||||
removeBtn.addEventListener('click', () => removeNode(err.node_id));
|
||||
item.appendChild(removeBtn);
|
||||
}
|
||||
|
||||
const dismissBtn = document.createElement('button');
|
||||
dismissBtn.textContent = 'Dismiss';
|
||||
dismissBtn.addEventListener('click', () => clearError(err.id));
|
||||
|
||||
Reference in New Issue
Block a user