310 lines
9.5 KiB
JavaScript
310 lines
9.5 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');
|
|
}
|
|
}
|