Extract CSS and JS from index.html into separate ES modules
This commit is contained in:
381
static/js/render.js
Normal file
381
static/js/render.js
Normal file
@@ -0,0 +1,381 @@
|
||||
import { getLabel, getShortLabel, isSwitch, getInterfaceSpeed, getInterfaceErrors, getInterfaceRates } from './nodes.js';
|
||||
import { buildSwitchUplinks, buildLocationTree, buildNodeIndex, findLocationForNode, findEffectiveSwitch } from './topology.js';
|
||||
import { formatUniverse } from './format.js';
|
||||
import { createNodeElement, renderLocation } from './components.js';
|
||||
import { updateErrorPanel, updateBroadcastStats } from './ui.js';
|
||||
import { renderTable } from './table.js';
|
||||
import { showFlowView } from './flow.js';
|
||||
import {
|
||||
nodeElements, locationElements,
|
||||
setUsedNodeIds, setUsedLocationIds, setPortErrors,
|
||||
setTableData, setFlowViewData, currentView,
|
||||
resetAnonCounter
|
||||
} from './state.js';
|
||||
|
||||
export function render(data, config) {
|
||||
resetAnonCounter();
|
||||
|
||||
const nodes = data.nodes || [];
|
||||
const links = data.links || [];
|
||||
|
||||
setPortErrors(data.errors || []);
|
||||
const unreachableNodeIds = new Set(nodes.filter(n => n.unreachable).map(n => n.id));
|
||||
const errorNodeIds = new Set((data.errors || []).filter(e => e.type !== 'unreachable').map(e => e.node_id));
|
||||
|
||||
const locationTree = buildLocationTree(config.locations || [], null);
|
||||
const nodeIndex = new Map();
|
||||
buildNodeIndex(locationTree, nodeIndex);
|
||||
|
||||
const nodesByTypeId = new Map();
|
||||
nodes.forEach(node => {
|
||||
nodesByTypeId.set(node.id, node);
|
||||
});
|
||||
|
||||
const nodeLocations = new Map();
|
||||
const assignedNodes = new Map();
|
||||
const unassignedNodes = [];
|
||||
|
||||
nodes.forEach(node => {
|
||||
const loc = findLocationForNode(node, nodeIndex);
|
||||
if (loc) {
|
||||
nodeLocations.set(node.id, loc);
|
||||
if (!assignedNodes.has(loc)) {
|
||||
assignedNodes.set(loc, []);
|
||||
}
|
||||
assignedNodes.get(loc).push(node);
|
||||
} else {
|
||||
unassignedNodes.push(node);
|
||||
}
|
||||
});
|
||||
|
||||
const switchConnections = new Map();
|
||||
const switchLinks = [];
|
||||
const allSwitches = nodes.filter(n => isSwitch(n));
|
||||
|
||||
links.forEach(link => {
|
||||
const nodeA = nodesByTypeId.get(link.node_a_id);
|
||||
const nodeB = nodesByTypeId.get(link.node_b_id);
|
||||
if (!nodeA || !nodeB) return;
|
||||
|
||||
const aIsSwitch = isSwitch(nodeA);
|
||||
const bIsSwitch = isSwitch(nodeB);
|
||||
|
||||
if (aIsSwitch && bIsSwitch) {
|
||||
switchLinks.push({
|
||||
switchA: nodeA,
|
||||
switchB: nodeB,
|
||||
portA: link.interface_a || '?',
|
||||
portB: link.interface_b || '?',
|
||||
speedA: getInterfaceSpeed(nodeA, link.interface_a),
|
||||
speedB: getInterfaceSpeed(nodeB, link.interface_b),
|
||||
errorsA: getInterfaceErrors(nodeA, link.interface_a),
|
||||
errorsB: getInterfaceErrors(nodeB, link.interface_b),
|
||||
ratesA: getInterfaceRates(nodeA, link.interface_a),
|
||||
ratesB: getInterfaceRates(nodeB, link.interface_b)
|
||||
});
|
||||
} else if (aIsSwitch && !bIsSwitch) {
|
||||
const nodeLoc = nodeLocations.get(nodeB.id);
|
||||
const effectiveSwitch = findEffectiveSwitch(nodeLoc, assignedNodes);
|
||||
const isLocalSwitch = effectiveSwitch && effectiveSwitch.id === nodeA.id;
|
||||
switchConnections.set(nodeB.id, {
|
||||
port: link.interface_a || '?',
|
||||
switchName: getLabel(nodeA),
|
||||
showSwitchName: !isLocalSwitch,
|
||||
external: effectiveSwitch && !isLocalSwitch,
|
||||
speed: getInterfaceSpeed(nodeA, link.interface_a),
|
||||
errors: getInterfaceErrors(nodeA, link.interface_a),
|
||||
rates: getInterfaceRates(nodeA, link.interface_a)
|
||||
});
|
||||
} else if (bIsSwitch && !aIsSwitch) {
|
||||
const nodeLoc = nodeLocations.get(nodeA.id);
|
||||
const effectiveSwitch = findEffectiveSwitch(nodeLoc, assignedNodes);
|
||||
const isLocalSwitch = effectiveSwitch && effectiveSwitch.id === nodeB.id;
|
||||
switchConnections.set(nodeA.id, {
|
||||
port: link.interface_b || '?',
|
||||
switchName: getLabel(nodeB),
|
||||
showSwitchName: !isLocalSwitch,
|
||||
external: effectiveSwitch && !isLocalSwitch,
|
||||
speed: getInterfaceSpeed(nodeB, link.interface_b),
|
||||
errors: getInterfaceErrors(nodeB, link.interface_b),
|
||||
rates: getInterfaceRates(nodeB, link.interface_b)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const danteNodes = new Map();
|
||||
|
||||
const formatDanteChannel = (ch) => {
|
||||
let str = ch.tx_channel + ' → ' + String(ch.rx_channel).padStart(2, '0');
|
||||
if (ch.type) str += ' [' + ch.type + ']';
|
||||
if (ch.status === 'no-source') str += ' ⚠';
|
||||
return str;
|
||||
};
|
||||
|
||||
nodes.forEach(node => {
|
||||
const nodeId = node.id;
|
||||
const danteTx = node.dante_flows?.tx || [];
|
||||
const danteRx = node.dante_flows?.rx || [];
|
||||
|
||||
if (danteTx.length === 0 && danteRx.length === 0) return;
|
||||
|
||||
const txEntries = danteTx.map(peer => {
|
||||
const peerNode = nodesByTypeId.get(peer.node_id);
|
||||
const peerName = peerNode ? getShortLabel(peerNode) : '??';
|
||||
const channels = (peer.channels || []).map(formatDanteChannel);
|
||||
const channelSummary = channels.length > 0 ? '\n ' + channels.join('\n ') : '';
|
||||
return { text: peerName + channelSummary, peerId: peer.node_id };
|
||||
});
|
||||
|
||||
const rxEntries = danteRx.map(peer => {
|
||||
const peerNode = nodesByTypeId.get(peer.node_id);
|
||||
const peerName = peerNode ? getShortLabel(peerNode) : '??';
|
||||
const channels = (peer.channels || []).map(formatDanteChannel);
|
||||
const channelSummary = channels.length > 0 ? '\n ' + channels.join('\n ') : '';
|
||||
return { text: peerName + channelSummary, peerId: peer.node_id };
|
||||
});
|
||||
|
||||
txEntries.sort((a, b) => a.text.split('\n')[0].localeCompare(b.text.split('\n')[0]));
|
||||
rxEntries.sort((a, b) => a.text.split('\n')[0].localeCompare(b.text.split('\n')[0]));
|
||||
|
||||
danteNodes.set(nodeId, {
|
||||
isTx: danteTx.length > 0,
|
||||
isRx: danteRx.length > 0,
|
||||
txTo: txEntries.map(e => e.text),
|
||||
txToPeerIds: txEntries.map(e => e.peerId),
|
||||
rxFrom: rxEntries.map(e => e.text),
|
||||
rxFromPeerIds: rxEntries.map(e => e.peerId)
|
||||
});
|
||||
});
|
||||
|
||||
const artnetNodes = new Map();
|
||||
|
||||
const universeInputs = new Map();
|
||||
const universeOutputs = new Map();
|
||||
|
||||
nodes.forEach(node => {
|
||||
const name = getShortLabel(node);
|
||||
(node.artnet_inputs || []).forEach(u => {
|
||||
if (!universeInputs.has(u)) universeInputs.set(u, []);
|
||||
universeInputs.get(u).push(name);
|
||||
});
|
||||
(node.artnet_outputs || []).forEach(u => {
|
||||
if (!universeOutputs.has(u)) universeOutputs.set(u, []);
|
||||
universeOutputs.get(u).push(name);
|
||||
});
|
||||
});
|
||||
|
||||
const collapseNames = (names) => {
|
||||
const counts = {};
|
||||
names.forEach(n => counts[n] = (counts[n] || 0) + 1);
|
||||
return Object.entries(counts).map(([name, count]) => count > 1 ? name + ' x' + count : name);
|
||||
};
|
||||
|
||||
nodes.forEach(node => {
|
||||
const nodeId = node.id;
|
||||
const artnetInputs = node.artnet_inputs || [];
|
||||
const artnetOutputs = node.artnet_outputs || [];
|
||||
|
||||
if (artnetInputs.length === 0 && artnetOutputs.length === 0) return;
|
||||
|
||||
const sortedInputs = artnetInputs.slice().sort((a, b) => a - b);
|
||||
const sortedOutputs = artnetOutputs.slice().sort((a, b) => a - b);
|
||||
|
||||
const inputs = sortedInputs.map(u => {
|
||||
const sources = collapseNames(universeOutputs.get(u) || []);
|
||||
const uniStr = formatUniverse(u);
|
||||
if (sources.length > 0) {
|
||||
return { display: sources[0] + ' [' + uniStr + ']', firstTarget: sources[0], universe: u };
|
||||
}
|
||||
return { display: uniStr, firstTarget: null, universe: u };
|
||||
});
|
||||
const outputs = sortedOutputs.map(u => {
|
||||
const dests = collapseNames(universeInputs.get(u) || []);
|
||||
const uniStr = formatUniverse(u);
|
||||
if (dests.length > 0) {
|
||||
return { display: dests[0] + ' [' + uniStr + ']', firstTarget: dests[0], universe: u };
|
||||
}
|
||||
return { display: uniStr, firstTarget: null, universe: u };
|
||||
});
|
||||
|
||||
artnetNodes.set(nodeId, {
|
||||
isOut: outputs.length > 0,
|
||||
isIn: inputs.length > 0,
|
||||
outputs: outputs,
|
||||
inputs: inputs
|
||||
});
|
||||
});
|
||||
|
||||
const sacnNodes = new Map();
|
||||
|
||||
const sacnUniverseInputs = new Map();
|
||||
const sacnUniverseOutputs = new Map();
|
||||
|
||||
function getSacnInputsFromMulticast(node) {
|
||||
const groups = node.multicast_groups || [];
|
||||
const inputs = [];
|
||||
groups.forEach(g => {
|
||||
if (typeof g === 'string' && g.startsWith('sacn:')) {
|
||||
const u = parseInt(g.substring(5), 10);
|
||||
if (!isNaN(u)) inputs.push(u);
|
||||
}
|
||||
});
|
||||
return inputs;
|
||||
}
|
||||
|
||||
nodes.forEach(node => {
|
||||
const name = getShortLabel(node);
|
||||
getSacnInputsFromMulticast(node).forEach(u => {
|
||||
if (!sacnUniverseInputs.has(u)) sacnUniverseInputs.set(u, []);
|
||||
sacnUniverseInputs.get(u).push(name);
|
||||
});
|
||||
(node.sacn_outputs || []).forEach(u => {
|
||||
if (!sacnUniverseOutputs.has(u)) sacnUniverseOutputs.set(u, []);
|
||||
sacnUniverseOutputs.get(u).push(name);
|
||||
});
|
||||
});
|
||||
|
||||
const sacnCollapseNames = (names) => {
|
||||
const counts = {};
|
||||
names.forEach(n => counts[n] = (counts[n] || 0) + 1);
|
||||
return Object.entries(counts).map(([name, count]) => count > 1 ? name + ' x' + count : name);
|
||||
};
|
||||
|
||||
nodes.forEach(node => {
|
||||
const nodeId = node.id;
|
||||
const sacnInputs = getSacnInputsFromMulticast(node);
|
||||
const sacnOutputs = node.sacn_outputs || [];
|
||||
|
||||
if (sacnInputs.length === 0 && sacnOutputs.length === 0) return;
|
||||
|
||||
const sortedSacnInputs = sacnInputs.slice().sort((a, b) => a - b);
|
||||
const sortedSacnOutputs = sacnOutputs.slice().sort((a, b) => a - b);
|
||||
|
||||
const inputs = sortedSacnInputs.map(u => {
|
||||
const sources = sacnCollapseNames(sacnUniverseOutputs.get(u) || []);
|
||||
if (sources.length > 0) {
|
||||
return { display: sources[0] + ' [' + u + ']', firstTarget: sources[0], universe: u };
|
||||
}
|
||||
return { display: String(u), firstTarget: null, universe: u };
|
||||
});
|
||||
const outputs = sortedSacnOutputs.map(u => {
|
||||
const dests = sacnCollapseNames(sacnUniverseInputs.get(u) || []);
|
||||
if (dests.length > 0) {
|
||||
return { display: dests[0] + ' [' + u + ']', firstTarget: dests[0], universe: u };
|
||||
}
|
||||
return { display: String(u), firstTarget: null, universe: u };
|
||||
});
|
||||
|
||||
sacnNodes.set(nodeId, {
|
||||
isOut: outputs.length > 0,
|
||||
isIn: inputs.length > 0,
|
||||
outputs: outputs,
|
||||
inputs: inputs
|
||||
});
|
||||
});
|
||||
|
||||
const switchUplinks = buildSwitchUplinks(allSwitches, switchLinks);
|
||||
|
||||
const container = document.getElementById('container');
|
||||
const usedNodeIdsSet = new Set();
|
||||
const usedLocationIdsSet = new Set();
|
||||
|
||||
locationTree.forEach(loc => {
|
||||
const el = renderLocation(loc, assignedNodes, true, switchConnections, switchUplinks, danteNodes, artnetNodes, sacnNodes, errorNodeIds, unreachableNodeIds, usedNodeIdsSet, usedLocationIdsSet);
|
||||
if (el && el.parentNode !== container) container.appendChild(el);
|
||||
});
|
||||
|
||||
let unassignedLoc = locationElements.get('__unassigned__');
|
||||
if (unassignedNodes.length > 0) {
|
||||
if (!unassignedLoc) {
|
||||
unassignedLoc = document.createElement('div');
|
||||
unassignedLoc.className = 'location top-level';
|
||||
const nameEl = document.createElement('div');
|
||||
nameEl.className = 'location-name';
|
||||
nameEl.textContent = 'Unassigned';
|
||||
unassignedLoc.appendChild(nameEl);
|
||||
locationElements.set('__unassigned__', unassignedLoc);
|
||||
}
|
||||
|
||||
const switches = unassignedNodes.filter(n => isSwitch(n));
|
||||
const nonSwitches = unassignedNodes.filter(n => !isSwitch(n));
|
||||
|
||||
let switchRow = unassignedLoc.querySelector(':scope > .node-row.switch-row');
|
||||
if (switches.length > 0) {
|
||||
if (!switchRow) {
|
||||
switchRow = document.createElement('div');
|
||||
switchRow.className = 'node-row switch-row';
|
||||
unassignedLoc.appendChild(switchRow);
|
||||
}
|
||||
const currentIds = new Set(switches.map(n => n.id));
|
||||
Array.from(switchRow.children).forEach(ch => {
|
||||
if (!currentIds.has(ch.dataset.id)) ch.remove();
|
||||
});
|
||||
switches.forEach(node => {
|
||||
usedNodeIdsSet.add(node.id);
|
||||
const uplink = switchUplinks.get(node.id);
|
||||
const danteInfo = danteNodes.get(node.id);
|
||||
const artnetInfo = artnetNodes.get(node.id);
|
||||
const sacnInfo = sacnNodes.get(node.id);
|
||||
const hasError = errorNodeIds.has(node.id);
|
||||
const isUnreachable = unreachableNodeIds.has(node.id);
|
||||
const el = createNodeElement(node, null, null, uplink, danteInfo, artnetInfo, sacnInfo, hasError, isUnreachable);
|
||||
if (el.parentNode !== switchRow) switchRow.appendChild(el);
|
||||
});
|
||||
} else if (switchRow) {
|
||||
switchRow.remove();
|
||||
}
|
||||
|
||||
let nodeRow = unassignedLoc.querySelector(':scope > .node-row:not(.switch-row)');
|
||||
if (nonSwitches.length > 0) {
|
||||
if (!nodeRow) {
|
||||
nodeRow = document.createElement('div');
|
||||
nodeRow.className = 'node-row';
|
||||
unassignedLoc.appendChild(nodeRow);
|
||||
}
|
||||
const currentIds = new Set(nonSwitches.map(n => n.id));
|
||||
Array.from(nodeRow.children).forEach(ch => {
|
||||
if (!currentIds.has(ch.dataset.id)) ch.remove();
|
||||
});
|
||||
nonSwitches.forEach(node => {
|
||||
usedNodeIdsSet.add(node.id);
|
||||
const conn = switchConnections.get(node.id);
|
||||
const danteInfo = danteNodes.get(node.id);
|
||||
const artnetInfo = artnetNodes.get(node.id);
|
||||
const sacnInfo = sacnNodes.get(node.id);
|
||||
const hasError = errorNodeIds.has(node.id);
|
||||
const isUnreachable = unreachableNodeIds.has(node.id);
|
||||
const el = createNodeElement(node, conn, null, null, danteInfo, artnetInfo, sacnInfo, hasError, isUnreachable);
|
||||
if (el.parentNode !== nodeRow) nodeRow.appendChild(el);
|
||||
});
|
||||
} else if (nodeRow) {
|
||||
nodeRow.remove();
|
||||
}
|
||||
|
||||
if (unassignedLoc.parentNode !== container) container.appendChild(unassignedLoc);
|
||||
usedLocationIdsSet.add('__unassigned__');
|
||||
} else if (unassignedLoc) {
|
||||
unassignedLoc.remove();
|
||||
}
|
||||
|
||||
setUsedNodeIds(usedNodeIdsSet);
|
||||
setUsedLocationIds(usedLocationIdsSet);
|
||||
|
||||
locationElements.forEach((el, id) => {
|
||||
if (!usedLocationIdsSet.has(id) && el.parentNode) {
|
||||
el.remove();
|
||||
}
|
||||
});
|
||||
|
||||
updateErrorPanel();
|
||||
updateBroadcastStats(data.broadcast_stats);
|
||||
|
||||
setTableData(data);
|
||||
setFlowViewData(data);
|
||||
if (currentView === 'table') {
|
||||
renderTable();
|
||||
}
|
||||
const hash = window.location.hash;
|
||||
if (hash.startsWith('#flow/')) {
|
||||
showFlowView(hash.slice(6));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user