diff --git a/static/js/components.js b/static/js/components.js index 9c94e0a..aa48a47 100644 --- a/static/js/components.js +++ b/static/js/components.js @@ -1,4 +1,4 @@ -import { getLabel, getShortLabel, isSwitch, getSpeedClass } from './nodes.js'; +import { getLabel, getShortLabel, getFirstName, isSwitch, getSpeedClass } from './nodes.js'; import { addClickableValue, buildLinkStats, buildDanteDetail, buildClickableList } from './ui.js'; import { nodeElements, locationElements, usedNodeIds, usedLocationIds } from './state.js'; @@ -186,7 +186,7 @@ export function createNodeElement(node, switchConnection, nodeLocation, uplinkIn const detail = container.querySelector('.dante-detail'); detail.innerHTML = ''; - buildDanteDetail(detail, danteInfo.txTo, '→', node.id, danteInfo.txToPeerIds); + buildDanteDetail(detail, danteInfo.txTo, '→', danteInfo.nodeName, danteInfo.txToPeerNames); } else { const container = div.querySelector(':scope > .dante-tx-hover'); if (container) container.remove(); @@ -207,7 +207,7 @@ export function createNodeElement(node, switchConnection, nodeLocation, uplinkIn const detail = container.querySelector('.dante-detail'); detail.innerHTML = ''; - buildDanteDetail(detail, danteInfo.rxFrom, '←', node.id, danteInfo.rxFromPeerIds); + buildDanteDetail(detail, danteInfo.rxFrom, '←', danteInfo.nodeName, danteInfo.rxFromPeerNames); } else { const container = div.querySelector(':scope > .dante-rx-hover'); if (container) container.remove(); @@ -230,7 +230,7 @@ export function createNodeElement(node, switchConnection, nodeLocation, uplinkIn const detail = container.querySelector('.artnet-detail'); detail.innerHTML = ''; buildClickableList(detail, artnetInfo.outputs.map(o => o.display), '←', (l, v) => l + ' ' + v, - { protocol: 'artnet', nodeId: node.id, universes: artnetInfo.outputs.map(o => o.universe) }); + { protocol: 'artnet', nodeName: getFirstName(node), universes: artnetInfo.outputs.map(o => o.universe) }); } else { const container = div.querySelector(':scope > .artnet-out-hover'); if (container) container.remove(); @@ -253,7 +253,7 @@ export function createNodeElement(node, switchConnection, nodeLocation, uplinkIn const detail = container.querySelector('.artnet-detail'); detail.innerHTML = ''; buildClickableList(detail, artnetInfo.inputs.map(i => i.display), '→', (l, v) => l + ' ' + v, - { protocol: 'artnet', nodeId: node.id, universes: artnetInfo.inputs.map(i => i.universe) }); + { protocol: 'artnet', nodeName: getFirstName(node), universes: artnetInfo.inputs.map(i => i.universe) }); } else { const container = div.querySelector(':scope > .artnet-in-hover'); if (container) container.remove(); @@ -276,7 +276,7 @@ export function createNodeElement(node, switchConnection, nodeLocation, uplinkIn const detail = container.querySelector('.sacn-detail'); detail.innerHTML = ''; buildClickableList(detail, sacnInfo.outputs.map(o => o.display), '←', (l, v) => l + ' ' + v, - { protocol: 'sacn', nodeId: node.id, universes: sacnInfo.outputs.map(o => o.universe) }); + { protocol: 'sacn', nodeName: getFirstName(node), universes: sacnInfo.outputs.map(o => o.universe) }); } else { const container = div.querySelector(':scope > .sacn-out-hover'); if (container) container.remove(); @@ -299,7 +299,7 @@ export function createNodeElement(node, switchConnection, nodeLocation, uplinkIn const detail = container.querySelector('.sacn-detail'); detail.innerHTML = ''; buildClickableList(detail, sacnInfo.inputs.map(i => i.display), '→', (l, v) => l + ' ' + v, - { protocol: 'sacn', nodeId: node.id, universes: sacnInfo.inputs.map(i => i.universe) }); + { protocol: 'sacn', nodeName: getFirstName(node), universes: sacnInfo.inputs.map(i => i.universe) }); } else { const container = div.querySelector(':scope > .sacn-in-hover'); if (container) container.remove(); diff --git a/static/js/flow.js b/static/js/flow.js index 93577ae..b7c50c7 100644 --- a/static/js/flow.js +++ b/static/js/flow.js @@ -1,4 +1,4 @@ -import { getShortLabel, isSwitch, findInterface } from './nodes.js'; +import { getShortLabel, getFirstName, isSwitch, findInterface } from './nodes.js'; import { flowViewData, currentMode, currentView } from './state.js'; function scrollToNode(typeid) { @@ -89,7 +89,7 @@ export function showFlowView(flowSpec) { else { const sourceNode = nodesByTypeId.get(sourceId); const destNode = nodesByTypeId.get(destId); - title = 'Dante: ' + getShortLabel(sourceNode) + ' → ' + getShortLabel(destNode); + title = 'Dante: ' + getFirstName(sourceNode) + ' → ' + getFirstName(destNode); const path = findPath(graph, sourceId, destId); if (path) paths.push({ path, sourceId, destId }); else error = 'No path found between nodes'; @@ -102,7 +102,7 @@ export function showFlowView(flowSpec) { else { const sourceNode = nodesByTypeId.get(sourceId); const danteTx = sourceNode.dante_flows?.tx || []; - title = 'Dante TX: ' + getShortLabel(sourceNode) + (txChannel ? ' ch ' + txChannel : ''); + title = 'Dante TX: ' + getFirstName(sourceNode) + (txChannel ? ' ch ' + txChannel : ''); const destIds = new Set(); danteTx.forEach(peer => { if (txChannel) { @@ -149,8 +149,8 @@ export function showFlowView(flowSpec) { const isSource = sourceIds.includes(clickedNodeId); const isDest = destIds.includes(clickedNodeId); if (isSource) { - const destNames = destIds.filter(id => id !== clickedNodeId).map(id => getShortLabel(nodesByTypeId.get(id))).join(', '); - title = protoName + ' ' + universe + ': ' + getShortLabel(clickedNode) + ' → ' + (destNames || '?'); + const destNames = destIds.filter(id => id !== clickedNodeId).map(id => getFirstName(nodesByTypeId.get(id))).join(', '); + title = protoName + ' ' + universe + ': ' + getFirstName(clickedNode) + ' → ' + (destNames || '?'); destIds.forEach(destId => { if (destId !== clickedNodeId) { const path = findPath(graph, clickedNodeId, destId); @@ -158,8 +158,8 @@ export function showFlowView(flowSpec) { } }); } else if (isDest) { - const sourceNames = sourceIds.map(id => getShortLabel(nodesByTypeId.get(id))).join(', '); - title = protoName + ' ' + universe + ': ' + (sourceNames || '?') + ' → ' + getShortLabel(clickedNode); + const sourceNames = sourceIds.map(id => getFirstName(nodesByTypeId.get(id))).join(', '); + title = protoName + ' ' + universe + ': ' + (sourceNames || '?') + ' → ' + getFirstName(clickedNode); sourceIds.forEach(sourceId => { const path = findPath(graph, sourceId, clickedNodeId); if (path) paths.push({ path, sourceId, destId: clickedNodeId }); @@ -324,8 +324,9 @@ export function renderFlowPath(pathInfo, nodesByTypeId, flowUniverse, flowProtoc const nodeEl = document.createElement('div'); nodeEl.className = 'flow-node'; + const isSourceNode = step.nodeId === sourceId && sourceId !== destId; if (isSwitch(node)) nodeEl.classList.add('switch'); - if (step.nodeId === sourceId && sourceId !== destId) nodeEl.classList.add('source'); + if (isSourceNode) nodeEl.classList.add('source'); else if (step.nodeId === destId) nodeEl.classList.add('dest'); nodeEl.textContent = getShortLabel(node); nodeEl.addEventListener('click', (e) => { @@ -333,30 +334,52 @@ export function renderFlowPath(pathInfo, nodesByTypeId, flowUniverse, flowProtoc closeFlowView(); scrollToNode(step.nodeId); }); - container.appendChild(nodeEl); + let mappingsEl = null; if (node.artmap_mappings && node.artmap_mappings.length > 0 && flowUniverse !== undefined) { const relevantMappings = getRelevantMappings(node.artmap_mappings, flowProtocol, flowUniverse); if (relevantMappings.length > 0) { - const mappingsEl = document.createElement('div'); + mappingsEl = document.createElement('div'); mappingsEl.className = 'flow-artmap-mappings'; + if (isSourceNode) mappingsEl.classList.add('before-node'); + const currentPrefix = flowProtocol + ':' + flowUniverse; + const nodeName = getFirstName(node); relevantMappings.forEach(m => { const mappingEl = document.createElement('div'); mappingEl.className = 'artmap-mapping'; - mappingEl.textContent = m.from + ' → ' + m.to; + const fromSpan = document.createElement('span'); + fromSpan.className = 'from'; + fromSpan.textContent = m.from; + const arrowSpan = document.createElement('span'); + arrowSpan.textContent = '→'; + const toSpan = document.createElement('span'); + toSpan.className = 'to'; + toSpan.textContent = m.to; + mappingEl.appendChild(fromSpan); + mappingEl.appendChild(arrowSpan); + mappingEl.appendChild(toSpan); mappingEl.addEventListener('click', (e) => { e.stopPropagation(); - const toProto = m.to.split(':')[0]; - const toUniverse = parseInt(m.to.split(':')[1], 10); - if (!isNaN(toUniverse)) { - openFlowHash(toProto, toUniverse); + const fromBase = m.from.split(':').slice(0, 2).join(':'); + const target = fromBase === currentPrefix ? m.to : m.from; + const targetProto = target.split(':')[0]; + const targetUniverse = parseInt(target.split(':')[1], 10); + if (!isNaN(targetUniverse)) { + openFlowHash(targetProto, targetUniverse, nodeName); } }); mappingsEl.appendChild(mappingEl); }); - container.appendChild(mappingsEl); } } + + if (mappingsEl && isSourceNode) { + container.appendChild(mappingsEl); + } + container.appendChild(nodeEl); + if (mappingsEl && !isSourceNode) { + container.appendChild(mappingsEl); + } }); return container; diff --git a/static/js/render.js b/static/js/render.js index b6ff871..d4a7f14 100644 --- a/static/js/render.js +++ b/static/js/render.js @@ -123,7 +123,7 @@ export function render(data, config) { const peerName = peerNode ? getFirstName(peerNode) : '??'; const channels = (peer.channels || []).map(formatDanteChannel); const channelSummary = channels.length > 0 ? '\n ' + channels.join('\n ') : ''; - return { text: peerName + channelSummary, peerId: peer.node_id }; + return { text: peerName + channelSummary, peerName }; }); const rxEntries = danteRx.map(peer => { @@ -131,7 +131,7 @@ export function render(data, config) { const peerName = peerNode ? getFirstName(peerNode) : '??'; const channels = (peer.channels || []).map(formatDanteChannel); const channelSummary = channels.length > 0 ? '\n ' + channels.join('\n ') : ''; - return { text: peerName + channelSummary, peerId: peer.node_id }; + return { text: peerName + channelSummary, peerName }; }); txEntries.sort((a, b) => a.text.split('\n')[0].localeCompare(b.text.split('\n')[0])); @@ -141,9 +141,10 @@ export function render(data, config) { isTx: danteTx.length > 0, isRx: danteRx.length > 0, txTo: txEntries.map(e => e.text), - txToPeerIds: txEntries.map(e => e.peerId), + txToPeerNames: txEntries.map(e => e.peerName), rxFrom: rxEntries.map(e => e.text), - rxFromPeerIds: rxEntries.map(e => e.peerId) + rxFromPeerNames: rxEntries.map(e => e.peerName), + nodeName: getFirstName(node) }); }); diff --git a/static/js/table.js b/static/js/table.js index 0974ac0..df01591 100644 --- a/static/js/table.js +++ b/static/js/table.js @@ -1,4 +1,4 @@ -import { getLabel, isSwitch, getInterfaceSpeed, getInterfaceErrors, getInterfaceRates } from './nodes.js'; +import { getLabel, getFirstName, isSwitch, getInterfaceSpeed, getInterfaceErrors, getInterfaceRates } from './nodes.js'; import { buildSwitchUplinks } from './topology.js'; import { escapeHtml, formatUniverse } from './format.js'; import { tableData, tableSortKeys, setTableSortKeys } from './state.js'; @@ -65,9 +65,9 @@ export function renderTable() { sortTable(th.dataset.sort); renderTable(); }); - const sortKey = tableSortKeys.find(k => k.column === th.dataset.sort); - if (sortKey) { - th.classList.add(sortKey.asc ? 'sorted-asc' : 'sorted-desc'); + const primarySort = tableSortKeys[0]; + if (primarySort && primarySort.column === th.dataset.sort) { + th.classList.add(primarySort.asc ? 'sorted-asc' : 'sorted-desc'); } }); } @@ -217,15 +217,19 @@ export function renderDanteTable() { nodes.forEach(node => nodesByTypeId.set(node.id, node)); let rows = []; nodes.forEach(node => { - const name = getLabel(node); + const name = getFirstName(node); + const nameTitle = getLabel(node); const tx = node.dante_flows?.tx || []; tx.forEach(peer => { const peerNode = nodesByTypeId.get(peer.node_id); - const peerName = peerNode ? getLabel(peerNode) : '??'; + const peerName = peerNode ? getFirstName(peerNode) : '??'; + const peerTitle = peerNode ? getLabel(peerNode) : '??'; (peer.channels || []).forEach(ch => { rows.push({ source: name, + sourceTitle: nameTitle, dest: peerName, + destTitle: peerTitle, txChannel: ch.tx_channel, rxChannel: ch.rx_channel, type: ch.type || '', @@ -233,7 +237,7 @@ export function renderDanteTable() { }); }); if (!peer.channels || peer.channels.length === 0) { - rows.push({ source: name, dest: peerName, txChannel: '', rxChannel: 0, type: '', status: 'active' }); + rows.push({ source: name, sourceTitle: nameTitle, dest: peerName, destTitle: peerTitle, txChannel: '', rxChannel: 0, type: '', status: 'active' }); } }); }); @@ -252,9 +256,9 @@ export function renderDanteTable() { rows.forEach(r => { const statusClass = r.status === 'no-source' ? 'status-warn' : 'status-ok'; html += '