179 lines
5.5 KiB
JavaScript
179 lines
5.5 KiB
JavaScript
// Node Discovery Component
|
|
|
|
class NodeDiscovery extends Component {
|
|
constructor(container, viewModel, eventBus) {
|
|
super(container, viewModel, eventBus);
|
|
this.nodes = [];
|
|
this.currentTarget = null;
|
|
}
|
|
|
|
mount() {
|
|
super.mount();
|
|
this.setupEventListeners();
|
|
this.setupViewModelListeners();
|
|
this.loadNodes();
|
|
this.startPeriodicRefresh();
|
|
}
|
|
|
|
setupEventListeners() {
|
|
// Broadcast button
|
|
const broadcastBtn = this.findElement('#broadcast-btn');
|
|
if (broadcastBtn) {
|
|
this.addEventListener(broadcastBtn, 'click', () => {
|
|
this.selectBroadcastTarget();
|
|
});
|
|
}
|
|
}
|
|
|
|
setupViewModelListeners() {
|
|
this.subscribeToEvent('nodeDiscovered', (data) => {
|
|
this.addOrUpdateNode(data.node);
|
|
});
|
|
|
|
this.subscribeToEvent('nodeLost', (data) => {
|
|
this.removeNode(data.node.ip);
|
|
});
|
|
|
|
this.subscribeToEvent('status', (data) => {
|
|
// Update UI to reflect current server state
|
|
if (data.data.nodes) {
|
|
this.nodes = data.data.nodes;
|
|
this.currentTarget = data.data.currentTarget;
|
|
this.renderNodeList();
|
|
}
|
|
});
|
|
}
|
|
|
|
async loadNodes() {
|
|
try {
|
|
const response = await fetch('/api/nodes');
|
|
const data = await response.json();
|
|
|
|
this.nodes = data.nodes || [];
|
|
this.renderNodeList();
|
|
|
|
} catch (error) {
|
|
console.error('Error loading nodes:', error);
|
|
this.showError('Failed to load nodes');
|
|
}
|
|
}
|
|
|
|
startPeriodicRefresh() {
|
|
// Refresh node list every 5 seconds
|
|
setInterval(() => {
|
|
this.loadNodes();
|
|
}, 5000);
|
|
}
|
|
|
|
addOrUpdateNode(node) {
|
|
const existingIndex = this.nodes.findIndex(n => n.ip === node.ip);
|
|
|
|
if (existingIndex >= 0) {
|
|
// Update existing node
|
|
this.nodes[existingIndex] = { ...node, lastSeen: Date.now() };
|
|
} else {
|
|
// Add new node
|
|
this.nodes.push({ ...node, lastSeen: Date.now() });
|
|
}
|
|
|
|
this.renderNodeList();
|
|
}
|
|
|
|
removeNode(nodeIp) {
|
|
this.nodes = this.nodes.filter(node => node.ip !== nodeIp);
|
|
this.renderNodeList();
|
|
}
|
|
|
|
renderNodeList() {
|
|
const nodeListContainer = this.findElement('#node-list');
|
|
if (!nodeListContainer) return;
|
|
|
|
if (this.nodes.length === 0) {
|
|
nodeListContainer.innerHTML = '<div class="empty-state">No nodes discovered</div>';
|
|
return;
|
|
}
|
|
|
|
const html = this.nodes.map(node => `
|
|
<div class="node-item ${node.status} ${node.ip === this.currentTarget ? 'selected' : ''}" data-ip="${this.escapeHtml(node.ip)}" style="cursor: pointer;">
|
|
<div class="node-indicator ${node.status}"></div>
|
|
<div class="node-info">
|
|
<div class="node-ip">${this.escapeHtml(node.ip === 'broadcast' ? 'Broadcast' : node.ip)}</div>
|
|
<div class="node-status">${node.status} • Port ${node.port}</div>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
|
|
nodeListContainer.innerHTML = html;
|
|
|
|
// Add click handlers for node selection
|
|
this.nodes.forEach(node => {
|
|
const nodeElement = nodeListContainer.querySelector(`[data-ip="${node.ip}"]`);
|
|
if (nodeElement) {
|
|
this.addEventListener(nodeElement, 'click', () => {
|
|
this.selectNode(node.ip);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
selectNode(nodeIp) {
|
|
this.currentTarget = nodeIp;
|
|
this.viewModel.publish('selectNode', { nodeIp });
|
|
|
|
// Update visual selection
|
|
const nodeListContainer = this.findElement('#node-list');
|
|
if (nodeListContainer) {
|
|
nodeListContainer.querySelectorAll('.node-item').forEach(item => {
|
|
item.classList.remove('selected');
|
|
});
|
|
const selectedNode = nodeListContainer.querySelector(`[data-ip="${nodeIp}"]`);
|
|
if (selectedNode) {
|
|
selectedNode.classList.add('selected');
|
|
}
|
|
}
|
|
}
|
|
|
|
selectBroadcast() {
|
|
this.currentTarget = 'broadcast';
|
|
this.viewModel.publish('selectBroadcast', {});
|
|
|
|
// Update visual selection
|
|
const nodeListContainer = this.findElement('#node-list');
|
|
if (nodeListContainer) {
|
|
nodeListContainer.querySelectorAll('.node-item').forEach(item => {
|
|
item.classList.remove('selected');
|
|
});
|
|
const broadcastNode = nodeListContainer.querySelector(`[data-ip="broadcast"]`);
|
|
if (broadcastNode) {
|
|
broadcastNode.classList.add('selected');
|
|
}
|
|
}
|
|
}
|
|
|
|
showError(message) {
|
|
const nodeListContainer = this.findElement('#node-list');
|
|
if (nodeListContainer) {
|
|
nodeListContainer.innerHTML = `<div class="error">${this.escapeHtml(message)}</div>`;
|
|
}
|
|
}
|
|
|
|
// Public method to select broadcast (called from outside)
|
|
selectBroadcastTarget() {
|
|
this.selectBroadcast();
|
|
}
|
|
|
|
escapeHtml(str) {
|
|
return String(str)
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
}
|
|
|
|
// Export for use in other modules
|
|
if (typeof module !== 'undefined' && module.exports) {
|
|
module.exports = NodeDiscovery;
|
|
}
|