Add granular click-to-copy for popup values and refactor helpers

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Ian Gulliver
2026-01-27 11:19:28 -08:00
parent 5ec5e8e3e5
commit d173e8bac6

View File

@@ -644,6 +644,10 @@
cursor: pointer; cursor: pointer;
} }
.clickable-value, .node-name {
cursor: pointer;
}
.node:hover .node-info-wrapper { .node:hover .node-info-wrapper {
display: block; display: block;
@@ -1030,6 +1034,76 @@
return Math.round(pps).toLocaleString() + ' pps'; return Math.round(pps).toLocaleString() + ' pps';
} }
function addClickableValue(container, label, value, plainLines, plainFormat) {
const lbl = document.createElement('span');
lbl.className = 'lbl';
lbl.textContent = label;
container.appendChild(lbl);
container.appendChild(document.createTextNode(' '));
const val = document.createElement('span');
val.className = 'clickable-value';
val.textContent = value;
val.addEventListener('click', (e) => {
e.stopPropagation();
navigator.clipboard.writeText(value);
});
container.appendChild(val);
plainLines.push(plainFormat ? plainFormat(label, value) : label + ': ' + value);
}
function buildClickableList(container, items, label, plainFormat) {
const plainLines = [];
items.forEach((item, idx) => {
if (idx > 0) container.appendChild(document.createTextNode('\n'));
addClickableValue(container, label, item, plainLines, plainFormat);
});
container.addEventListener('click', (e) => {
e.stopPropagation();
navigator.clipboard.writeText(plainLines.join('\n'));
});
}
function buildLinkStats(container, speed, errIn, errOut, rates) {
const plainLines = [];
addClickableValue(container, 'LINK', formatLinkSpeed(speed), plainLines);
container.appendChild(document.createTextNode('\n'));
addClickableValue(container, 'ERR', 'RX ' + errIn + ' / TX ' + errOut, plainLines);
if (rates) {
container.appendChild(document.createTextNode('\n'));
addClickableValue(container, 'RX', formatMbps(rates.rxBytes) + ' (' + formatPps(rates.rxPkts) + ')', plainLines);
container.appendChild(document.createTextNode('\n'));
addClickableValue(container, 'TX', formatMbps(rates.txBytes) + ' (' + formatPps(rates.txPkts) + ')', plainLines);
}
container.addEventListener('click', (e) => {
e.stopPropagation();
navigator.clipboard.writeText(plainLines.join('\n'));
});
}
function buildDanteDetail(container, entries, arrow) {
const plainLines = [];
entries.forEach((entry, entryIdx) => {
entry.split('\n').forEach((line, lineIdx) => {
if (entryIdx > 0 && lineIdx === 0) {
container.appendChild(document.createTextNode('\n\n'));
plainLines.push('');
} else if (container.childNodes.length > 0) {
container.appendChild(document.createTextNode('\n'));
}
if (line.startsWith(' ')) {
container.appendChild(document.createTextNode(' ' + line.trim()));
plainLines.push(' ' + line.trim());
} else {
addClickableValue(container, arrow, line, plainLines, (l, v) => l + ' ' + v);
}
});
});
container.addEventListener('click', (e) => {
e.stopPropagation();
navigator.clipboard.writeText(plainLines.join('\n'));
});
}
function formatLinkSpeed(bps) { function formatLinkSpeed(bps) {
if (!bps) return '?'; if (!bps) return '?';
const mbps = bps / 1000000; const mbps = bps / 1000000;
@@ -1148,29 +1222,33 @@
statsWrapper.className = 'link-stats-wrapper'; statsWrapper.className = 'link-stats-wrapper';
const statsInfo = document.createElement('div'); const statsInfo = document.createElement('div');
statsInfo.className = 'link-stats'; statsInfo.className = 'link-stats';
let statsHtml = '<span class="lbl">LINK</span> ' + formatLinkSpeed(switchConnection.speed); const r = switchConnection.rates;
statsHtml += '\n<span class="lbl">ERR</span> RX ' + errIn + ' / TX ' + errOut; buildLinkStats(statsInfo, switchConnection.speed, errIn, errOut,
let statsPlain = 'LINK: ' + formatLinkSpeed(switchConnection.speed); r ? {rxBytes: r.outBytes, rxPkts: r.outPkts, txBytes: r.inBytes, txPkts: r.inPkts} : null);
statsPlain += '\nERR: RX ' + errIn + ' / TX ' + errOut;
if (switchConnection.rates) {
const r = switchConnection.rates;
statsHtml += '\n<span class="lbl">RX</span> ' + formatMbps(r.outBytes) + ' (' + formatPps(r.outPkts) + ')';
statsHtml += '\n<span class="lbl">TX</span> ' + formatMbps(r.inBytes) + ' (' + formatPps(r.inPkts) + ')';
statsPlain += '\nRX: ' + formatMbps(r.outBytes) + ' (' + formatPps(r.outPkts) + ')';
statsPlain += '\nTX: ' + formatMbps(r.inBytes) + ' (' + formatPps(r.inPkts) + ')';
}
statsInfo.innerHTML = statsHtml;
statsInfo.addEventListener('click', (e) => {
e.stopPropagation();
navigator.clipboard.writeText(statsPlain);
});
statsWrapper.appendChild(statsInfo); statsWrapper.appendChild(statsInfo);
portEl.appendChild(statsWrapper); portEl.appendChild(statsWrapper);
div.appendChild(portEl); div.appendChild(portEl);
} }
const labelEl = document.createElement('span'); const labelEl = document.createElement('span');
labelEl.textContent = getLabel(node); if (node.names && node.names.length > 0) {
node.names.forEach((name, idx) => {
if (idx > 0) labelEl.appendChild(document.createTextNode('\n'));
const nameSpan = document.createElement('span');
nameSpan.className = 'node-name';
nameSpan.textContent = name;
nameSpan.addEventListener('click', (e) => {
e.stopPropagation();
navigator.clipboard.writeText(name).then(() => {
div.classList.add('copied');
setTimeout(() => div.classList.remove('copied'), 300);
});
});
labelEl.appendChild(nameSpan);
});
} else {
labelEl.textContent = getLabel(node);
}
div.appendChild(labelEl); div.appendChild(labelEl);
const nodeInfoWrapper = document.createElement('div'); const nodeInfoWrapper = document.createElement('div');
@@ -1186,18 +1264,16 @@
}); });
ips.sort(); ips.sort();
macs.sort(); macs.sort();
const lines = [];
const plainLines = []; const plainLines = [];
ips.forEach(ip => { ips.forEach((ip, idx) => {
lines.push('<span class="lbl">IP</span> ' + ip); if (idx > 0) nodeInfo.appendChild(document.createTextNode('\n'));
plainLines.push('IP: ' + ip); addClickableValue(nodeInfo, 'IP', ip, plainLines);
}); });
macs.forEach(mac => { macs.forEach((mac, idx) => {
lines.push('<span class="lbl">MAC</span> ' + mac); if (ips.length > 0 || idx > 0) nodeInfo.appendChild(document.createTextNode('\n'));
plainLines.push('MAC: ' + mac); addClickableValue(nodeInfo, 'MAC', mac, plainLines);
}); });
if (lines.length > 0) { if (plainLines.length > 0) {
nodeInfo.innerHTML = lines.join('\n');
nodeInfo.addEventListener('click', (e) => { nodeInfo.addEventListener('click', (e) => {
e.stopPropagation(); e.stopPropagation();
navigator.clipboard.writeText(plainLines.join('\n')); navigator.clipboard.writeText(plainLines.join('\n'));
@@ -1226,22 +1302,9 @@
statsWrapper.className = 'link-stats-wrapper'; statsWrapper.className = 'link-stats-wrapper';
const statsInfo = document.createElement('div'); const statsInfo = document.createElement('div');
statsInfo.className = 'link-stats'; statsInfo.className = 'link-stats';
let statsHtml = '<span class="lbl">LINK</span> ' + formatLinkSpeed(uplinkInfo.speed); const r = uplinkInfo.rates;
statsHtml += '\n<span class="lbl">ERR</span> RX ' + errIn + ' / TX ' + errOut; buildLinkStats(statsInfo, uplinkInfo.speed, errIn, errOut,
let statsPlain = 'LINK: ' + formatLinkSpeed(uplinkInfo.speed); r ? {rxBytes: r.inBytes, rxPkts: r.inPkts, txBytes: r.outBytes, txPkts: r.outPkts} : null);
statsPlain += '\nERR: RX ' + errIn + ' / TX ' + errOut;
if (uplinkInfo.rates) {
const r = uplinkInfo.rates;
statsHtml += '\n<span class="lbl">RX</span> ' + formatMbps(r.inBytes) + ' (' + formatPps(r.inPkts) + ')';
statsHtml += '\n<span class="lbl">TX</span> ' + formatMbps(r.outBytes) + ' (' + formatPps(r.outPkts) + ')';
statsPlain += '\nRX: ' + formatMbps(r.inBytes) + ' (' + formatPps(r.inPkts) + ')';
statsPlain += '\nTX: ' + formatMbps(r.outBytes) + ' (' + formatPps(r.outPkts) + ')';
}
statsInfo.innerHTML = statsHtml;
statsInfo.addEventListener('click', (e) => {
e.stopPropagation();
navigator.clipboard.writeText(statsPlain);
});
statsWrapper.appendChild(statsInfo); statsWrapper.appendChild(statsInfo);
uplinkEl.appendChild(statsWrapper); uplinkEl.appendChild(statsWrapper);
div.appendChild(uplinkEl); div.appendChild(uplinkEl);
@@ -1257,29 +1320,7 @@
detailWrapper.className = 'dante-detail-wrapper'; detailWrapper.className = 'dante-detail-wrapper';
const detail = document.createElement('div'); const detail = document.createElement('div');
detail.className = 'dante-detail'; detail.className = 'dante-detail';
const txHtml = danteInfo.txTo.map(entry => { buildDanteDetail(detail, danteInfo.txTo, '→');
const lines = entry.split('\n');
return lines.map(line => {
if (line.startsWith(' ')) {
return ' ' + line.trim();
}
return '<span class="lbl">→</span> ' + line;
}).join('\n');
}).join('\n\n');
const txPlain = danteInfo.txTo.map(entry => {
const lines = entry.split('\n');
return lines.map(line => {
if (line.startsWith(' ')) {
return ' ' + line.trim();
}
return '→ ' + line;
}).join('\n');
}).join('\n\n');
detail.innerHTML = txHtml;
detail.addEventListener('click', (e) => {
e.stopPropagation();
navigator.clipboard.writeText(txPlain);
});
detailWrapper.appendChild(detail); detailWrapper.appendChild(detail);
txEl.appendChild(detailWrapper); txEl.appendChild(detailWrapper);
div.appendChild(txEl); div.appendChild(txEl);
@@ -1295,29 +1336,7 @@
detailWrapper.className = 'dante-detail-wrapper'; detailWrapper.className = 'dante-detail-wrapper';
const detail = document.createElement('div'); const detail = document.createElement('div');
detail.className = 'dante-detail'; detail.className = 'dante-detail';
const rxHtml = danteInfo.rxFrom.map(entry => { buildDanteDetail(detail, danteInfo.rxFrom, '←');
const lines = entry.split('\n');
return lines.map(line => {
if (line.startsWith(' ')) {
return ' ' + line.trim();
}
return '<span class="lbl">←</span> ' + line;
}).join('\n');
}).join('\n\n');
const rxPlain = danteInfo.rxFrom.map(entry => {
const lines = entry.split('\n');
return lines.map(line => {
if (line.startsWith(' ')) {
return ' ' + line.trim();
}
return '← ' + line;
}).join('\n');
}).join('\n\n');
detail.innerHTML = rxHtml;
detail.addEventListener('click', (e) => {
e.stopPropagation();
navigator.clipboard.writeText(rxPlain);
});
detailWrapper.appendChild(detail); detailWrapper.appendChild(detail);
rxEl.appendChild(detailWrapper); rxEl.appendChild(detailWrapper);
div.appendChild(rxEl); div.appendChild(rxEl);
@@ -1332,12 +1351,7 @@
detailWrapper.className = 'artnet-detail-wrapper'; detailWrapper.className = 'artnet-detail-wrapper';
const detail = document.createElement('div'); const detail = document.createElement('div');
detail.className = 'artnet-detail'; detail.className = 'artnet-detail';
detail.innerHTML = artnetInfo.outputs.map(u => '<span class="lbl">←</span> ' + u).join('\n'); buildClickableList(detail, artnetInfo.outputs, '←', (l, v) => l + ' ' + v);
const outPlain = artnetInfo.outputs.map(u => '← ' + u).join('\n');
detail.addEventListener('click', (e) => {
e.stopPropagation();
navigator.clipboard.writeText(outPlain);
});
detailWrapper.appendChild(detail); detailWrapper.appendChild(detail);
outEl.appendChild(detailWrapper); outEl.appendChild(detailWrapper);
div.appendChild(outEl); div.appendChild(outEl);
@@ -1352,12 +1366,7 @@
detailWrapper.className = 'artnet-detail-wrapper'; detailWrapper.className = 'artnet-detail-wrapper';
const detail = document.createElement('div'); const detail = document.createElement('div');
detail.className = 'artnet-detail'; detail.className = 'artnet-detail';
detail.innerHTML = artnetInfo.inputs.map(u => '<span class="lbl">→</span> ' + u).join('\n'); buildClickableList(detail, artnetInfo.inputs, '→', (l, v) => l + ' ' + v);
const inPlain = artnetInfo.inputs.map(u => '→ ' + u).join('\n');
detail.addEventListener('click', (e) => {
e.stopPropagation();
navigator.clipboard.writeText(inPlain);
});
detailWrapper.appendChild(detail); detailWrapper.appendChild(detail);
inEl.appendChild(detailWrapper); inEl.appendChild(detailWrapper);
div.appendChild(inEl); div.appendChild(inEl);
@@ -1372,20 +1381,20 @@
detailWrapper.className = 'sacn-detail-wrapper'; detailWrapper.className = 'sacn-detail-wrapper';
const detail = document.createElement('div'); const detail = document.createElement('div');
detail.className = 'sacn-detail'; detail.className = 'sacn-detail';
detail.innerHTML = sacnInfo.universes.map(u => '<span class="lbl">←</span> ' + u).join('\n'); buildClickableList(detail, sacnInfo.universes, '←', (l, v) => l + ' ' + v);
const sacnPlain = sacnInfo.universes.map(u => '← ' + u).join('\n');
detail.addEventListener('click', (e) => {
e.stopPropagation();
navigator.clipboard.writeText(sacnPlain);
});
detailWrapper.appendChild(detail); detailWrapper.appendChild(detail);
sacnEl.appendChild(detailWrapper); sacnEl.appendChild(detailWrapper);
div.appendChild(sacnEl); div.appendChild(sacnEl);
} }
div.addEventListener('click', () => { div.addEventListener('click', () => {
const json = JSON.stringify(node, null, 2); let copyText;
navigator.clipboard.writeText(json).then(() => { if (node.names && node.names.length > 0) {
copyText = node.names.join('\n');
} else {
copyText = getLabel(node);
}
navigator.clipboard.writeText(copyText).then(() => {
div.classList.add('copied'); div.classList.add('copied');
setTimeout(() => div.classList.remove('copied'), 300); setTimeout(() => div.classList.remove('copied'), 300);
}); });