Files
vna_system/vna_system/web_ui/static/js/modules/ui.js
2025-09-30 14:15:25 +03:00

310 lines
9.4 KiB
JavaScript

/**
* UI Manager
* Handles user interface interactions and state management
*/
import { formatProcessorName, debounce } from './utils.js';
export class UIManager {
constructor(notifications, websocket, charts) {
this.notifications = notifications;
this.websocket = websocket; // injected WebSocketManager
this.charts = charts; // injected ChartManager
// UI Elements
this.elements = {
connectionStatus: null,
processorToggles: null,
sweepCount: null,
dataRate: null,
navButtons: null,
views: null
};
// State
this.currentView = 'dashboard';
this.connectionStatus = 'disconnected';
// processorId -> { enabled, uiParameters, config }
this.processors = new Map();
// Event handlers
this.eventHandlers = {
viewChange: [],
processorToggle: [],
};
}
/**
* Initialize UI Manager
*/
async init() {
console.log('Initializing UI Manager...');
// Get DOM elements
this.findElements();
// Set up event listeners
this.setupEventListeners();
// Initialize UI state
this.initializeUIState();
// Wire WebSocket events
if (this.websocket) {
this.websocket.on('connecting', () => this.setConnectionStatus('connecting'));
this.websocket.on('connected', () => this.setConnectionStatus('connected'));
this.websocket.on('disconnected', () => this.setConnectionStatus('disconnected'));
// main data stream from backend
this.websocket.on('processor_result', (payload) => this.onProcessorResult(payload));
}
console.log('UI Manager initialized');
}
/**
* Find and cache DOM elements
*/
findElements() {
this.elements.connectionStatus = document.getElementById('connectionStatus');
this.elements.processorToggles = document.getElementById('processorToggles');
this.elements.navButtons = document.querySelectorAll('.nav-btn[data-view]');
this.elements.views = document.querySelectorAll('.view');
// Validate required elements
const required = ['connectionStatus', 'processorToggles'];
for (const key of required) {
if (!this.elements[key]) {
throw new Error(`Required UI element not found: ${key}`);
}
}
}
/**
* Set up event listeners
*/
setupEventListeners() {
// Navigation
this.elements.navButtons.forEach(button => {
button.addEventListener('click', (e) => {
const view = e.currentTarget.dataset.view;
this.switchView(view);
});
});
// Processor toggles container (event delegation)
this.elements.processorToggles.addEventListener('click', (e) => {
const toggle = e.target.closest('.processor-toggle');
if (!toggle) return;
const processor = toggle.dataset.processor;
const isEnabled = toggle.classList.contains('processor-toggle--active');
this.toggleProcessor(processor, !isEnabled);
});
// Window resize
window.addEventListener('resize', debounce(() => this.handleResize(), 300));
}
/**
* Initialize UI state
*/
initializeUIState() {
this.setConnectionStatus('disconnected');
this.switchView(this.currentView);
}
/**
* Switch between views
*/
switchView(viewName) {
if (this.currentView === viewName) return;
this.elements.navButtons.forEach(button => {
const isActive = button.dataset.view === viewName;
button.classList.toggle('nav-btn--active', isActive);
});
this.elements.views.forEach(view => {
const isActive = view.id === `${viewName}View`;
view.classList.toggle('view--active', isActive);
});
this.currentView = viewName;
this.emitEvent('viewChange', viewName);
}
/**
* Update connection status
*/
setConnectionStatus(status) {
if (this.connectionStatus === status) return;
this.connectionStatus = status;
const statusElement = this.elements.connectionStatus;
const textElement = statusElement.querySelector('.status-indicator__text');
// Remove existing status classes
statusElement.classList.remove(
'status-indicator--connected',
'status-indicator--connecting',
'status-indicator--disconnected'
);
// Add new status class and update text
switch (status) {
case 'connected':
statusElement.classList.add('status-indicator--connected');
textElement.textContent = 'Connected';
break;
case 'connecting':
statusElement.classList.add('status-indicator--connecting');
textElement.textContent = 'Connecting...';
break;
case 'disconnected':
default:
statusElement.classList.add('status-indicator--disconnected');
textElement.textContent = 'Disconnected';
break;
}
}
/**
* Handle incoming processor result from backend
*/
onProcessorResult(payload) {
const { processor_id, timestamp, data, plotly_config, ui_parameters, metadata } = payload;
if (!processor_id) return;
// Register/update processor in UI map
const proc = this.processors.get(processor_id) || { enabled: true };
proc.uiParameters = ui_parameters || [];
proc.config = metadata?.config || {};
this.processors.set(processor_id, proc);
// Refresh toggles
this.refreshProcessorToggles();
// Pass to charts
if (this.charts) {
this.charts.addResult({
processor_id,
timestamp,
data,
plotly_config,
metadata
});
}
}
/**
* Rebuild processor toggles
*/
refreshProcessorToggles() {
const container = this.elements.processorToggles;
if (!container) return;
container.innerHTML = '';
for (const [name, data] of this.processors.entries()) {
const toggle = document.createElement('div');
toggle.className = `processor-toggle ${data.enabled ? 'processor-toggle--active' : ''}`;
toggle.dataset.processor = name;
toggle.innerHTML = `
<div class="processor-toggle__checkbox"></div>
<div class="processor-toggle__label">${formatProcessorName(name)}</div>
`;
container.appendChild(toggle);
}
if (typeof lucide !== 'undefined') {
lucide.createIcons({ attrs: { 'stroke-width': 1.5 } });
}
}
/**
* Toggle processor visibility (UI-only)
*/
toggleProcessor(processorId, enabled) {
const proc = this.processors.get(processorId) || { enabled: true };
proc.enabled = enabled;
this.processors.set(processorId, proc);
// Update toggle UI
const toggle = this.elements.processorToggles.querySelector(`[data-processor="${processorId}"]`);
if (toggle) {
toggle.classList.toggle('processor-toggle--active', enabled);
} else {
this.refreshProcessorToggles();
}
// Update charts
if (this.charts) {
this.charts.toggleProcessor(processorId, enabled);
}
this.emitEvent('processorToggle', processorId, enabled);
}
/**
* Public API
*/
setProcessorEnabled(processorId, enabled) {
if (!processorId) return;
if (!this.processors.has(processorId)) {
const processor = { enabled };
this.processors.set(processorId, processor);
return;
}
this.toggleProcessor(processorId, enabled);
}
handleResize() {
console.log('Window resized');
// Charts handle their own resize
}
// System status and theme methods simplified
updateSystemStatus(statusData) { console.log('System status:', statusData); }
setTheme(theme) { document.documentElement.setAttribute('data-theme', theme); }
getCurrentTheme() { return document.documentElement.getAttribute('data-theme') || 'dark'; }
// Events
onViewChange(callback) { this.eventHandlers.viewChange.push(callback); }
onProcessorToggle(callback) { this.eventHandlers.processorToggle.push(callback); }
emitEvent(eventType, ...args) {
if (this.eventHandlers[eventType]) {
this.eventHandlers[eventType].forEach(handler => {
try { handler(...args); }
catch (error) { console.error(` Error in ${eventType} handler:`, error); }
});
}
}
getStats() {
return {
currentView: this.currentView,
connectionStatus: this.connectionStatus,
processorsCount: this.processors.size
};
}
destroy() {
console.log('Cleaning up UI Manager...');
// Clear event handlers
Object.keys(this.eventHandlers).forEach(key => {
this.eventHandlers[key] = [];
});
// Clear processors
this.processors.clear();
console.log('UI Manager cleanup complete');
}
}