diff options
Diffstat (limited to 'static/app.js')
| -rw-r--r-- | static/app.js | 605 |
1 files changed, 385 insertions, 220 deletions
diff --git a/static/app.js b/static/app.js index 8a04c45..ca205f3 100644 --- a/static/app.js +++ b/static/app.js @@ -1,234 +1,399 @@ /* jshint esversion: 2024, module: true */ -import { ApiClient } from "./api-client.js"; -import { ConfigManager } from "./config-manager.js"; -import { InterfaceRenderer } from "./interface-renderer.js"; -import { ThemeManager } from "./theme-manager.js"; +import { ThemeManager } from './theme-manager.js'; +import { ApiClient } from './api-client.js'; +import { InterfaceRenderer } from './interface-renderer.js'; +import { ConfigManager } from './config-manager.js'; +import { StructuredEditor } from './structured-editor.js'; /** * Main Application Class * @class Application */ class Application { - /** - * @param {Object} elements - DOM elements - */ - constructor(elements) { - this.elements = elements; - this.state = { - currentInterface: null, - interfaces: [], - editorMode: "raw", - }; - - // Initialize modules - this.themeManager = new ThemeManager(elements); - this.apiClient = new ApiClient(); - this.interfaceRenderer = new InterfaceRenderer(elements, this.state); - this.configManager = new ConfigManager( - elements, - this.apiClient, - this.state, - ); - } - - /** - * Initialize the application - * @method init - */ - init() { - this.themeManager.init(); - this.setupEventListeners(); - this.loadStatus(); - } - - /** - * Set up all event listeners - * @method setupEventListeners - */ - setupEventListeners() { - // Navigation - this.elements.buttons.nav.forEach((button) => { - button.addEventListener("click", (event) => { - this.show(event.currentTarget.dataset.panel); - }); - }); - - // Status panel - this.elements.buttons.refreshStatus?.addEventListener("click", () => - this.loadStatus(), - ); - - // Configs panel - delegated to ConfigManager - this.configManager.setupEventListeners(); - - // Logs panel - this.elements.buttons.refreshLogs?.addEventListener("click", () => - this.loadLogs(), - ); - - // Commands panel - this.elements.buttons.restartNetworkd?.addEventListener("click", () => - this.restartNetworkd(), - ); - this.elements.buttons.rebootDevice?.addEventListener("click", () => - this.rebootDevice(), - ); - - // Touch support - document.addEventListener("touchstart", this.handleTouchStart, { - passive: true, - }); - } - - /** - * Handle touch events for better mobile support - * @method handleTouchStart - * @param {TouchEvent} event - */ - handleTouchStart = (event) => { - // Add visual feedback for touch - if ( - event.target.classList.contains("button") || - event.target.classList.contains("nav-button") - ) { - event.target.style.opacity = "0.7"; - setTimeout(() => { - event.target.style.opacity = ""; - }, 150); - } - }; - - /** - * Show specified panel and hide others - * @method show - * @param {string} panel - Panel to show - */ - show(panel) { - // Hide all panels and remove active class from buttons - Object.values(this.elements.panels).forEach((p) => - p?.classList.remove("active"), - ); - this.elements.buttons.nav.forEach((btn) => btn?.classList.remove("active")); - - // Show selected panel and activate button - this.elements.panels[panel]?.classList.add("active"); - document.querySelector(`[data-panel="${panel}"]`)?.classList.add("active"); - - // Load panel-specific data - const panelActions = { - status: () => this.loadStatus(), - configs: () => this.configManager.refreshConfigs(), - logs: () => this.loadLogs(), - }; - - panelActions[panel]?.(); - } - - /** - * Load and display network status - * @method loadStatus - */ - async loadStatus() { - try { - const data = await this.apiClient.get("/api/status"); - this.state.interfaces = data.Interfaces ?? []; - this.interfaceRenderer.renderInterfaceTabs(this.state.interfaces); - - // Show first interface by default - if (this.state.interfaces.length > 0 && !this.state.currentInterface) { - this.interfaceRenderer.showInterfaceDetails(this.state.interfaces[0]); - } - } catch (error) { - this.elements.outputs.ifaceDetails.innerHTML = `<div class="error-message">Error loading status: ${error.message}</div>`; - } - } - - /** - * Load system logs - * @method loadLogs - */ - async loadLogs() { - try { - const text = await this.apiClient.getText("/api/logs"); - this.elements.outputs.logsArea.textContent = text; - } catch (error) { - this.elements.outputs.logsArea.textContent = `Error: ${error.message}`; - } - } - - /** - * Restart networkd service - * @method restartNetworkd - */ - async restartNetworkd() { - if (!confirm("Restart systemd-networkd? Active connections may be reset.")) - return; - - try { - const result = await this.apiClient.post("/api/reload"); - this.elements.outputs.cmdResult.textContent = `Success: ${JSON.stringify(result)}`; - } catch (error) { - this.elements.outputs.cmdResult.textContent = `Error: ${error.message}`; - } - } - - /** - * Reboot the device - * @method rebootDevice - */ - async rebootDevice() { - if (!confirm("Reboot device now?")) return; - - try { - const result = await this.apiClient.post("/api/reboot"); - this.elements.outputs.cmdResult.textContent = `Success: ${JSON.stringify(result)}`; - } catch (error) { - this.elements.outputs.cmdResult.textContent = `Error: ${error.message}`; - } - } + /** + * @param {Object} elements - DOM elements + */ + constructor(elements) { + this.elements = elements; + this.state = { + currentInterface: null, + interfaces: [], + editorMode: 'raw', // 'raw' or 'structured' + currentConfigFile: null + }; + + // Initialize modules + this.themeManager = new ThemeManager(elements); + this.apiClient = new ApiClient(); + this.interfaceRenderer = new InterfaceRenderer(elements, this.state); + this.configManager = new ConfigManager(elements, this.apiClient, this.state); + + // Structured editor will be initialized after DOM is ready + this.structuredEditor = null; + + // Create editor mode toggle UI + this.createEditorModeToggle(); + } + + /** + * Initialize the application + * @method init + */ + init() { + this.themeManager.init(); + this.setupEventListeners(); + this.loadStatus(); + + // Initialize structured editor now that DOM is ready + this.initializeStructuredEditor(); + } + + /** + * Create editor mode toggle UI + * @method createEditorModeToggle + */ + createEditorModeToggle() { + const editorToggleHTML = ` + <div class="editor-mode-toggle" style="margin-bottom: var(--spacing-l);"> + <button class="button small ${this.state.editorMode === 'raw' ? 'active' : ''}" + data-mode="raw" id="rawEditorBtn"> + 📝 Raw Editor + </button> + <button class="button small ${this.state.editorMode === 'structured' ? 'active' : ''}" + data-mode="structured" id="structuredEditorBtn"> + 🏗️ Structured Editor + </button> + </div> + <div id="rawEditorContainer"> + <!-- Existing raw editor will go here --> + </div> + <div id="structuredEditorContainer" style="display: none;"> + <!-- Structured editor will be rendered here --> + </div> + `; + + // Insert the toggle and containers into the configs panel + const configCard = this.elements.panels.configs.querySelector('.card:last-child'); + configCard.insertAdjacentHTML('afterbegin', editorToggleHTML); + + // Move existing form elements to raw editor container + const rawEditorContainer = document.getElementById('rawEditorContainer'); + const formGroups = Array.from(configCard.querySelectorAll('.form-group, .checkbox-group')); + const configActions = configCard.querySelector('.config-actions') || + configCard.querySelector('div:has(> #validateConfig)'); + + formGroups.forEach(group => { + if (!group.closest('.editor-mode-toggle')) { + rawEditorContainer.appendChild(group); + } + }); + + // Move config actions if they exist + if (configActions) { + rawEditorContainer.appendChild(configActions); + } + + // Update elements reference + this.elements.editorContainers = { + raw: document.getElementById('rawEditorContainer'), + structured: document.getElementById('structuredEditorContainer') + }; + + this.elements.editorButtons = { + raw: document.getElementById('rawEditorBtn'), + structured: document.getElementById('structuredEditorBtn') + }; + } + + /** + * Initialize structured editor + * @method initializeStructuredEditor + */ + initializeStructuredEditor() { + if (this.elements.editorContainers.structured) { + this.structuredEditor = new StructuredEditor(this.elements.editorContainers.structured); + } else { + console.error('Structured editor container not found'); + } + } + + /** + * Set up all event listeners + * @method setupEventListeners + */ + setupEventListeners() { + // Navigation + this.elements.buttons.nav.forEach(button => { + button.addEventListener('click', (event) => { + this.show(event.currentTarget.dataset.panel); + }); + }); + + // Status panel + this.elements.buttons.refreshStatus?.addEventListener('click', () => this.loadStatus()); + + // Configs panel - raw editor + this.elements.buttons.refreshConfigs?.addEventListener('click', + () => this.configManager.refreshConfigs()); + this.elements.buttons.saveConfig?.addEventListener('click', + () => this.handleSaveConfig()); + this.elements.buttons.validateConfig?.addEventListener('click', + () => this.configManager.validateConfig()); + this.elements.inputs.configSelect?.addEventListener('change', + () => this.handleConfigFileChange()); + + // Editor mode toggle + this.elements.editorButtons?.raw?.addEventListener('click', + () => this.setEditorMode('raw')); + this.elements.editorButtons?.structured?.addEventListener('click', + () => this.setEditorMode('structured')); + + // Logs panel + this.elements.buttons.refreshLogs?.addEventListener('click', () => this.loadLogs()); + + // Commands panel + this.elements.buttons.restartNetworkd?.addEventListener('click', () => this.restartNetworkd()); + this.elements.buttons.rebootDevice?.addEventListener('click', () => this.rebootDevice()); + + // Touch support + document.addEventListener('touchstart', this.handleTouchStart, { passive: true }); + } + + /** + * Handle configuration file change + * @method handleConfigFileChange + */ + async handleConfigFileChange() { + const name = this.elements.inputs.configSelect.value; + if (!name) return; + + this.state.currentConfigFile = name; + + if (this.state.editorMode === 'raw') { + await this.configManager.loadConfig(); + } else { + await this.loadConfigForStructuredEditor(); + } + } + + /** + * Load config for structured editor + * @method loadConfigForStructuredEditor + */ + async loadConfigForStructuredEditor() { + if (!this.structuredEditor) { + console.error('Structured editor not initialized'); + return; + } + + try { + const text = await this.apiClient.getText(`/api/config/${encodeURIComponent(this.state.currentConfigFile)}`); + await this.structuredEditor.loadConfiguration(text, this.state.currentConfigFile); + } catch (error) { + alert(`Failed to load config for structured editor: ${error.message}`); + } + } + + /** + * Set editor mode (raw or structured) + * @method setEditorMode + * @param {string} mode - Editor mode + */ + setEditorMode(mode) { + this.state.editorMode = mode; + + // Update UI + if (this.elements.editorButtons?.raw && this.elements.editorButtons?.structured) { + this.elements.editorButtons.raw.classList.toggle('active', mode === 'raw'); + this.elements.editorButtons.structured.classList.toggle('active', mode === 'structured'); + } + + if (this.elements.editorContainers?.raw && this.elements.editorContainers?.structured) { + this.elements.editorContainers.raw.style.display = mode === 'raw' ? 'block' : 'none'; + this.elements.editorContainers.structured.style.display = mode === 'structured' ? 'block' : 'none'; + } + + // If switching to structured mode and we have a config file loaded, load it + if (mode === 'structured' && this.state.currentConfigFile && this.structuredEditor) { + this.loadConfigForStructuredEditor(); + } + } + + /** + * Handle save configuration based on current editor mode + * @method handleSaveConfig + */ + async handleSaveConfig() { + const name = this.state.currentConfigFile; + if (!name) { + alert('Please select a configuration file first.'); + return; + } + + const restart = this.elements.inputs.restartAfterSave.checked; + + if (!confirm(`Save file ${name}? This will create a backup and ${restart ? 'restart' : 'not restart'} networkd.`)) { + return; + } + + try { + let content; + if (this.state.editorMode === 'raw') { + content = this.elements.inputs.cfgEditor.value; + } else if (this.structuredEditor) { + content = this.structuredEditor.getConfigurationText(); + } else { + throw new Error('Structured editor not available'); + } + + const result = await this.apiClient.post('/api/save', { name, content, restart }); + alert(`Saved: ${result.status ?? 'ok'}`); + + // Refresh the config in structured editor if needed + if (this.state.editorMode === 'structured' && this.structuredEditor) { + await this.structuredEditor.loadConfiguration(content, name); + } + } catch (error) { + alert(`Save failed: ${error.message}`); + } + } + + /** + * Show specified panel and hide others + * @method show + * @param {string} panel - Panel to show + */ + show(panel) { + // Hide all panels and remove active class from buttons + Object.values(this.elements.panels).forEach(p => { + if (p) p.classList.remove('active'); + }); + this.elements.buttons.nav.forEach(btn => { + if (btn) btn.classList.remove('active'); + }); + + // Show selected panel and activate button + const targetPanel = this.elements.panels[panel]; + const targetButton = document.querySelector(`[data-panel="${panel}"]`); + + if (targetPanel) targetPanel.classList.add('active'); + if (targetButton) targetButton.classList.add('active'); + + // Load panel-specific data + const panelActions = { + status: () => this.loadStatus(), + configs: () => this.configManager.refreshConfigs(), + logs: () => this.loadLogs(), + }; + + panelActions[panel]?.(); + } + + /** + * Load and display network status + * @method loadStatus + */ + async loadStatus() { + try { + const data = await this.apiClient.get('/api/status'); + this.state.interfaces = data.Interfaces ?? []; + this.interfaceRenderer.renderInterfaceTabs(this.state.interfaces); + + // Show first interface by default + if (this.state.interfaces.length > 0 && !this.state.currentInterface) { + this.interfaceRenderer.showInterfaceDetails(this.state.interfaces[0]); + } + } catch (error) { + this.elements.outputs.ifaceDetails.innerHTML = + `<div class="error-message">Error loading status: ${error.message}</div>`; + } + } + + /** + * Load system logs + * @method loadLogs + */ + async loadLogs() { + try { + const text = await this.apiClient.getText('/api/logs'); + this.elements.outputs.logsArea.textContent = text; + } catch (error) { + this.elements.outputs.logsArea.textContent = `Error: ${error.message}`; + } + } + + /** + * Restart networkd service + * @method restartNetworkd + */ + async restartNetworkd() { + if (!confirm('Restart systemd-networkd? Active connections may be reset.')) return; + + try { + const result = await this.apiClient.post('/api/reload'); + this.elements.outputs.cmdResult.textContent = `Success: ${JSON.stringify(result)}`; + } catch (error) { + this.elements.outputs.cmdResult.textContent = `Error: ${error.message}`; + } + } + + /** + * Reboot the device + * @method rebootDevice + */ + async rebootDevice() { + if (!confirm('Reboot device now?')) return; + + try { + const result = await this.apiClient.post('/api/reboot'); + this.elements.outputs.cmdResult.textContent = `Success: ${JSON.stringify(result)}`; + } catch (error) { + this.elements.outputs.cmdResult.textContent = `Error: ${error.message}`; + } + } } // Initialize application when DOM is loaded -document.addEventListener("DOMContentLoaded", () => { - const elements = { - themeToggle: document.getElementById("themeToggle"), - themeIcon: document.getElementById("themeIcon"), - panels: { - status: document.getElementById("panelStatus"), - configs: document.getElementById("panelConfigs"), - logs: document.getElementById("panelLogs"), - commands: document.getElementById("panelCommands"), - }, - buttons: { - nav: document.querySelectorAll(".nav-button"), - refreshStatus: document.getElementById("refreshStatus"), - refreshConfigs: document.getElementById("refreshConfigs"), - saveConfig: document.getElementById("saveConfig"), - validateConfig: document.getElementById("validateConfig"), - refreshLogs: document.getElementById("refreshLogs"), - restartNetworkd: document.getElementById("restartNetworkd"), - rebootDevice: document.getElementById("rebootDevice"), - }, - inputs: { - configSelect: document.getElementById("configSelect"), - cfgEditor: document.getElementById("cfgEditor"), - restartAfterSave: document.getElementById("restartAfterSave"), - }, - outputs: { - ifaceTabs: document.getElementById("interfaceTabs"), - ifaceDetails: document.getElementById("interfaceDetails"), - validateResult: document.getElementById("validateResult"), - logsArea: document.getElementById("logsArea"), - cmdResult: document.getElementById("cmdResult"), - }, - }; - - const app = new Application(elements); - app.init(); - - // Make app globally available for debugging - window.app = app; +document.addEventListener('DOMContentLoaded', () => { + const elements = { + themeToggle: document.getElementById('themeToggle'), + themeIcon: document.getElementById('themeIcon'), + panels: { + status: document.getElementById('panelStatus'), + configs: document.getElementById('panelConfigs'), + logs: document.getElementById('panelLogs'), + commands: document.getElementById('panelCommands'), + }, + buttons: { + nav: document.querySelectorAll('.nav-button'), + refreshStatus: document.getElementById('refreshStatus'), + refreshConfigs: document.getElementById('refreshConfigs'), + saveConfig: document.getElementById('saveConfig'), + validateConfig: document.getElementById('validateConfig'), + refreshLogs: document.getElementById('refreshLogs'), + restartNetworkd: document.getElementById('restartNetworkd'), + rebootDevice: document.getElementById('rebootDevice'), + }, + inputs: { + configSelect: document.getElementById('configSelect'), + cfgEditor: document.getElementById('cfgEditor'), + restartAfterSave: document.getElementById('restartAfterSave'), + }, + outputs: { + ifaceTabs: document.getElementById('interfaceTabs'), + ifaceDetails: document.getElementById('interfaceDetails'), + validateResult: document.getElementById('validateResult'), + logsArea: document.getElementById('logsArea'), + cmdResult: document.getElementById('cmdResult'), + }, + }; + + const app = new Application(elements); + app.init(); + + // Make app globally available for debugging + window.app = app; }); export { Application }; |
